diff options
| author | Silarn <jrim@rimpo.org> | 2019-07-22 01:00:42 -0500 |
|---|---|---|
| committer | Silarn <jrim@rimpo.org> | 2019-07-22 01:00:42 -0500 |
| commit | dcd6d624672019727d7effd17aac86f72bff438b (patch) | |
| tree | 1e8d3856f657d898c5992631599cf272d785f973 | |
| parent | 179a73857125ee604f42b0d5c2d765183c86d2c7 (diff) | |
| parent | e73c309f08eff98f0dbd2590f594a83b67431eac (diff) | |
Merge branch 'Develop'
135 files changed, 20164 insertions, 6625 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c5f58a7..94c76373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,15 +1,33 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.8.12) -ADD_COMPILE_OPTIONS($<$<CXX_COMPILER_ID:MSVC>:/MP>) +ADD_COMPILE_OPTIONS($<$<CXX_COMPILER_ID:MSVC>:/MP> $<$<CXX_COMPILER_ID:MSVC>:$<$<CONFIG:RELEASE>:/O2>> $<$<CXX_COMPILER_ID:MSVC>:$<$<CONFIG:RELWITHDEBINFO>:/O2>>) PROJECT(organizer) # sets ModOrganizer as StartUp Project in Visual Studio set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY VS_STARTUP_PROJECT "ModOrganizer") +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +set(CMAKE_INSTALL_MESSAGE NEVER) + SET(DEPENDENCIES_DIR CACHE PATH "") # hint to find qt in dependencies path LIST(APPEND CMAKE_PREFIX_PATH ${QT_ROOT}/lib/cmake) LIST(APPEND CMAKE_PREFIX_PATH ${LZ4_ROOT}/dll) ADD_SUBDIRECTORY(src) + +set(vcxproj_user_file "${CMAKE_CURRENT_BINARY_DIR}/INSTALL.vcxproj.user") +if(NOT EXISTS ${vcxproj_user_file}) + FILE(WRITE ${vcxproj_user_file} + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<Project ToolsVersion=\"Current\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n" + "<PropertyGroup>\n" + "<LocalDebuggerWorkingDirectory>${CMAKE_INSTALL_PREFIX}/bin</LocalDebuggerWorkingDirectory>\n" + "<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>" + "<LocalDebuggerCommand>${CMAKE_INSTALL_PREFIX}/bin/ModOrganizer.exe</LocalDebuggerCommand>\n" + "</PropertyGroup>\n" + "</Project>\n") +endif() + +INSTALL(FILES dump_running_process.bat DESTINATION bin) diff --git a/appveyor.yml b/appveyor.yml index 2992614d..4cf48be9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,11 @@ version: 1.0.{build} skip_branch_with_pr: true -image: Visual Studio 2017 +image: Visual Studio 2019 environment: WEBHOOK_URL: secure: gOKbXaZM9ImtMD5XrYITvdyZUW/az082G9OIN1EC1VYoNNsKROB4s6VsQiL6qbLNLc4XQhc8Y9DhdU9Hfh49vhypcjUEQmMsENxnp+1hYMJctTqIYDunVSd767eez5wz6rKHqk+XOHmg5gyVNCV9u4DVcCZJSVe4UTKn1lc47H0= +build: + parallel: true build_script: - cmd: >- git clone --depth=1 --branch=%APPVEYOR_REPO_BRANCH% https://github.com/ModOrganizer2/modorganizer-umbrella.git c:\projects\modorganizer-umbrella 2> $null @@ -14,9 +16,9 @@ build_script: C:\Python37-x64\python.exe unimake.py -d c:\projects\modorganizer-build -s Appveyor_Build=True %APPVEYOR_PROJECT_NAME% artifacts: -- path: vsbuild\src\RelWithDebInfo\ModOrganizer.exe +- path: build\src\ModOrganizer.exe name: modorganizer_exe -- path: vsbuild\src\RelWithDebInfo\ModOrganizer.pdb +- path: build\src\ModOrganizer.pdb name: modorganizer_pdb on_success: - ps: Set-Location -Path $env:APPVEYOR_BUILD_FOLDER @@ -24,5 +26,7 @@ on_success: - ps: ./send.ps1 success $env:WEBHOOK_URL on_failure: - ps: Set-Location -Path $env:APPVEYOR_BUILD_FOLDER + - ps: Push-AppveyorArtifact ${env:APPVEYOR_BUILD_FOLDER}\stdout.log + - ps: Push-AppveyorArtifact ${env:APPVEYOR_BUILD_FOLDER}\stderr.log - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - - ps: ./send.ps1 failure $env:WEBHOOK_URL
\ No newline at end of file + - ps: ./send.ps1 failure $env:WEBHOOK_URL diff --git a/dump_running_process.bat b/dump_running_process.bat new file mode 100644 index 00000000..4697fe5e --- /dev/null +++ b/dump_running_process.bat @@ -0,0 +1,2 @@ +pushd "%~dp0" +start ModOrganizer.exe --crashdump diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8b368257..f197211a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,6 +9,28 @@ IF(CMAKE_SIZEOF_VOID_P EQUAL 8) SET(PROJ_ARCH x64) ENDIF() +macro(add_msvc_precompiled_header PrecompiledHeader PrecompiledSource SourcesVar HeadersVar) + if(MSVC) + get_filename_component(PrecompiledBasename ${PrecompiledHeader} NAME_WE) + set(PrecompiledBinary "${CMAKE_CURRENT_BINARY_DIR}/${PrecompiledBasename}.pch") + set(Sources ${${SourcesVar}}) + + set_source_files_properties( + ${PrecompiledSource} PROPERTIES + COMPILE_FLAGS "/Yc\"${PrecompiledHeader}\" /Fp\"${PrecompiledBinary}\"" + OBJECT_OUTPUTS "${PrecompiledBinary}") + + set_source_files_properties( + ${Sources} PROPERTIES + COMPILE_FLAGS "/Yu\"${PrecompiledHeader}\" /FI\"${PrecompiledHeader}\" /Fp\"${PrecompiledBinary}\"" + OBJECT_DEPENDS "${PrecompiledBinary}") + + list(APPEND ${SourcesVar} ${PrecompiledSource}) + list(APPEND ${HeadersVar} ${PrecompiledHeader}) + endif(MSVC) +endmacro(add_msvc_precompiled_header) + + SET(organizer_SRCS transfersavesdialog.cpp syncoverwritedialog.cpp @@ -32,6 +54,14 @@ SET(organizer_SRCS modlist.cpp modidlineedit.cpp modinfodialog.cpp + modinfodialogcategories.cpp + modinfodialogconflicts.cpp + modinfodialogesps.cpp + modinfodialogfiletree.cpp + modinfodialogimages.cpp + modinfodialognexus.cpp + modinfodialogtab.cpp + modinfodialogtextfiles.cpp modinfo.cpp modinfobackup.cpp modinfoforeign.cpp @@ -93,6 +123,12 @@ SET(organizer_SRCS lcdnumber.cpp forcedloaddialog.cpp forcedloaddialogwidget.cpp + filterwidget.cpp + statusbar.cpp + apiuseraccount.cpp + filerenamer.cpp + texteditor.cpp + expanderwidget.cpp shared/windows_error.cpp shared/error_report.cpp @@ -126,6 +162,15 @@ SET(organizer_HDRS modlist.h modidlineedit.h modinfodialog.h + modinfodialogcategories.h + modinfodialogconflicts.h + modinfodialogesps.h + modinfodialogfiletree.h + modinfodialogfwd.h + modinfodialogimages.h + modinfodialognexus.h + modinfodialogtab.h + modinfodialogtextfiles.h modinfo.h modinfobackup.h modinfoforeign.h @@ -183,12 +228,17 @@ SET(organizer_HDRS instancemanager.h usvfsconnector.h eventfilter.h - descriptionpage.h moshortcut.h listdialog.h lcdnumber.h forcedloaddialog.h forcedloaddialogwidget.h + filterwidget.h + statusbar.h + apiuseraccount.h + filerenamer.h + texteditor.h + expanderwidget.h shared/windows_error.h shared/error_report.h @@ -203,7 +253,6 @@ SET(organizer_HDRS SET(organizer_UIS transfersavesdialog.ui syncoverwritedialog.ui - simpleinstalldialog.ui settingsdialog.ui selectiondialog.ui queryoverwritedialog.ui @@ -215,8 +264,6 @@ SET(organizer_UIS mainwindow.ui lockeddialog.ui waitingonclosedialog.ui - installdialog.ui - finddialog.ui editexecutablesdialog.ui credentialsdialog.ui categoriesdialog.ui @@ -243,16 +290,191 @@ SET(organizer_RCS ) -SOURCE_GROUP(Source FILES ${organizer_SRCS}) -SOURCE_GROUP(Headers FILES ${organizer_HDRS}) -SOURCE_GROUP(UI FILES ${organizer_UIS}) +source_group(src REGULAR_EXPRESSION ".*\\.(h|cpp|ui)") + +set(application + iuserinterface + main + mainwindow + moapplication + moshortcut + selfupdater + singleinstance + statusbar +) + +set(browser + browserdialog + browserview +) + +set(core + categories + shared/directoryentry + directoryrefresher + installationmanager + instancemanager + loadmechanism + nexusinterface + nxmaccessmanager + organizercore + organizerproxy + apiuseraccount +) + +set(dialogs + aboutdialog + activatemodsdialog + categoriesdialog + credentialsdialog + filedialogmemory + forcedloaddialog + forcedloaddialogwidget + listdialog + messagedialog + motddialog + overwriteinfodialog + queryoverwritedialog + problemsdialog + savetextasdialog + selectiondialog + syncoverwritedialog + transfersavesdialog + waitingonclosedialog +) + +set(downloads + downloadlist + downloadlistsortproxy + downloadlistwidget + downloadmanager +) + +set(executables + executableslist + editexecutablesdialog +) + +set(locking + ilockedwaitingforprocess + lockeddialog + lockeddialogbase +) + +set(modinfo + modinfo + modinfobackup + modinfoforeign + modinfooverwrite + modinforegular + modinfoseparator + modinfowithconflictinfo +) + +set(modinfo\\dialog + modinfodialog + modinfodialogcategories + modinfodialogconflicts + modinfodialogesps + modinfodialogfiletree + modinfodialogfwd + modinfodialogimages + modinfodialognexus + modinfodialogtab + modinfodialogtextfiles +) +set(modlist + modlist + modlistsortproxy + modlistview +) + +set(plugins + plugincontainer + pluginlist + pluginlistsortproxy + pluginlistview +) + +set(previews + previewdialog + previewgenerator +) + +set(profiles + profile + profileinputdialog + profilesdialog +) + +set(settings + settings + settingsdialog +) + +set(utilities + shared/appconfig + bbcode + csvbuilder + shared/error_report + eventfilter + helper + shared/leaktrace + persistentcookiejar + serverinfo + spawn + shared/stackdata + shared/util + usvfsconnector + shared/windows_error +) + +set(widgets + expanderwidget + genericicondelegate + filerenamer + filterwidget + icondelegate + lcdnumber + logbuffer + loghighlighter + modflagicondelegate + modidlineedit + noeditdelegate + qtgroupingproxy + texteditor + viewmarkingscrollbar +) + +set(src_filters + application core browser dialogs downloads executables locking modinfo modinfo\\dialog + modlist plugins previews profiles settings utilities widgets +) + +foreach(filter in list ${src_filters}) + set(files) + foreach(d in lists ${${filter}}) + set(files ${files} ${d}.cpp ${d}.h ${d}.inc ${d}.ui) + endforeach() + + source_group(src\\${filter} FILES ${files}) +endforeach() + +file(GLOB_RECURSE rule_files ../vsbuild/CMakeFiles/*.rule) +file(GLOB qm_files ../vsbuild/src/*.qm) + +source_group(cmake FILES CMakeLists.txt) +source_group(autogen FILES ${rule_files} ${qm_files}) +source_group(resources FILES ${organizer_RCS} ${organizer_QRCS}) # MO projects SET(dependency_project_path "${DEPENDENCIES_DIR}") SET(default_project_path "${DEPENDENCIES_DIR}/modorganizer_super") GET_FILENAME_COMPONENT(${default_project_path} ${default_project_path} REALPATH) - SET(project_path "${default_project_path}" CACHE PATH "path to the other mo projects") + +add_msvc_precompiled_header("pch.h" "pch.cpp" organizer_SRCS organizer_HDRS) + #TODO this should not be a hardcoded path SET(lib_path "${project_path}/../../install/libs") @@ -260,6 +482,8 @@ SET(lib_path "${project_path}/../../install/libs") SET(CMAKE_INCLUDE_CURRENT_DIR ON) SET(CMAKE_AUTOMOC ON) SET(CMAKE_AUTOUIC ON) +SET(CMAKE_AUTORCC ON) + FIND_PACKAGE(Qt5Widgets REQUIRED) FIND_PACKAGE(Qt5QuickWidgets REQUIRED) FIND_PACKAGE(Qt5Quick REQUIRED) @@ -269,10 +493,14 @@ FIND_PACKAGE(Qt5WebEngineWidgets REQUIRED) FIND_PACKAGE(Qt5WebSockets REQUIRED) FIND_PACKAGE(Qt5Qml REQUIRED) FIND_PACKAGE(Qt5LinguistTools) -QT5_WRAP_UI(organizer_UIHDRS ${organizer_UIS}) -QT5_ADD_RESOURCES(organizer_RCCPPS ${organizer_QRCS}) + SET(mo_translation_sources ${CMAKE_SOURCE_DIR}/src ${project_path}/uibase/src) -QT5_CREATE_TRANSLATION(organizer_translations_qm ${mo_translation_sources} ${CMAKE_SOURCE_DIR}/src/organizer_en.ts) +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP autogen) +set_property(GLOBAL PROPERTY AUTOMOC_SOURCE_GROUP autogen) +set_property(GLOBAL PROPERTY AUTORCC_SOURCE_GROUP autogen) + +QT5_CREATE_TRANSLATION(organizer_translations_qm ${mo_translation_sources} ${CMAKE_SOURCE_DIR}/src/organizer_en.ts OPTIONS -silent) ADD_CUSTOM_TARGET(translations DEPENDS ${organizer_translations_qm}) INCLUDE_DIRECTORIES(${Qt5Declarative_INCLUDES}) @@ -301,12 +529,12 @@ INCLUDE_DIRECTORIES(${project_path}/uibase/src ${project_path}/game_gamebryo/src/creation ${project_path}/game_features/src ${project_path}/githubpp/src - ${LZ4_ROOT}/include) + ${LZ4_ROOT}/lib) INCLUDE_DIRECTORIES(shared ${ZLIB_INCLUDE_DIRS}) LINK_DIRECTORIES(${lib_path} ${ZLIB_ROOT}/lib - ${LZ4_ROOT}/dll) + ${LZ4_ROOT}/bin) EXECUTE_PROCESS( COMMAND git log -1 --format=%h WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} @@ -322,7 +550,7 @@ ELSE() SET(usvfs_name usvfs_x86) ENDIF() -ADD_EXECUTABLE(ModOrganizer WIN32 ${organizer_HDRS} ${organizer_SRCS} ${organizer_UIHDRS} ${organizer_RCS} ${organizer_RCCPPS} ${organizer_translations_qm}) +ADD_EXECUTABLE(ModOrganizer WIN32 ${organizer_HDRS} ${organizer_SRCS} ${organizer_UIS} ${organizer_RCS} ${organizer_QRCS} ${organizer_translations_qm}) TARGET_LINK_LIBRARIES(ModOrganizer Qt5::Widgets Qt5::WinExtras Qt5::WebEngineWidgets Qt5::Quick Qt5::Qml Qt5::QuickWidgets Qt5::Network Qt5::WebSockets @@ -333,7 +561,7 @@ TARGET_LINK_LIBRARIES(ModOrganizer Dbghelp advapi32 Version Shlwapi liblz4 Crypt32) IF (MSVC) - SET_TARGET_PROPERTIES(ModOrganizer PROPERTIES COMPILE_FLAGS "/std:c++latest") + SET_TARGET_PROPERTIES(ModOrganizer PROPERTIES COMPILE_FLAGS "/std:c++latest /permissive-") ENDIF() IF (MSVC AND "${CMAKE_SIZEOF_VOID_P}" EQUAL 4) # 32 bits @@ -375,11 +603,11 @@ SET(windeploy_parameters "--no-translations --plugindir qtplugins --libdir dlls INSTALL( CODE "EXECUTE_PROCESS(COMMAND - ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets --websockets ${windeploy_parameters} + ${qt5bin}/windeployqt.exe --verbose 0 ModOrganizer.exe --webenginewidgets --websockets ${windeploy_parameters} WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin ) EXECUTE_PROCESS(COMMAND - ${qt5bin}/windeployqt.exe uibase.dll ${windeploy_parameters} + ${qt5bin}/windeployqt.exe --verbose 0 uibase.dll ${windeploy_parameters} WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin ) file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/platforms) diff --git a/src/aboutdialog.cpp b/src/aboutdialog.cpp index 9d68fe2b..41fd24f7 100644 --- a/src/aboutdialog.cpp +++ b/src/aboutdialog.cpp @@ -115,7 +115,7 @@ void AboutDialog::on_creditsList_currentItemChanged(QListWidgetItem *current, QL }
}
-void AboutDialog::on_copyrightText_linkActivated(const QString &link)
+void AboutDialog::on_sourceText_linkActivated(const QString &link)
{
emit linkClicked(link);
-}
\ No newline at end of file +}
diff --git a/src/aboutdialog.h b/src/aboutdialog.h index 285d0066..9b9b6102 100644 --- a/src/aboutdialog.h +++ b/src/aboutdialog.h @@ -74,7 +74,7 @@ private: private slots:
void on_creditsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_copyrightText_linkActivated(const QString &link);
+ void on_sourceText_linkActivated(const QString &link);
private:
diff --git a/src/aboutdialog.ui b/src/aboutdialog.ui index 86b95c83..c72a85f8 100644 --- a/src/aboutdialog.ui +++ b/src/aboutdialog.ui @@ -120,20 +120,43 @@ <item>
<widget class="QLabel" name="label_3">
<property name="text">
- <string notr="true"><html><head/><body><p>Copyright 2011-2016 Sebastian Herbord</p><p>Copyright 2016-2018 Mod Organizer 2 contributors</p></body></html></string>
+ <string notr="true"><html><head/><body><p>Copyright © 2011-2016 Sebastian Herbord<br/>Copyright © 2016-2019 Mod Organizer 2 Contributors</p></body></html></string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="copyrightText">
<property name="text">
- <string notr="true"><html><head/><body><p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p><p>See the GNU General Public License for more details.</p><p>Source code can be found at <a href="https://github.com/Modorganizer2/modorganizer"><span style=" text-decoration: underline; color:#007af4;">GitHub</span></a>.</p></body></html></string>
+ <string notr="true"><html><head/><body><p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p><p>See the GNU General Public License for more details.</p></body></html></string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
+ <item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="sourceText">
+ <property name="text">
+ <string><html><head/><body><p>Source code can be found at <a href="https://github.com/ModOrganizer2/modorganizer"><span style=" text-decoration: underline; color:#007af4;">GitHub</span></a>.</p></body></html></string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
<widget class="QWidget" name="software">
@@ -157,7 +180,7 @@ <item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
- <string>Lead Developers/ Maintainers</string>
+ <string>Lead Developers && Maintainers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
@@ -177,11 +200,6 @@ </item>
<item>
<property name="text">
- <string notr="true">erasmux</string>
- </property>
- </item>
- <item>
- <property name="text">
<string notr="true">Silarn</string>
</property>
</item>
@@ -198,7 +216,7 @@ <item row="1" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
- <string>Mo2 devs and Contributors</string>
+ <string>MO2 Developers && Contributors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7" stretch="0">
<item>
@@ -213,12 +231,22 @@ </item>
<item>
<property name="text">
- <string>Project579</string>
+ <string notr="true">erasmux</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">isanae</string>
</property>
</item>
<item>
<property name="text">
- <string>przester</string>
+ <string notr="true">Project579</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">przester</string>
</property>
</item>
</widget>
@@ -294,6 +322,11 @@ </item>
<item>
<property name="text">
+ <string>yohru (Japanese)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
<string>Mordan (Greek)</string>
</property>
</item>
@@ -342,12 +375,12 @@ </item>
<item>
<property name="text">
- <string>yohru (Japanese)</string>
+ <string>Nubbie (Swedish)</string>
</property>
</item>
<item>
<property name="text">
- <string>...and all other Transifex contributors!</string>
+ <string>...and all other contributors!</string>
</property>
<property name="toolTip">
<string/>
diff --git a/src/apiuseraccount.cpp b/src/apiuseraccount.cpp new file mode 100644 index 00000000..b901e41a --- /dev/null +++ b/src/apiuseraccount.cpp @@ -0,0 +1,67 @@ +#include "apiuseraccount.h" + +APIUserAccount::APIUserAccount() + : m_type(APIUserAccountTypes::None) +{ +} + +const QString& APIUserAccount::id() const +{ + return m_id; +} + +const QString& APIUserAccount::name() const +{ + return m_name; +} + +APIUserAccountTypes APIUserAccount::type() const +{ + return m_type; +} + +const APILimits& APIUserAccount::limits() const +{ + return m_limits; +} + +APIUserAccount& APIUserAccount::id(const QString& id) +{ + m_id = id; + return *this; +} + +APIUserAccount& APIUserAccount::name(const QString& name) +{ + m_name = name; + return *this; +} + +APIUserAccount& APIUserAccount::type(APIUserAccountTypes type) +{ + m_type = type; + return *this; +} + +APIUserAccount& APIUserAccount::limits(const APILimits& limits) +{ + m_limits = limits; + return *this; +} + +int APIUserAccount::remainingRequests() const +{ + return std::max( + m_limits.remainingDailyRequests, + m_limits.remainingHourlyRequests); +} + +bool APIUserAccount::shouldThrottle() const +{ + return (remainingRequests() < ThrottleThreshold); +} + +bool APIUserAccount::exhausted() const +{ + return (remainingRequests() <= 0); +} diff --git a/src/apiuseraccount.h b/src/apiuseraccount.h new file mode 100644 index 00000000..8a238d71 --- /dev/null +++ b/src/apiuseraccount.h @@ -0,0 +1,129 @@ +#ifndef APIUSERACCOUNT_H +#define APIUSERACCOUNT_H + +#include <QString> + +/** +* represents user account types on a mod provider website such as nexus +*/ +enum class APIUserAccountTypes +{ + // not logged in + None = 0, + + // regular account + Regular, + + // premium account + Premium +}; + + +/** +* current limits imposed on the user account +**/ +struct APILimits +{ + // maximum number of requests per day + int maxDailyRequests = 0; + + // remaining number of requests today + int remainingDailyRequests = 0; + + // maximum number of requests per hour + int maxHourlyRequests = 0; + + // remaining number of requests this hour + int remainingHourlyRequests = 0; +}; + + +/** +* API statistics +*/ +struct APIStats +{ + // number of API requests currently queued + int requestsQueued = 0; +}; + + +/** +* represents a user account on the mod provier website +*/ +class APIUserAccount +{ +public: + // when the number of remanining requests is under this number, further + // requests will be throttled by avoiding non-critical ones + static const int ThrottleThreshold = 200; + + APIUserAccount(); + + /** + * user id + */ + const QString& id() const; + + /** + * user name + */ + const QString& name() const; + + /** + * account type + */ + APIUserAccountTypes type() const; + + /** + * current API limits + */ + const APILimits& limits() const; + + + /** + * sets the user id + */ + APIUserAccount& id(const QString& id); + + /** + * sets the user name + **/ + APIUserAccount& name(const QString& name); + + /** + * sets the acount type + */ + APIUserAccount& type(APIUserAccountTypes type); + + /** + * sets the current limits + */ + APIUserAccount& limits(const APILimits& limits); + + + /** + * returns the number of remaining requests + */ + int remainingRequests() const; + + /** + * whether the number of remaining requests is low enough that further + * requests should be throttled + */ + bool shouldThrottle() const; + + /** + * true if all the remaining requests have been used and the API will refuse + * further requests + */ + bool exhausted() const; + +private: + QString m_id, m_name; + APIUserAccountTypes m_type; + APILimits m_limits; + APIStats m_stats; +}; + +#endif // APIUSERACCOUNT_H diff --git a/src/bbcode.cpp b/src/bbcode.cpp index 3475f1b2..9f064106 100644 --- a/src/bbcode.cpp +++ b/src/bbcode.cpp @@ -174,7 +174,7 @@ private: "<a href=\"\\1\">\\1</a>");
m_TagMap["url="] = std::make_pair(QRegExp("\\[url=([^\\]]*)\\](.*)\\[/url\\]"),
"<a href=\"\\1\">\\2</a>");
- m_TagMap["img"] = std::make_pair(QRegExp("\\[img\\](.*)\\[/img\\]"),
+ m_TagMap["img"] = std::make_pair(QRegExp("\\[img(?:\\s*width=\\d+\\s*,?\\s*height=\\d+)?\\](.*)\\[/img\\]"),
"<img src=\"\\1\">");
m_TagMap["img="] = std::make_pair(QRegExp("\\[img=([^\\]]*)\\](.*)\\[/img\\]"),
"<img src=\"\\2\" alt=\"\\1\">");
diff --git a/src/descriptionpage.h b/src/descriptionpage.h deleted file mode 100644 index f6158ee0..00000000 --- a/src/descriptionpage.h +++ /dev/null @@ -1,28 +0,0 @@ -#include <QWebEnginePage> - -#ifndef DESCRIPTIONPAGE_H -#define DESCRIPTIONPAGE_H - -class DescriptionPage : public QWebEnginePage -{ - Q_OBJECT - -public: - DescriptionPage(QObject* parent = 0) : QWebEnginePage(parent){} - - bool acceptNavigationRequest(const QUrl & url, QWebEnginePage::NavigationType type, bool isMainFrame) - { - if (type == QWebEnginePage::NavigationTypeLinkClicked) - { - emit linkClicked(url); - return false; - } - return true; - } - -signals: - void linkClicked(const QUrl&); - -}; - -#endif //DESCRIPTIONPAGE_H diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp index dd72abbd..5e698e0e 100644 --- a/src/downloadlist.cpp +++ b/src/downloadlist.cpp @@ -47,7 +47,7 @@ int DownloadList::rowCount(const QModelIndex&) const int DownloadList::columnCount(const QModelIndex&) const
{
- return 4;
+ return COL_COUNT;
}
@@ -69,6 +69,9 @@ QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int (orientation == Qt::Horizontal)) {
switch (section) {
case COL_NAME: return tr("Name");
+ case COL_MODNAME: return tr("Mod name");
+ case COL_VERSION: return tr("Version");
+ case COL_ID: return tr("Nexus ID");
case COL_SIZE: return tr("Size");
case COL_STATUS: return tr("Status");
case COL_FILETIME: return tr("Filetime");
@@ -93,6 +96,30 @@ QVariant DownloadList::data(const QModelIndex &index, int role) const } else {
switch (index.column()) {
case COL_NAME: return m_MetaDisplay ? m_Manager->getDisplayName(index.row()) : m_Manager->getFileName(index.row());
+ case COL_MODNAME: {
+ if (m_Manager->isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(index.row());
+ return info->modName;
+ }
+ }
+ case COL_VERSION: {
+ if (m_Manager->isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(index.row());
+ return info->version.canonicalString();
+ }
+ }
+ case COL_ID: {
+ if (m_Manager->isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(index.row());
+ return QString("%1").arg(m_Manager->getModID(index.row()));
+ }
+ }
case COL_SIZE: return sizeFormat(m_Manager->getFileSize(index.row()));
case COL_FILETIME: return m_Manager->getFileTime(index.row());
case COL_STATUS:
@@ -143,10 +170,10 @@ QVariant DownloadList::data(const QModelIndex &index, int role) const && m_Manager->isInfoIncomplete(index.row()))
return QIcon(":/MO/gui/warning_16");
} else if (role == Qt::TextAlignmentRole) {
- if (index.column() == COL_NAME)
- return QVariant(Qt::AlignVCenter | Qt::AlignLeft);
- else
+ if (index.column() == COL_SIZE)
return QVariant(Qt::AlignVCenter | Qt::AlignRight);
+ else
+ return QVariant(Qt::AlignVCenter | Qt::AlignLeft);
}
return QVariant();
}
diff --git a/src/downloadlist.h b/src/downloadlist.h index bf0cdc37..6f63f0c8 100644 --- a/src/downloadlist.h +++ b/src/downloadlist.h @@ -39,7 +39,13 @@ public: COL_NAME = 0,
COL_STATUS,
COL_SIZE,
- COL_FILETIME
+ COL_FILETIME,
+ COL_MODNAME,
+ COL_VERSION,
+ COL_ID,
+
+ // number of columns
+ COL_COUNT
};
public:
diff --git a/src/downloadlistsortproxy.cpp b/src/downloadlistsortproxy.cpp index dc97dc3e..7bda139b 100644 --- a/src/downloadlistsortproxy.cpp +++ b/src/downloadlistsortproxy.cpp @@ -43,6 +43,48 @@ bool DownloadListSortProxy::lessThan(const QModelIndex &left, && (rightIndex < m_Manager->numTotalDownloads())) {
if (left.column() == DownloadList::COL_NAME) {
return m_Manager->getFileName(left.row()).compare(m_Manager->getFileName(right.row()), Qt::CaseInsensitive) < 0;
+ } else if (left.column() == DownloadList::COL_MODNAME) {
+ QString leftName, rightName;
+
+ if (!m_Manager->isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
+ leftName = info->modName;
+ }
+
+ if (!m_Manager->isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
+ rightName = info->modName;
+ }
+
+ return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
+ } else if (left.column() == DownloadList::COL_VERSION) {
+ MOBase::VersionInfo versionLeft, versionRight;
+
+ if (!m_Manager->isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
+ versionLeft = info->version;
+ }
+
+ if (!m_Manager->isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
+ versionRight = info->version;
+ }
+
+ return versionLeft < versionRight;
+ } else if (left.column() == DownloadList::COL_ID) {
+ int leftID=0, rightID=0;
+
+ if (!m_Manager->isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
+ leftID = info->modID;
+ }
+
+ if (!m_Manager->isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
+ rightID = info->modID;
+ }
+
+ return leftID < rightID;
} else if (left.column() == DownloadList::COL_STATUS) {
DownloadManager::DownloadState leftState = m_Manager->getState(left.row());
DownloadManager::DownloadState rightState = m_Manager->getState(right.row());
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp index f0c4da1a..c75ecdd2 100644 --- a/src/downloadlistwidget.cpp +++ b/src/downloadlistwidget.cpp @@ -123,6 +123,15 @@ DownloadListWidget::~DownloadListWidget() void DownloadListWidget::setManager(DownloadManager *manager)
{
m_Manager = manager;
+
+ // hide these columns by default
+ //
+ // note that this is overridden by the ini if MO has been started at least
+ // once before, which is handled in MainWindow::processUpdates() for older
+ // versions
+ header()->hideSection(DownloadList::COL_MODNAME);
+ header()->hideSection(DownloadList::COL_VERSION);
+ header()->hideSection(DownloadList::COL_ID);
}
void DownloadListWidget::setSourceModel(DownloadList *sourceModel)
@@ -199,7 +208,7 @@ void DownloadListWidget::onCustomContextMenu(const QPoint &point) if (state >= DownloadManager::STATE_READY) {
menu.addAction(tr("Install"), this, SLOT(issueInstall()));
- if (m_Manager->isInfoIncomplete(m_ContextRow))
+ if (m_Manager->isInfoIncomplete(m_ContextRow))
menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
else
menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
@@ -226,9 +235,9 @@ void DownloadListWidget::onCustomContextMenu(const QPoint &point) menu.addSeparator();
}
- menu.addAction(tr("Delete Installed..."), this, SLOT(issueDeleteCompleted()));
- menu.addAction(tr("Delete Uninstalled..."), this, SLOT(issueDeleteUninstalled()));
- menu.addAction(tr("Delete All..."), this, SLOT(issueDeleteAll()));
+ menu.addAction(tr("Delete Installed Downloads..."), this, SLOT(issueDeleteCompleted()));
+ menu.addAction(tr("Delete Uninstalled Downloads..."), this, SLOT(issueDeleteUninstalled()));
+ menu.addAction(tr("Delete All Downloads..."), this, SLOT(issueDeleteAll()));
menu.addSeparator();
if (!hidden) {
@@ -259,8 +268,8 @@ void DownloadListWidget::issueQueryInfoMd5() void DownloadListWidget::issueDelete()
{
- if (QMessageBox::question(nullptr, tr("Delete Files?"),
- tr("This will permanently delete the selected download."),
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will permanently delete the selected download.\n\nAre you absolutely sure you want to proceed?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(m_ContextRow, true);
}
@@ -314,8 +323,8 @@ void DownloadListWidget::issueResume() void DownloadListWidget::issueDeleteAll()
{
- if (QMessageBox::question(nullptr, tr("Delete Files?"),
- tr("This will remove all finished downloads from this list and from disk."),
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will remove all finished downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-1, true);
}
@@ -323,8 +332,8 @@ void DownloadListWidget::issueDeleteAll() void DownloadListWidget::issueDeleteCompleted()
{
- if (QMessageBox::question(nullptr, tr("Delete Files?"),
- tr("This will remove all installed downloads from this list and from disk."),
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will remove all installed downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-2, true);
}
@@ -332,8 +341,8 @@ void DownloadListWidget::issueDeleteCompleted() void DownloadListWidget::issueDeleteUninstalled()
{
- if (QMessageBox::question(nullptr, tr("Delete Files?"),
- tr("This will remove all uninstalled downloads from this list and from disk."),
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will remove all uninstalled downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-3, true);
}
@@ -341,7 +350,7 @@ void DownloadListWidget::issueDeleteUninstalled() void DownloadListWidget::issueRemoveFromViewAll()
{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
+ if (QMessageBox::question(nullptr, tr("Hide Files?"),
tr("This will remove all finished downloads from this list (but NOT from disk)."),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-1, false);
@@ -350,7 +359,7 @@ void DownloadListWidget::issueRemoveFromViewAll() void DownloadListWidget::issueRemoveFromViewCompleted()
{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
+ if (QMessageBox::question(nullptr, tr("Hide Files?"),
tr("This will remove all installed downloads from this list (but NOT from disk)."),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-2, false);
@@ -359,7 +368,7 @@ void DownloadListWidget::issueRemoveFromViewCompleted() void DownloadListWidget::issueRemoveFromViewUninstalled()
{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
+ if (QMessageBox::question(nullptr, tr("Hide Files?"),
tr("This will remove all uninstalled downloads from this list (but NOT from disk)."),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-3, false);
diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 25542ef3..648b102a 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -549,10 +549,21 @@ void DownloadManager::addNXMDownload(const QString &url) NXMUrl nxmInfo(url); QStringList validGames; + MOBase::IPluginGame* foundGame = nullptr; validGames.append(m_ManagedGame->gameShortName()); validGames.append(m_ManagedGame->validShortNames()); + for (auto game : validGames) { + MOBase::IPluginGame* gamePlugin = m_OrganizerCore->getGame(game); + if ( + nxmInfo.game().compare(gamePlugin->gameShortName(), Qt::CaseInsensitive) == 0 || + nxmInfo.game().compare(gamePlugin->gameNexusName(), Qt::CaseInsensitive) == 0 + ) { + foundGame = gamePlugin; + break; + } + } qDebug("add nxm download: %s", qUtf8Printable(url)); - if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) { + if (foundGame == nullptr) { qDebug("download requested for wrong game (game: %s, url: %s)", qUtf8Printable(m_ManagedGame->gameShortName()), qUtf8Printable(nxmInfo.game())); QMessageBox::information(nullptr, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO " "has been set up for \"%2\".").arg(nxmInfo.game()).arg(m_ManagedGame->gameShortName()), QMessageBox::Ok); @@ -560,7 +571,7 @@ void DownloadManager::addNXMDownload(const QString &url) } for (auto tuple : m_PendingDownloads) { - if (std::get<0>(tuple).compare(nxmInfo.game(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) { + if (std::get<0>(tuple).compare(foundGame->gameShortName(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) { QString debugStr("download requested is already queued (mod: %1, file: %2)"); QString infoStr(tr("There is already a download queued for this file.\n\nMod %1\nFile %2")); @@ -620,7 +631,7 @@ void DownloadManager::addNXMDownload(const QString &url) emit aboutToUpdate(); - m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId())); + m_PendingDownloads.append(std::make_tuple(foundGame->gameShortName(), nxmInfo.modId(), nxmInfo.fileId())); emit update(-1); emit downloadAdded(); @@ -628,9 +639,10 @@ void DownloadManager::addNXMDownload(const QString &url) info->nexusKey = nxmInfo.key(); info->nexusExpires = nxmInfo.expires(); + info->nexusDownloadUser = nxmInfo.userId(); QObject *test = info; - m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, qVariantFromValue(test), "")); + m_RequestIDs.insert(m_NexusInterface->requestFileInfo(foundGame->gameShortName(), nxmInfo.modId(), nxmInfo.fileId(), this, qVariantFromValue(test), "")); } @@ -1029,14 +1041,14 @@ void DownloadManager::openFile(int index) reportError(tr("OpenFile: invalid download index %1").arg(index)); return; } + QDir path = QDir(m_OutputDirectory); if (path.exists(getFileName(index))) { - - ::ShellExecuteW(nullptr, L"open", ToWString(QDir::toNativeSeparators(getFilePath(index))).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::OpenFile(getFilePath(index)); return; } - ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OutputDirectory); return; } @@ -1046,23 +1058,22 @@ void DownloadManager::openInDownloadsFolder(int index) reportError(tr("OpenFileInDownloadsFolder: invalid download index %1").arg(index)); return; } - QString params = "/select,\""; - QDir path = QDir(m_OutputDirectory); - if (path.exists(getFileName(index))) { - params = params + QDir::toNativeSeparators(getFilePath(index)) + "\""; - ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); - return; - } - else if (path.exists(getFileName(index) + ".unfinished")) { - params = params + QDir::toNativeSeparators(getFilePath(index)+".unfinished") + "\""; + const auto path = getFilePath(index); - ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + if (QFile::exists(path)) { + shell::ExploreFile(path); return; } + else { + const auto unfinished = path + ".unfinished"; + if (QFile::exists(unfinished)) { + shell::ExploreFile(unfinished); + return; + } + } - ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; + shell::ExploreFile(m_OutputDirectory); } diff --git a/src/editexecutablesdialog.cpp b/src/editexecutablesdialog.cpp index 177661ff..8929d207 100644 --- a/src/editexecutablesdialog.cpp +++ b/src/editexecutablesdialog.cpp @@ -22,394 +22,677 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "filedialogmemory.h" #include "stackdata.h" #include "modlist.h" +#include "forcedloaddialog.h" +#include "organizercore.h" + #include <QMessageBox> #include <Shellapi.h> #include <utility.h> -#include "forcedloaddialog.h" #include <algorithm> using namespace MOBase; using namespace MOShared; -EditExecutablesDialog::EditExecutablesDialog( - const ExecutablesList &executablesList, const ModList &modList, - Profile *profile, const IPluginGame *game, QWidget *parent) +EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, QWidget* parent) : TutorableDialog("EditExecutables", parent) , ui(new Ui::EditExecutablesDialog) - , m_CurrentItem(nullptr) - , m_ExecutablesList(executablesList) - , m_Profile(profile) - , m_GamePlugin(game) + , m_organizerCore(oc) + , m_originalExecutables(*oc.executablesList()) + , m_executablesList(*oc.executablesList()) + , m_settingUI(false) { ui->setupUi(this); + ui->splitter->setSizes({200, 1}); + ui->splitter->setStretchFactor(0, 0); + ui->splitter->setStretchFactor(1, 1); - refreshExecutablesWidget(); + loadCustomOverwrites(); + loadForcedLibraries(); - ui->newFilesModBox->addItems(modList.allMods()); + ui->mods->addItems(m_organizerCore.modList()->allMods()); + fillList(); + setDirty(false); - m_ForcedLibraries = m_Profile->determineForcedLibraries(ui->titleEdit->text()); + // some widgets need to do more than just save() and have their own handler + connect(ui->binary, &QLineEdit::textChanged, [&]{ save(); }); + connect(ui->workingDirectory, &QLineEdit::textChanged, [&]{ save(); }); + connect(ui->arguments, &QLineEdit::textChanged, [&]{ save(); }); + connect(ui->steamAppID, &QLineEdit::textChanged, [&]{ save(); }); + connect(ui->mods, &QComboBox::currentTextChanged, [&]{ save(); }); + connect(ui->useApplicationIcon, &QCheckBox::toggled, [&]{ save(); }); + connect(ui->list->model(), &QAbstractItemModel::rowsMoved, [&]{ saveOrder(); }); } -EditExecutablesDialog::~EditExecutablesDialog() +EditExecutablesDialog::~EditExecutablesDialog() = default; + + +void EditExecutablesDialog::loadCustomOverwrites() { - delete ui; + const auto* p = m_organizerCore.currentProfile(); + + for (const auto& e : m_executablesList) { + const auto s = p->setting("custom_overwrites", e.title()).toString(); + + if (!s.isEmpty()) { + m_customOverwrites.set(e.title(), true, s); + } + } } -ExecutablesList EditExecutablesDialog::getExecutablesList() const +void EditExecutablesDialog::loadForcedLibraries() { - ExecutablesList newList; - for (int i = 0; i < ui->executablesListBox->count(); ++i) { - newList.addExecutable(m_ExecutablesList.find(ui->executablesListBox->item(i)->text())); + const auto* p = m_organizerCore.currentProfile(); + + for (const auto& e : m_executablesList) { + if (p->forcedLibrariesEnabled(e.title())) { + m_forcedLibraries.set(e.title(), true, p->determineForcedLibraries(e.title())); + } } - return newList; } -void EditExecutablesDialog::refreshExecutablesWidget() +ExecutablesList EditExecutablesDialog::getExecutablesList() const { - ui->executablesListBox->clear(); - std::vector<Executable>::const_iterator current, end; - m_ExecutablesList.getExecutables(current, end); + ExecutablesList newList; + + // make sure the executables are in the same order as in the list + for (int i = 0; i < ui->list->count(); ++i) { + const auto& title = ui->list->item(i)->text(); + + auto itor = m_executablesList.find(title); - for(; current != end; ++current) { - QListWidgetItem *newItem = new QListWidgetItem(current->m_Title); - newItem->setTextColor(current->isCustom() ? QColor(Qt::black) : QColor(Qt::darkGray)); - ui->executablesListBox->addItem(newItem); + if (itor == m_executablesList.end()) { + qWarning().nospace() + << "getExecutablesList(): executable '" << title << "' not found"; + + continue; + } + + newList.setExecutable(*itor); } - ui->addButton->setEnabled(false); - ui->removeButton->setEnabled(false); + return newList; } - -void EditExecutablesDialog::on_binaryEdit_textChanged(const QString &name) +const EditExecutablesDialog::CustomOverwrites& +EditExecutablesDialog::getCustomOverwrites() const { - updateButtonStates(); + return m_customOverwrites; } -void EditExecutablesDialog::on_workingDirEdit_textChanged(const QString &dir) +const EditExecutablesDialog::ForcedLibraries& +EditExecutablesDialog::getForcedLibraries() const { - updateButtonStates(); + return m_forcedLibraries; } -void EditExecutablesDialog::updateButtonStates() +void EditExecutablesDialog::commitChanges() { - bool enabled = true; + const auto newExecutables = getExecutablesList(); + auto* profile = m_organizerCore.currentProfile(); - QString filePath(ui->binaryEdit->text()); - QFileInfo fileInfo(filePath); - if (!fileInfo.exists()) - enabled = false; - if (!fileInfo.isFile()) - enabled = false; + // remove all the custom overwrites and forced libraries + for (const auto& e : m_originalExecutables) { + profile->removeSetting("custom_overwrites", e.title()); + profile->removeForcedLibraries(e.title()); + } + + // set the new custom overwrites and forced libraries + for (const auto& e : newExecutables) { + if (auto modName=m_customOverwrites.find(e.title())) { + if (modName && modName->enabled) { + profile->storeSetting("custom_overwrites", e.title(), modName->value); + } + } - QString dirPath(ui->workingDirEdit->text()); - if (!dirPath.isEmpty()) { - QDir dirInfo(dirPath); - if (!dirInfo.exists()) - enabled = false; + if (auto libraryList=m_forcedLibraries.find(e.title())) { + if (libraryList && libraryList->enabled && !libraryList->value.empty()) { + profile->setForcedLibrariesEnabled(e.title(), true); + profile->storeForcedLibraries(e.title(), libraryList->value); + } + } } - ui->addButton->setEnabled(enabled); + // set the new executables list + m_organizerCore.setExecutablesList(newExecutables); + + setDirty(false); } -void EditExecutablesDialog::resetInput() +void EditExecutablesDialog::setDirty(bool b) { - ui->binaryEdit->setText(""); - ui->titleEdit->setText(""); - ui->workingDirEdit->clear(); - ui->argumentsEdit->setText(""); - ui->appIDOverwriteEdit->clear(); - ui->overwriteAppIDBox->setChecked(false); - ui->useAppIconCheckBox->setChecked(false); - ui->newFilesModCheckBox->setChecked(false); - ui->forceLoadCheckBox->setChecked(false); - m_CurrentItem = nullptr; + if (auto* button=ui->buttons->button(QDialogButtonBox::Apply)) { + button->setEnabled(b); + } } - -void EditExecutablesDialog::saveExecutable() +QListWidgetItem* EditExecutablesDialog::selectedItem() { - m_ExecutablesList.updateExecutable( - ui->titleEdit->text(), - QDir::fromNativeSeparators(ui->binaryEdit->text()), - ui->argumentsEdit->text(), - QDir::fromNativeSeparators(ui->workingDirEdit->text()), - ui->overwriteAppIDBox->isChecked() ? - ui->appIDOverwriteEdit->text() : "", - Executable::UseApplicationIcon | Executable::CustomExecutable, - (ui->useAppIconCheckBox->isChecked() ? - Executable::UseApplicationIcon : Executable::Flags()) - | Executable::CustomExecutable); + const auto selection = ui->list->selectedItems(); - if (ui->newFilesModCheckBox->isChecked()) { - m_Profile->storeSetting("custom_overwrites", ui->titleEdit->text(), - ui->newFilesModBox->currentText()); - } - else { - m_Profile->removeSetting("custom_overwrites", ui->titleEdit->text()); + if (selection.empty()) { + return nullptr; } - m_Profile->removeForcedLibraries(ui->titleEdit->text()); - m_Profile->storeForcedLibraries(ui->titleEdit->text(), m_ForcedLibraries); - m_Profile->setForcedLibrariesEnabled(ui->titleEdit->text(), ui->forceLoadCheckBox->isChecked()); + return selection[0]; } - -void EditExecutablesDialog::delayedRefresh() +Executable* EditExecutablesDialog::selectedExe() { - QModelIndex index = ui->executablesListBox->currentIndex(); - resetInput(); - refreshExecutablesWidget(); - on_executablesListBox_clicked(index); -} + auto* item = selectedItem(); + if (!item) { + return nullptr; + } + + const auto& title = item->text(); + auto itor = m_executablesList.find(title); + if (itor == m_executablesList.end()) { + return nullptr; + } + + return &*itor; +} -void EditExecutablesDialog::on_forceLoadButton_clicked() +void EditExecutablesDialog::fillList() { - ForcedLoadDialog dialog(m_GamePlugin, this); - dialog.setValues(m_ForcedLibraries); - if (dialog.exec() == QDialog::Accepted) { - m_ForcedLibraries = dialog.values(); + ui->list->clear(); + + for(const auto& exe : m_executablesList) { + ui->list->addItem(createListItem(exe)); + } + + // select the first one in the list, if any + if (ui->list->count() > 0) { + ui->list->item(0)->setSelected(true); + } else { + updateUI(nullptr, nullptr); } } -void EditExecutablesDialog::on_forceLoadCheckBox_toggled() +QListWidgetItem* EditExecutablesDialog::createListItem(const Executable& exe) { - ui->forceLoadButton->setEnabled(ui->forceLoadCheckBox->isChecked()); + return new QListWidgetItem(exe.title()); } +void EditExecutablesDialog::updateUI( + const QListWidgetItem* item, const Executable* e) +{ + // the ui is currently being set, ignore changes + m_settingUI = true; + + if (e) { + setEdits(*e); + } else { + clearEdits(); + } + + setButtons(item, e); -void EditExecutablesDialog::on_addButton_clicked() + // any changes from now on are from the user + m_settingUI = false; +} + +void EditExecutablesDialog::setButtons( + const QListWidgetItem* item, const Executable* e) { - if (executableChanged()) { - saveExecutable(); + // add and remove are always enabled + + if (item) { + ui->up->setEnabled(canMove(item, -1)); + ui->down->setEnabled(canMove(item, +1)); + } else { + ui->up->setEnabled(false); + ui->down->setEnabled(false); } +} - resetInput(); - refreshExecutablesWidget(); +void EditExecutablesDialog::clearEdits() +{ + ui->title->clear(); + ui->title->setEnabled(false); + ui->binary->clear(); + ui->binary->setEnabled(false); + ui->browseBinary->setEnabled(false); + ui->workingDirectory->clear(); + ui->workingDirectory->setEnabled(false); + ui->browseWorkingDirectory->setEnabled(false); + ui->arguments->clear(); + ui->arguments->setEnabled(false); + ui->overwriteSteamAppID->setEnabled(false); + ui->overwriteSteamAppID->setChecked(false); + ui->steamAppID->setEnabled(false); + ui->steamAppID->clear(); + ui->createFilesInMod->setEnabled(false); + ui->createFilesInMod->setChecked(false); + ui->mods->setEnabled(false); + ui->mods->setCurrentIndex(-1); + ui->forceLoadLibraries->setEnabled(false); + ui->forceLoadLibraries->setChecked(false); + ui->configureLibraries->setEnabled(false); + ui->useApplicationIcon->setEnabled(false); + ui->useApplicationIcon->setChecked(false); } -void EditExecutablesDialog::on_browseButton_clicked() +void EditExecutablesDialog::setEdits(const Executable& e) { - QString binaryName = FileDialogMemory::getOpenFileName( - "editExecutableBinary", this, tr("Select a binary"), QString(), - tr("Executable (%1)").arg("*.exe *.bat *.jar")); + ui->title->setText(e.title()); + ui->binary->setText(QDir::toNativeSeparators(e.binaryInfo().absoluteFilePath())); + ui->workingDirectory->setText(QDir::toNativeSeparators(e.workingDirectory())); + ui->arguments->setText(e.arguments()); + ui->overwriteSteamAppID->setChecked(!e.steamAppID().isEmpty()); + ui->steamAppID->setEnabled(!e.steamAppID().isEmpty()); + ui->steamAppID->setText(e.steamAppID()); + ui->useApplicationIcon->setChecked(e.usesOwnIcon()); - if (binaryName.endsWith(".jar", Qt::CaseInsensitive)) { - QString binaryPath; - { // try to find java automatically - std::wstring binaryNameW = ToWString(binaryName); - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(binaryNameW.c_str(), nullptr, buffer) - > reinterpret_cast<HINSTANCE>(32)) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(binaryNameW.c_str(), &binaryType)) { - qDebug("failed to determine binary type of \"%ls\": %lu", binaryNameW.c_str(), ::GetLastError()); - } else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty()) { - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); + { + int modIndex = -1; + + const auto modName = m_customOverwrites.find(e.title()); + + if (modName && !modName->value.isEmpty()) { + modIndex = ui->mods->findText(modName->value); + + if (modIndex == -1) { + qWarning().nospace() + << "executable '" << e.title() << "' uses mod '" << modName->value << "' " + << "as a custom overwrite, but that mod doesn't exist"; } } - if (binaryPath.isEmpty()) { - QMessageBox::information(this, tr("Java (32-bit) required"), - tr("MO requires 32-bit java to run this application. If you already have it installed, select javaw.exe " - "from that installation as the binary.")); - } else { - ui->binaryEdit->setText(binaryPath); - } - ui->workingDirEdit->setText(QDir::toNativeSeparators(QFileInfo(binaryName).absolutePath())); - ui->argumentsEdit->setText("-jar \"" + QDir::toNativeSeparators(binaryName) + "\""); + const bool hasCustomOverwrites = (modName && modName->enabled); + + ui->createFilesInMod->setChecked(hasCustomOverwrites); + ui->mods->setEnabled(hasCustomOverwrites); + ui->mods->setCurrentIndex(modIndex); + } + + { + const auto libraryList = m_forcedLibraries.find(e.title()); + const bool hasForcedLibraries = (libraryList && libraryList->enabled); + + ui->forceLoadLibraries->setChecked(hasForcedLibraries); + ui->configureLibraries->setEnabled(hasForcedLibraries); + } + + // always enabled + ui->title->setEnabled(true); + ui->binary->setEnabled(true); + ui->browseBinary->setEnabled(true); + ui->workingDirectory->setEnabled(true); + ui->browseWorkingDirectory->setEnabled(true); + ui->arguments->setEnabled(true); + ui->overwriteSteamAppID->setEnabled(true); + ui->useApplicationIcon->setEnabled(true); + ui->createFilesInMod->setEnabled(true); + ui->forceLoadLibraries->setEnabled(true); +} + +void EditExecutablesDialog::save() +{ + if (m_settingUI) { + return; + } + + auto* e = selectedExe(); + if (!e) { + qWarning("trying to save but nothing is selected"); + return; + } + + // title may have changed, start with the stuff using it + + // custom overwrites + if (ui->createFilesInMod->isChecked()) { + m_customOverwrites.set(e->title(), true, ui->mods->currentText()); + } else { + m_customOverwrites.setEnabled(e->title(), false); + } + + // forced libraries + m_forcedLibraries.setEnabled(e->title(), ui->forceLoadLibraries->isChecked()); + + // get the new title, but ignore it if it's conflicting with an already + // existing executable + QString newTitle = ui->title->text(); + if (isTitleConflicting(newTitle)) { + newTitle = e->title(); + } + + if (e->title() != newTitle) { + // now rename both the custom overwrites and forced libraries if the title + // is being changed + m_customOverwrites.rename(e->title(), newTitle); + m_forcedLibraries.rename(e->title(), newTitle); + + // save the new title + e->title(newTitle); + } + + e->binaryInfo(ui->binary->text()); + e->workingDirectory(ui->workingDirectory->text()); + e->arguments(ui->arguments->text()); + + if (ui->overwriteSteamAppID->isChecked()) { + e->steamAppID(ui->steamAppID->text()); } else { - ui->binaryEdit->setText(QDir::toNativeSeparators(binaryName)); + e->steamAppID(""); } - if (ui->titleEdit->text().isEmpty()) { - ui->titleEdit->setText(QFileInfo(binaryName).baseName()); + if (ui->useApplicationIcon->isChecked()) { + e->flags(e->flags() | Executable::UseApplicationIcon); + } else { + e->flags(e->flags() & (~Executable::UseApplicationIcon)); } + + setDirty(true); } -void EditExecutablesDialog::on_browseDirButton_clicked() +void EditExecutablesDialog::saveOrder() { - QString dirName = FileDialogMemory::getExistingDirectory("editExecutableDirectory", this, - tr("Select a directory")); + m_executablesList = getExecutablesList(); + setDirty(true); +} + +bool EditExecutablesDialog::canMove(const QListWidgetItem* item, int direction) +{ + if (!item) { + return false; + } + + if (direction < 0) { + // moving up + return (ui->list->row(item) > 0); - ui->workingDirEdit->setText(dirName); + } else if (direction > 0) { + // moving down + return (ui->list->row(item) < (ui->list->count() - 1)); + } + + return false; } -void EditExecutablesDialog::on_removeButton_clicked() +void EditExecutablesDialog::move(QListWidgetItem* item, int direction) { - if (QMessageBox::question(this, tr("Confirm"), tr("Really remove \"%1\" from executables?").arg(ui->titleEdit->text()), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_Profile->removeSetting("custom_overwrites", ui->titleEdit->text()); - m_Profile->removeForcedLibraries(ui->titleEdit->text()); - m_ExecutablesList.remove(ui->titleEdit->text()); + if (!canMove(item, direction)) { + return; } - resetInput(); - refreshExecutablesWidget(); + const auto row = ui->list->row(item); + + // removing item + ui->list->takeItem(row); + ui->list->insertItem(row + (direction > 0 ? 1 : -1), item); + item->setSelected(true); + + setDirty(true); } -void EditExecutablesDialog::on_titleEdit_textChanged(const QString &arg1) +void EditExecutablesDialog::on_list_itemSelectionChanged() { - QPushButton *addButton = findChild<QPushButton*>("addButton"); - QPushButton *removeButton = findChild<QPushButton*>("removeButton"); + updateUI(selectedItem(), selectedExe()); +} - QListWidget *executablesWidget = findChild<QListWidget*>("executablesListBox"); +void EditExecutablesDialog::on_reset_clicked() +{ + const auto title = tr("Reset plugin executables"); - QList<QListWidgetItem*> existingItems = executablesWidget->findItems(arg1, Qt::MatchFixedString); + const auto text = tr( + "This will restore all the executables provided by the game plugin. If " + "there are existing executables with the same names, they will be " + "automatically renamed and left unchanged."); - addButton->setEnabled(arg1.length() != 0); + const auto buttons = QMessageBox::Ok | QMessageBox::Cancel; - if (existingItems.count() == 0) { - addButton->setText(tr("Add")); - removeButton->setEnabled(false); - } else { - // existing item. is it a custom one? - addButton->setText(tr("Modify")); - removeButton->setEnabled(true); + if (QMessageBox::question(this, title, text, buttons) != QMessageBox::Ok) { + return; } + + m_executablesList.resetFromPlugin(m_organizerCore.managedGame()); + fillList(); + + setDirty(true); } +void EditExecutablesDialog::on_add_clicked() +{ + auto title = m_executablesList.makeNonConflictingTitle(tr("New Executable")); + if (!title) { + return; + } + + const Executable e(*title); -bool EditExecutablesDialog::executableChanged() + m_executablesList.setExecutable(e); + + auto* item = createListItem(e); + ui->list->addItem(item); + item->setSelected(true); + + setDirty(true); +} + +void EditExecutablesDialog::on_remove_clicked() { - if (m_CurrentItem != nullptr) { - Executable const &selectedExecutable(m_ExecutablesList.find(m_CurrentItem->text())); + auto* item = selectedItem(); + if (!item) { + qWarning("trying to remove entry but nothing is selected"); + return; + } + + auto* exe = selectedExe(); + if (!exe) { + qWarning("trying to remove entry but nothing is selected"); + return; + } + + const int currentRow = ui->list->row(item); + delete item; - QString storedCustomOverwrite = m_Profile->setting("custom_overwrites", selectedExecutable.m_Title).toString(); - bool forcedLibrariesDirty = false; - auto forcedLibaries = m_Profile->determineForcedLibraries(selectedExecutable.m_Title); - forcedLibrariesDirty |= !std::equal(forcedLibaries.begin(), forcedLibaries.end(), - m_ForcedLibraries.begin(), m_ForcedLibraries.end(), - [](const ExecutableForcedLoadSetting &lhs, const ExecutableForcedLoadSetting &rhs) - { - return lhs.enabled() == rhs.enabled() && - lhs.forced() == rhs.forced() && - lhs.library() == rhs.library() && - lhs.process() == rhs.process(); - }); - forcedLibrariesDirty |= m_Profile->setting("forced_libraries", ui->titleEdit->text() + "/enabled", false).toBool() != - ui->forceLoadCheckBox->isChecked(); + m_customOverwrites.remove(exe->title()); + m_forcedLibraries.remove(exe->title()); - return selectedExecutable.m_Title != ui->titleEdit->text() - || selectedExecutable.m_Arguments != ui->argumentsEdit->text() - || selectedExecutable.m_SteamAppID != ui->appIDOverwriteEdit->text() - || !storedCustomOverwrite.isEmpty() != ui->newFilesModCheckBox->isChecked() - || !storedCustomOverwrite.isEmpty() && (storedCustomOverwrite != ui->newFilesModBox->currentText()) - || selectedExecutable.m_WorkingDirectory != QDir::fromNativeSeparators(ui->workingDirEdit->text()) - || selectedExecutable.m_BinaryInfo.absoluteFilePath() != QDir::fromNativeSeparators(ui->binaryEdit->text()) - || selectedExecutable.usesOwnIcon() != ui->useAppIconCheckBox->isChecked() - || forcedLibrariesDirty - ; + // removing from main list, must be done last because it invalidates the + // exe pointer + m_executablesList.remove(exe->title()); + + + // reselecting the same row as before, or the last one + if (currentRow >= ui->list->count()) { + // that was the last item, select the new list item, if any + if (ui->list->count() > 0) { + ui->list->item(ui->list->count() - 1)->setSelected(true); + } } else { - QFileInfo fileInfo(ui->binaryEdit->text()); - return !ui->binaryEdit->text().isEmpty() - && !ui->titleEdit->text().isEmpty() - && fileInfo.exists() - && fileInfo.isFile(); + ui->list->item(currentRow)->setSelected(true); } + + setDirty(true); } -void EditExecutablesDialog::on_executablesListBox_itemSelectionChanged() + +void EditExecutablesDialog::on_up_clicked() { - if (ui->executablesListBox->selectedItems().size() == 0) { - // deselected - resetInput(); + auto* item = selectedItem(); + if (!item) { + return; } + + move(item, -1); } -void EditExecutablesDialog::on_overwriteAppIDBox_toggled(bool checked) +void EditExecutablesDialog::on_down_clicked() { - ui->appIDOverwriteEdit->setEnabled(checked); + auto* item = selectedItem(); + if (!item) { + return; + } + + move(item, +1); } -void EditExecutablesDialog::on_closeButton_clicked() +bool EditExecutablesDialog::isTitleConflicting(const QString& s) { - if (executableChanged()) { - QMessageBox::StandardButton res = QMessageBox::question(this, tr("Save Changes?"), - tr("You made changes to the current executable, do you want to save them?"), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return; - } else if (res == QMessageBox::Yes) { - saveExecutable(); - // the executable list returned to callers is generated from the user data in the widgets, - // NOT the list we just saved - refreshExecutablesWidget(); + for (const auto& exe : m_executablesList) { + if (exe.title() == s) { + if (&exe != selectedExe()) { + // found an executable that's not the current one with the same title + return true; + } } } - this->accept(); + + return false; } -void EditExecutablesDialog::on_executablesListBox_clicked(const QModelIndex ¤t) +void EditExecutablesDialog::on_title_textChanged(const QString& s) { - if (current.isValid()) { + if (m_settingUI) { + return; + } - if (executableChanged()) { - QMessageBox::StandardButton res = QMessageBox::question(this, tr("Save Changes?"), - tr("You made changes to the current executable, do you want to save them?"), - QMessageBox::Yes | QMessageBox::No); - if (res == QMessageBox::Yes) { - saveExecutable(); + // don't allow changing the title to something that already exists + if (isTitleConflicting(s)) { + return; + } - //This is necessary if we're adding a new item, but it doesn't look very nice. - //Ideally we'd end up with the correct row displayed - ui->executablesListBox->selectionModel()->clearSelection(); - ui->executablesListBox->selectionModel()->select(current, QItemSelectionModel::SelectCurrent); - QTimer::singleShot(50, this, SLOT(delayedRefresh())); - return; - } - } + // must save before modifying the item in the list widget because saving + // relies on the item's text being the same as an item in m_executablesList + save(); + + // once the executable is saved, the list item must be changed to match the + // new name + if (auto* i=selectedItem()) { + i->setText(s); + } +} + +void EditExecutablesDialog::on_overwriteSteamAppID_toggled(bool checked) +{ + if (m_settingUI) { + return; + } - ui->executablesListBox->selectionModel()->clearSelection(); - ui->executablesListBox->selectionModel()->select(current, QItemSelectionModel::SelectCurrent); + ui->steamAppID->setEnabled(checked); + save(); +} - m_CurrentItem = ui->executablesListBox->item(current.row()); +void EditExecutablesDialog::on_createFilesInMod_toggled(bool checked) +{ + if (m_settingUI) { + return; + } - Executable const &selectedExecutable(m_ExecutablesList.find(m_CurrentItem->text())); + ui->mods->setEnabled(checked); + save(); +} - ui->titleEdit->setText(selectedExecutable.m_Title); - ui->binaryEdit->setText(QDir::toNativeSeparators(selectedExecutable.m_BinaryInfo.absoluteFilePath())); - ui->argumentsEdit->setText(selectedExecutable.m_Arguments); - ui->workingDirEdit->setText(QDir::toNativeSeparators(selectedExecutable.m_WorkingDirectory)); - ui->removeButton->setEnabled(selectedExecutable.isCustom()); - ui->overwriteAppIDBox->setChecked(!selectedExecutable.m_SteamAppID.isEmpty()); - if (!selectedExecutable.m_SteamAppID.isEmpty()) { - ui->appIDOverwriteEdit->setText(selectedExecutable.m_SteamAppID); - } else { - ui->appIDOverwriteEdit->clear(); - } - ui->useAppIconCheckBox->setChecked(selectedExecutable.usesOwnIcon()); +void EditExecutablesDialog::on_forceLoadLibraries_toggled(bool checked) +{ + if (m_settingUI) { + return; + } - int index = -1; + ui->configureLibraries->setEnabled(ui->forceLoadLibraries->isChecked()); + save(); +} - QString customOverwrite = m_Profile->setting("custom_overwrites", selectedExecutable.m_Title).toString(); - if (!customOverwrite.isEmpty()) { - index = ui->newFilesModBox->findText(customOverwrite); - qDebug("find %s -> %d", qUtf8Printable(customOverwrite), index); - } +void EditExecutablesDialog::on_browseBinary_clicked() +{ + const QString binaryName = FileDialogMemory::getOpenFileName( + "editExecutableBinary", this, tr("Select a binary"), ui->binary->text(), + tr("Executable (%1)").arg("*.exe *.bat *.jar")); - ui->newFilesModCheckBox->setChecked(index != -1); - if (index != -1) { - ui->newFilesModBox->setCurrentIndex(index); + if (binaryName.isNull()) { + // canceled + return; + } + + // setting binary + if (binaryName.endsWith(".jar", Qt::CaseInsensitive)) { + // special case for jar files, uses the system java installation + setJarBinary(binaryName); + } else { + ui->binary->setText(QDir::toNativeSeparators(binaryName)); + } + + // setting title if currently empty + if (ui->title->text().isEmpty()) { + const auto prefix = QFileInfo(binaryName).baseName(); + const auto newTitle = m_executablesList.makeNonConflictingTitle(prefix); + + if (newTitle) { + ui->title->setText(*newTitle); } + } + + save(); +} + +void EditExecutablesDialog::on_browseWorkingDirectory_clicked() +{ + QString dirName = FileDialogMemory::getExistingDirectory( + "editExecutableDirectory", this, tr("Select a directory"), + ui->workingDirectory->text()); + + if (dirName.isNull()) { + // canceled + return; + } + + ui->workingDirectory->setText(dirName); +} + +void EditExecutablesDialog::on_configureLibraries_clicked() +{ + auto* e = selectedExe(); + if (!e) { + qWarning("trying to configure libraries but nothing is selected"); + return; + } + + ForcedLoadDialog dialog(m_organizerCore.managedGame(), this); + + if (auto libraryList=m_forcedLibraries.find(e->title())) { + dialog.setValues(libraryList->value); + } - m_ForcedLibraries = m_Profile->determineForcedLibraries(ui->titleEdit->text()); - bool forcedLibraries = m_Profile->forcedLibrariesEnabled(ui->titleEdit->text()); - ui->forceLoadButton->setEnabled(forcedLibraries); - ui->forceLoadCheckBox->setChecked(forcedLibraries); + if (dialog.exec() == QDialog::Accepted) { + m_forcedLibraries.setValue(e->title(), dialog.values()); + save(); + } +} + +void EditExecutablesDialog::on_buttons_clicked(QAbstractButton* b) +{ + if (b == ui->buttons->button(QDialogButtonBox::Ok)) { + commitChanges(); + accept(); + } else if (b == ui->buttons->button(QDialogButtonBox::Apply)) { + commitChanges(); + } else { + reject(); } } -void EditExecutablesDialog::on_newFilesModCheckBox_toggled(bool checked) +void EditExecutablesDialog::setJarBinary(const QString& binaryName) { - ui->newFilesModBox->setEnabled(checked); + auto java = OrganizerCore::findJavaInstallation(binaryName); + + if (java.isEmpty()) { + QMessageBox::information( + this, tr("Java (32-bit) required"), + tr("MO requires 32-bit java to run this application. If you already " + "have it installed, select javaw.exe from that installation as " + "the binary.")); + } + + // only save once + + m_settingUI = true; + ui->binary->setText(java); + ui->workingDirectory->setText(QDir::toNativeSeparators(QFileInfo(binaryName).absolutePath())); + ui->arguments->setText("-jar \"" + QDir::toNativeSeparators(binaryName) + "\""); + m_settingUI = false; + + save(); } diff --git a/src/editexecutablesdialog.h b/src/editexecutablesdialog.h index bee3cba6..9715489e 100644 --- a/src/editexecutablesdialog.h +++ b/src/editexecutablesdialog.h @@ -22,106 +22,201 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "tutorabledialog.h"
#include <QListWidgetItem>
-#include <QTimer>
#include "executableslist.h"
#include "profile.h"
#include "iplugingame.h"
+#include <QTimer>
+#include <QAbstractButton>
+#include <optional>
namespace Ui {
class EditExecutablesDialog;
}
-
class ModList;
+class OrganizerCore;
-
-/**
- * @brief Dialog to manage the list of executables
+/** helper class to manage custom overwrites within the edit executables
+ * dialog, stores a T and a bool in map indexed by a QString
**/
-class EditExecutablesDialog : public MOBase::TutorableDialog
+template <class T>
+class ToggableMap
{
- Q_OBJECT
-
public:
+ struct Value
+ {
+ bool enabled;
+ T value;
+
+ Value(bool b, T&& v)
+ : enabled(b), value(std::forward<T>(v))
+ {
+ }
+ };
/**
- * @brief constructor
- *
- * @param executablesList current list of executables
- * @param parent parent widget
+ * returns the Value associated with the given title, or empty
**/
- explicit EditExecutablesDialog(const ExecutablesList &executablesList,
- const ModList &modList,
- Profile *profile,
- const MOBase::IPluginGame *game,
- QWidget *parent = 0);
+ std::optional<Value> find(const QString& title) const
+ {
+ auto itor = m_map.find(title);
+ if (itor == m_map.end()) {
+ return {};
+ }
- ~EditExecutablesDialog();
+ return itor->second;
+ }
/**
- * @brief retrieve the updated list of executables
- *
- * @return updated list of executables
+ * sets the given value, adds it if not found
**/
- ExecutablesList getExecutablesList() const;
+ void set(QString title, bool b, T value)
+ {
+ m_map.insert_or_assign(std::move(title), Value(b, std::move(value)));
+ }
- void saveExecutable();
+ /**
+ * sets whether the given value is enabled, inserts it if not found
+ **/
+ void setEnabled(const QString& title, bool b)
+ {
+ auto itor = m_map.find(title);
-private slots:
- void on_newFilesModCheckBox_toggled(bool checked);
+ if (itor == m_map.end()) {
+ m_map.emplace(title, Value(b, {}));
+ } else {
+ itor->second.enabled = b;
+ }
+ }
-private slots:
+ /**
+ * sets the given value, inserts it enabled if not found
+ **/
+ void setValue(const QString& title, T value)
+ {
+ auto itor = m_map.find(title);
- void on_binaryEdit_textChanged(const QString &arg1);
+ if (itor == m_map.end()) {
+ m_map.emplace(title, Value(true, std::move(value)));
+ } else {
+ itor->second.value = std::move(value);
+ }
+ }
- void on_workingDirEdit_textChanged(const QString &arg1);
+ /**
+ * renames the given value, ignored if not found
+ **/
+ void rename(const QString& oldTitle, QString newTitle)
+ {
+ auto itor = m_map.find(oldTitle);
+ if (itor == m_map.end()) {
+ return;
+ }
- void on_addButton_clicked();
+ // move to new title, erase old
+ m_map.emplace(std::move(newTitle), std::move(itor->second));
+ m_map.erase(itor);
+ }
- void on_browseButton_clicked();
+ /**
+ * removes the given value, ignored if not found
+ **/
+ void remove(const QString& title)
+ {
+ auto itor = m_map.find(title);
+ if (itor == m_map.end()) {
+ return;
+ }
- void on_removeButton_clicked();
+ m_map.erase(itor);
+ }
- void on_titleEdit_textChanged(const QString &arg1);
+private:
+ std::map<QString, Value> m_map;
+};
- void on_overwriteAppIDBox_toggled(bool checked);
- void on_browseDirButton_clicked();
+/**
+ * @brief Dialog to manage the list of executables
+ **/
+class EditExecutablesDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
- void on_closeButton_clicked();
+public:
+ using CustomOverwrites = ToggableMap<QString>;
+ using ForcedLibraries = ToggableMap<QList<MOBase::ExecutableForcedLoadSetting>>;
- void delayedRefresh();
+ explicit EditExecutablesDialog(OrganizerCore& oc, QWidget* parent=nullptr);
- void on_executablesListBox_itemSelectionChanged();
+ ~EditExecutablesDialog();
- void on_executablesListBox_clicked(const QModelIndex &index);
+ ExecutablesList getExecutablesList() const;
+ const CustomOverwrites& getCustomOverwrites() const;
+ const ForcedLibraries& getForcedLibraries() const;
- void on_forceLoadButton_clicked();
+private slots:
+ void on_list_itemSelectionChanged();
- void on_forceLoadCheckBox_toggled();
+ void on_reset_clicked();
+ void on_add_clicked();
+ void on_remove_clicked();
+ void on_up_clicked();
+ void on_down_clicked();
-private:
+ void on_title_textChanged(const QString& s);
+ void on_overwriteSteamAppID_toggled(bool checked);
+ void on_createFilesInMod_toggled(bool checked);
+ void on_forceLoadLibraries_toggled(bool checked);
- void resetInput();
+ void on_browseBinary_clicked();
+ void on_browseWorkingDirectory_clicked();
+ void on_configureLibraries_clicked();
- void refreshExecutablesWidget();
+ void on_buttons_clicked(QAbstractButton* b);
- bool executableChanged();
+private:
+ std::unique_ptr<Ui::EditExecutablesDialog> ui;
+ OrganizerCore& m_organizerCore;
- void updateButtonStates();
+ // copy of the original executables, used to clear the current settings when
+ // committing changes
+ const ExecutablesList m_originalExecutables;
-private:
- Ui::EditExecutablesDialog *ui;
+ // current executable list
+ ExecutablesList m_executablesList;
+
+ // custom overwrites set in the dialog
+ CustomOverwrites m_customOverwrites;
+
+ // forced libraries set in the dialog
+ ForcedLibraries m_forcedLibraries;
- QListWidgetItem *m_CurrentItem;
+ // true when the change events being triggered are in response to loading
+ // the executable's data into the UI, not from a user change
+ bool m_settingUI;
- ExecutablesList m_ExecutablesList;
- Profile *m_Profile;
+ void loadCustomOverwrites();
+ void loadForcedLibraries();
- QList<MOBase::ExecutableForcedLoadSetting> m_ForcedLibraries;
+ QListWidgetItem* selectedItem();
+ Executable* selectedExe();
- const MOBase::IPluginGame *m_GamePlugin;
+ void fillList();
+ QListWidgetItem* createListItem(const Executable& exe);
+ void updateUI(const QListWidgetItem* item, const Executable* e);
+ void clearEdits();
+ void setEdits(const Executable& e);
+ void setButtons(const QListWidgetItem* item, const Executable* e);
+ void save();
+ void saveOrder();
+ bool canMove(const QListWidgetItem* item, int direction);
+ void move(QListWidgetItem* item, int direction);
+ void setJarBinary(const QString& binaryName);
+ bool isTitleConflicting(const QString& s);
+ void commitChanges();
+ void setDirty(bool b);
};
#endif // EDITEXECUTABLESDIALOG_H
diff --git a/src/editexecutablesdialog.ui b/src/editexecutablesdialog.ui index 4f9223d5..a42dbeed 100644 --- a/src/editexecutablesdialog.ui +++ b/src/editexecutablesdialog.ui @@ -6,323 +6,486 @@ <rect>
<x>0</x>
<y>0</y>
- <width>426</width>
- <height>460</height>
+ <width>710</width>
+ <height>440</height>
</rect>
</property>
- <property name="minimumSize">
- <size>
- <width>200</width>
- <height>200</height>
- </size>
- </property>
<property name="windowTitle">
<string>Modify Executables</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QListWidget" name="executablesListBox">
- <property name="toolTip">
- <string>List of configured executables</string>
- </property>
- <property name="whatsThis">
- <string>This is a list of your configured executables. Executables in grey are automatically recognised and can not be modified.</string>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::InternalMove</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::TargetMoveAction</enum>
+ <widget class="QWidget" name="widget_3" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
</property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_4">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string>Title</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="titleEdit">
- <property name="toolTip">
- <string>Name of the executable. This is only for display purposes.</string>
- </property>
- <property name="whatsThis">
- <string>Name of the executable. This is only for display purposes.</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Binary</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="binaryEdit">
- <property name="toolTip">
- <string>Binary to run</string>
- </property>
- <property name="whatsThis">
- <string>Binary to run</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="browseButton">
- <property name="toolTip">
- <string>Browse filesystem</string>
- </property>
- <property name="whatsThis">
- <string>Browse filesystem for the executable to run.</string>
- </property>
- <property name="text">
- <string>...</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <item>
- <widget class="QLabel" name="label_4">
- <property name="text">
- <string>Start in</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="workingDirEdit"/>
- </item>
- <item>
- <widget class="QPushButton" name="browseDirButton">
- <property name="text">
- <string>...</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>Arguments</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="argumentsEdit">
- <property name="toolTip">
- <string>Arguments to pass to the application</string>
- </property>
- <property name="whatsThis">
- <string>Arguments to pass to the application</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_5">
- <item>
- <widget class="QCheckBox" name="overwriteAppIDBox">
- <property name="toolTip">
- <string>Allow the Steam AppID to be used for this executable to be changed.</string>
- </property>
- <property name="whatsThis">
- <string>Allow the Steam AppID to be used for this executable to be changed.
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QSplitter" name="splitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="handleWidth">
+ <number>9</number>
+ </property>
+ <property name="childrenCollapsible">
+ <bool>false</bool>
+ </property>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="spacing">
+ <number>3</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_6">
+ <property name="text">
+ <string>Executables</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QToolButton" name="add">
+ <property name="toolTip">
+ <string>Add an executable</string>
+ </property>
+ <property name="statusTip">
+ <string>Add an executable</string>
+ </property>
+ <property name="whatsThis">
+ <string>Add an executable</string>
+ </property>
+ <property name="text">
+ <string>Add</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/add</normaloff>:/MO/gui/add</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="remove">
+ <property name="toolTip">
+ <string>Remove the selected executable</string>
+ </property>
+ <property name="statusTip">
+ <string>Remove the selected executable</string>
+ </property>
+ <property name="whatsThis">
+ <string>Remove the selected executable</string>
+ </property>
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/list-remove.png</normaloff>:/MO/gui/resources/list-remove.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="up">
+ <property name="toolTip">
+ <string>Move the executable up in the list</string>
+ </property>
+ <property name="statusTip">
+ <string>Move the executable up in the list</string>
+ </property>
+ <property name="whatsThis">
+ <string>Move the executable up in the list</string>
+ </property>
+ <property name="text">
+ <string>Move the executable up in the list</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/go-up.png</normaloff>:/MO/gui/resources/go-up.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="down">
+ <property name="toolTip">
+ <string>Move the executable down in the list</string>
+ </property>
+ <property name="statusTip">
+ <string>Move the executable down in the list</string>
+ </property>
+ <property name="whatsThis">
+ <string>Move the executable down in the list</string>
+ </property>
+ <property name="text">
+ <string>Move the executable down in the list</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/go-down.png</normaloff>:/MO/gui/resources/go-down.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="reset">
+ <property name="toolTip">
+ <string>Adds the executables provided by the game plugin and moves any existing executables out of the way</string>
+ </property>
+ <property name="statusTip">
+ <string>Adds the executables provided by the game plugin and moves any existing executables out of the way</string>
+ </property>
+ <property name="whatsThis">
+ <string>Adds the executables provided by the game plugin and moves any existing executables out of the way</string>
+ </property>
+ <property name="text">
+ <string>Reset</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QListWidget" name="list">
+ <property name="toolTip">
+ <string>List of configured executables</string>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of your configured executables. Executables in grey are automatically recognised and can not be modified.</string>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::InternalMove</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::TargetMoveAction</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="widget_4" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>10</number>
+ </property>
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="bottomMargin">
+ <number>20</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Title</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="title">
+ <property name="toolTip">
+ <string>Name of the executable. This is only for display purposes.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Name of the executable. This is only for display purposes.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Binary</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+ <item>
+ <widget class="QLineEdit" name="binary">
+ <property name="toolTip">
+ <string>Binary to run</string>
+ </property>
+ <property name="whatsThis">
+ <string>Binary to run</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="browseBinary">
+ <property name="toolTip">
+ <string>Browse filesystem</string>
+ </property>
+ <property name="whatsThis">
+ <string>Browse filesystem for the executable to run.</string>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Start in</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,0">
+ <item>
+ <widget class="QLineEdit" name="workingDirectory"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="browseWorkingDirectory">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Arguments</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="arguments">
+ <property name="toolTip">
+ <string>Arguments to pass to the application</string>
+ </property>
+ <property name="whatsThis">
+ <string>Arguments to pass to the application</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="overwriteSteamAppID">
+ <property name="toolTip">
+ <string>Allow the Steam AppID to be used for this executable to be changed.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Allow the Steam AppID to be used for this executable to be changed.
Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game.
Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID. This overwrite is already preconfigured.</string>
- </property>
- <property name="text">
- <string>Overwrite Steam AppID</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="appIDOverwriteEdit">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Steam AppID to use for this executable that differs from the games AppID.</string>
- </property>
- <property name="whatsThis">
- <string>Steam AppID to use for this executable that differs from the games AppID.
+ </property>
+ <property name="text">
+ <string>Overwrite Steam AppID</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="steamAppID">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Steam AppID to use for this executable that differs from the games AppID.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Steam AppID to use for this executable that differs from the games AppID.
Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game (usually 72850).
Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID (usually 202480). This overwrite is already preconfigured.</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0,1">
- <item>
- <widget class="QCheckBox" name="newFilesModCheckBox">
- <property name="toolTip">
- <string>If this is enabled, new files are created in the specified mod instead of the "Overwrite" mod.</string>
- </property>
- <property name="text">
- <string>Create Files in Mod instead of Overwrite (*)</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="newFilesModBox">
- <property name="enabled">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_8">
- <item>
- <widget class="QCheckBox" name="forceLoadCheckBox">
- <property name="toolTip">
- <string>If this is enabled, the configured libraries will be automatically loaded when this executable is launched.</string>
- </property>
- <property name="text">
- <string>Force Load Libraries (*)</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="forceLoadButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="text">
- <string>Configure Libraries</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QCheckBox" name="useAppIconCheckBox">
- <property name="text">
- <string>Use Application's Icon for shortcuts</string>
- </property>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0,1">
+ <item>
+ <widget class="QCheckBox" name="createFilesInMod">
+ <property name="toolTip">
+ <string>If this is enabled, new files are created in the specified mod instead of the "Overwrite" mod.</string>
+ </property>
+ <property name="text">
+ <string>Create Files in Mod instead of Overwrite (*)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="mods">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QCheckBox" name="forceLoadLibraries">
+ <property name="toolTip">
+ <string>If this is enabled, the configured libraries will be automatically loaded when this executable is launched.</string>
+ </property>
+ <property name="text">
+ <string>Force Load Libraries (*)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="configureLibraries">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Configure Libraries</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useApplicationIcon">
+ <property name="text">
+ <string>Use Application's Icon for shortcuts</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>(*) Profile Specific</string>
+ </property>
+ <property name="margin">
+ <number>5</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
<item>
- <widget class="QLabel" name="label_5">
- <property name="text">
- <string>(*) This setting is profile-specific</string>
+ <widget class="QDialogButtonBox" name="buttons">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QPushButton" name="addButton">
- <property name="toolTip">
- <string>Add an executable</string>
- </property>
- <property name="whatsThis">
- <string>Add an executable</string>
- </property>
- <property name="text">
- <string>Add</string>
- </property>
- <property name="icon">
- <iconset>
- <normaloff>:/new/guiresources/resources/list-add.png</normaloff>:/new/guiresources/resources/list-add.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="removeButton">
- <property name="toolTip">
- <string>Remove the selected executable</string>
- </property>
- <property name="whatsThis">
- <string>Remove the selected executable</string>
- </property>
- <property name="text">
- <string>Remove</string>
- </property>
- <property name="icon">
- <iconset>
- <normaloff>:/new/guiresources/resources/list-remove.png</normaloff>:/new/guiresources/resources/list-remove.png</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_7">
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="closeButton">
- <property name="text">
- <string>Close</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
</layout>
</widget>
<tabstops>
- <tabstop>executablesListBox</tabstop>
- <tabstop>titleEdit</tabstop>
- <tabstop>binaryEdit</tabstop>
- <tabstop>browseButton</tabstop>
- <tabstop>workingDirEdit</tabstop>
- <tabstop>browseDirButton</tabstop>
- <tabstop>argumentsEdit</tabstop>
- <tabstop>overwriteAppIDBox</tabstop>
- <tabstop>appIDOverwriteEdit</tabstop>
- <tabstop>newFilesModCheckBox</tabstop>
- <tabstop>newFilesModBox</tabstop>
- <tabstop>useAppIconCheckBox</tabstop>
- <tabstop>addButton</tabstop>
- <tabstop>removeButton</tabstop>
- <tabstop>closeButton</tabstop>
+ <tabstop>binary</tabstop>
+ <tabstop>browseBinary</tabstop>
+ <tabstop>workingDirectory</tabstop>
+ <tabstop>browseWorkingDirectory</tabstop>
+ <tabstop>overwriteSteamAppID</tabstop>
+ <tabstop>steamAppID</tabstop>
+ <tabstop>createFilesInMod</tabstop>
+ <tabstop>mods</tabstop>
</tabstops>
- <resources/>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
<connections/>
</ui>
diff --git a/src/executableslist.cpp b/src/executableslist.cpp index 4b2e380b..077d2a93 100644 --- a/src/executableslist.cpp +++ b/src/executableslist.cpp @@ -33,193 +33,418 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase;
+ExecutablesList::iterator ExecutablesList::begin()
+{
+ return m_Executables.begin();
+}
-ExecutablesList::ExecutablesList()
+ExecutablesList::const_iterator ExecutablesList::begin() const
{
+ return m_Executables.begin();
}
-ExecutablesList::~ExecutablesList()
+ExecutablesList::iterator ExecutablesList::end()
{
+ return m_Executables.end();
}
-void ExecutablesList::init(IPluginGame const *game)
+ExecutablesList::const_iterator ExecutablesList::end() const
{
- Q_ASSERT(game != nullptr);
+ return m_Executables.end();
+}
+
+std::size_t ExecutablesList::size() const
+{
+ return m_Executables.size();
+}
+
+bool ExecutablesList::empty() const
+{
+ return m_Executables.empty();
+}
+
+void ExecutablesList::load(const MOBase::IPluginGame* game, QSettings& settings)
+{
+ qDebug("setting up configured executables");
+
m_Executables.clear();
- for (const ExecutableInfo &info : game->executables()) {
- if (info.isValid()) {
- addExecutableInternal(info.title(),
- info.binary().absoluteFilePath(),
- info.arguments().join(" "),
- info.workingDirectory().absolutePath(),
- info.steamAppID());
+
+ // whether the executable list in the .ini is still using the old custom
+ // executables from 2.2.0, see upgradeFromCustom()
+ bool needsUpgrade = false;
+
+ int numCustomExecutables = settings.beginReadArray("customExecutables");
+ for (int i = 0; i < numCustomExecutables; ++i) {
+ settings.setArrayIndex(i);
+
+ Executable::Flags flags;
+ if (settings.value("toolbar", false).toBool())
+ flags |= Executable::ShowInToolbar;
+ if (settings.value("ownicon", false).toBool())
+ flags |= Executable::UseApplicationIcon;
+
+ if (settings.contains("custom")) {
+ // the "custom" setting only exists in older versions
+ needsUpgrade = true;
}
- }
- ExecutableInfo explorerpp = ExecutableInfo("Explore Virtual Folder", QFileInfo(QCoreApplication::applicationDirPath() + "/explorer++/Explorer++.exe" ))
- .withArgument(QString("\"%1\"").arg(QDir::toNativeSeparators(game->dataDirectory().absolutePath())));
- if (explorerpp.isValid()) {
- addExecutableInternal(explorerpp.title(),
- explorerpp.binary().absoluteFilePath(),
- explorerpp.arguments().join(" "),
- explorerpp.workingDirectory().absolutePath(),
- explorerpp.steamAppID());
+ setExecutable(Executable()
+ .title(settings.value("title").toString())
+ .binaryInfo(settings.value("binary").toString())
+ .arguments(settings.value("arguments").toString())
+ .steamAppID(settings.value("steamAppID", "").toString())
+ .workingDirectory(settings.value("workingDirectory", "").toString())
+ .flags(flags));
}
+ settings.endArray();
+
+ addFromPlugin(game, IgnoreExisting);
+
+ if (needsUpgrade)
+ upgradeFromCustom(game);
}
-void ExecutablesList::getExecutables(std::vector<Executable>::iterator &begin, std::vector<Executable>::iterator &end)
+void ExecutablesList::store(QSettings& settings)
{
- begin = m_Executables.begin();
- end = m_Executables.end();
+ settings.remove("customExecutables");
+ settings.beginWriteArray("customExecutables");
+
+ int count = 0;
+
+ for (const auto& item : *this) {
+ settings.setArrayIndex(count++);
+
+ settings.setValue("title", item.title());
+ settings.setValue("toolbar", item.isShownOnToolbar());
+ settings.setValue("ownicon", item.usesOwnIcon());
+ settings.setValue("binary", item.binaryInfo().absoluteFilePath());
+ settings.setValue("arguments", item.arguments());
+ settings.setValue("workingDirectory", item.workingDirectory());
+ settings.setValue("steamAppID", item.steamAppID());
+ }
+
+ settings.endArray();
}
-void ExecutablesList::getExecutables(std::vector<Executable>::const_iterator &begin,
- std::vector<Executable>::const_iterator &end) const
+std::vector<Executable> ExecutablesList::getPluginExecutables(
+ MOBase::IPluginGame const *game) const
{
- begin = m_Executables.begin();
- end = m_Executables.end();
+ Q_ASSERT(game != nullptr);
+
+ std::vector<Executable> v;
+
+ for (const ExecutableInfo &info : game->executables()) {
+ if (!info.isValid()) {
+ continue;
+ }
+
+ v.push_back({info, Executable::UseApplicationIcon});
+ }
+
+ const QFileInfo eppBin(QCoreApplication::applicationDirPath() + "/explorer++/Explorer++.exe");
+
+ if (eppBin.exists()) {
+ const auto args = QString("\"%1\"")
+ .arg(QDir::toNativeSeparators(game->dataDirectory().absolutePath()));
+
+ const auto exe = Executable()
+ .title("Explore Virtual Folder")
+ .binaryInfo(eppBin)
+ .arguments(args)
+ .workingDirectory(eppBin.absolutePath())
+ .flags(Executable::UseApplicationIcon);
+
+ v.push_back(exe);
+ }
+
+ return v;
}
-const Executable &ExecutablesList::find(const QString &title) const
+void ExecutablesList::resetFromPlugin(MOBase::IPluginGame const *game)
{
- for (Executable const &exe : m_Executables) {
- if (exe.m_Title == title) {
- return exe;
- }
+ qDebug("resetting plugin executables");
+
+ Q_ASSERT(game != nullptr);
+
+ for (const auto& exe : getPluginExecutables(game)) {
+ setExecutable(exe, MoveExisting);
}
- throw std::runtime_error(QString("invalid executable name: %1").arg(title).toLocal8Bit().constData());
}
+void ExecutablesList::addFromPlugin(IPluginGame const *game, SetFlags flags)
+{
+ Q_ASSERT(game != nullptr);
-Executable &ExecutablesList::find(const QString &title)
+ for (const auto& exe : getPluginExecutables(game)) {
+ setExecutable(exe, flags);
+ }
+}
+
+const Executable &ExecutablesList::get(const QString &title) const
{
- for (Executable &exe : m_Executables) {
- if (exe.m_Title == title) {
+ for (const auto& exe : m_Executables) {
+ if (exe.title() == title) {
return exe;
}
}
- throw std::runtime_error(QString("invalid executable name: %1").arg(title).toLocal8Bit().constData());
+
+ throw std::runtime_error(QString("executable not found: %1").arg(title).toLocal8Bit().constData());
}
+Executable &ExecutablesList::get(const QString &title)
+{
+ return const_cast<Executable&>(std::as_const(*this).get(title));
+}
-Executable &ExecutablesList::findByBinary(const QFileInfo &info)
+Executable &ExecutablesList::getByBinary(const QFileInfo &info)
{
for (Executable &exe : m_Executables) {
- if (info == exe.m_BinaryInfo) {
+ if (exe.binaryInfo() == info) {
return exe;
}
}
throw std::runtime_error("invalid info");
}
-
-std::vector<Executable>::iterator ExecutablesList::findExe(const QString &title)
+ExecutablesList::iterator ExecutablesList::find(const QString &title)
{
- for (std::vector<Executable>::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) {
- if (iter->m_Title == title) {
- return iter;
- }
- }
- return m_Executables.end();
+ return std::find_if(begin(), end(), [&](auto&& e) { return e.title() == title; });
}
+ExecutablesList::const_iterator ExecutablesList::find(const QString &title) const
+{
+ return std::find_if(begin(), end(), [&](auto&& e) { return e.title() == title; });
+}
bool ExecutablesList::titleExists(const QString &title) const
{
- auto test = [&] (const Executable &exe) { return exe.m_Title == title; };
+ auto test = [&] (const Executable &exe) { return exe.title() == title; };
return std::find_if(m_Executables.begin(), m_Executables.end(), test) != m_Executables.end();
}
+void ExecutablesList::setExecutable(const Executable &exe)
+{
+ setExecutable(exe, MergeExisting);
+}
-void ExecutablesList::addExecutable(const Executable &executable)
+void ExecutablesList::setExecutable(const Executable &exe, SetFlags flags)
{
- auto existingExe = findExe(executable.m_Title);
- if (existingExe != m_Executables.end()) {
- *existingExe = executable;
+ auto itor = find(exe.title());
+
+ if (itor != end()) {
+ if (flags == IgnoreExisting) {
+ return;
+ }
+
+ if (flags == MoveExisting) {
+ const auto newTitle = makeNonConflictingTitle(exe.title());
+ if (!newTitle) {
+ qCritical().nospace()
+ << "executable '" << exe.title() << "' was in the way but could "
+ << "not be renamed";
+
+ return;
+ }
+
+ qWarning().nospace()
+ << "executable '" << itor->title() << "' was in the way and was "
+ << "renamed to '" << *newTitle << "'";
+
+ itor->title(*newTitle);
+ itor = end();
+ }
+ }
+
+ if (itor == m_Executables.end()) {
+ m_Executables.push_back(exe);
} else {
- m_Executables.push_back(executable);
+ itor->mergeFrom(exe);
}
}
+void ExecutablesList::remove(const QString &title)
+{
+ auto itor = find(title);
+ if (itor != m_Executables.end()) {
+ m_Executables.erase(itor);
+ }
+}
-void ExecutablesList::updateExecutable(const QString &title,
- const QString &executableName,
- const QString &arguments,
- const QString &workingDirectory,
- const QString &steamAppID,
- Executable::Flags mask,
- Executable::Flags flags)
+std::optional<QString> ExecutablesList::makeNonConflictingTitle(
+ const QString& prefix)
{
- QFileInfo file(executableName);
- QDir dir(workingDirectory);
- auto existingExe = findExe(title);
- flags &= mask;
+ const int max = 100;
- if (existingExe != m_Executables.end()) {
- existingExe->m_Title = title;
- existingExe->m_Flags &= ~mask;
- existingExe->m_Flags |= flags;
- // for pre-configured executables don't overwrite settings we didn't store
- if (flags & Executable::CustomExecutable) {
- if (file.exists()) {
- // don't overwrite a valid binary with an invalid one
- existingExe->m_BinaryInfo = file;
- }
- if (dir.exists()) {
- // don't overwrite a valid working directory with an invalid one
- existingExe->m_WorkingDirectory = workingDirectory;
- }
- existingExe->m_Arguments = arguments;
- existingExe->m_SteamAppID = steamAppID;
+ QString title = prefix;
+
+ for (int i=1; i<max; ++i) {
+ if (!titleExists(title)) {
+ return title;
}
- } else {
- Executable newExe;
- newExe.m_Title = title;
- newExe.m_BinaryInfo = file;
- newExe.m_Arguments = arguments;
- newExe.m_WorkingDirectory = workingDirectory;
- newExe.m_SteamAppID = steamAppID;
- newExe.m_Flags = Executable::CustomExecutable | flags;
- m_Executables.push_back(newExe);
+
+ title = prefix + QString(" (%1)").arg(i);
}
-}
+ qCritical().nospace()
+ << "ran out of executable titles for prefix '" << prefix << "'";
-void ExecutablesList::remove(const QString &title)
+ return {};
+}
+
+void ExecutablesList::upgradeFromCustom(MOBase::IPluginGame const *game)
{
- for (std::vector<Executable>::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) {
- if (iter->isCustom() && (iter->m_Title == title)) {
- m_Executables.erase(iter);
- break;
+ qDebug() << "upgrading executables list";
+
+ Q_ASSERT(game != nullptr);
+
+ // prior to 2.2.1, plugin executables were special in the sense that they
+ // did not store certain settings, like the binary info and working directory;
+ // those were filled in when MO started, but never saved
+ //
+ // this interferes with the new executables list, which is completely
+ // customizable, because plugin executables are only added to the list when
+ // they don't exist at all and are ignored otherwise, leaving some of the
+ // fields completely blank
+ //
+ // when the "custom" setting is found in the .ini file (see load()), it is
+ // assumed that the older scheme is still present; in that case, the plugin
+ // executables force their binary info and working directory into the existing
+ // executables one last time
+ //
+ // from that point on, plugin executables are ignored unless they're
+ // completely missing from the list, allowing users to customize them as they
+ // want
+
+ for (const auto& exe : getPluginExecutables(game)) {
+ auto itor = find(exe.title());
+ if (itor == end()){
+ continue;
+ }
+
+ if (!itor->binaryInfo().exists()) {
+ itor->binaryInfo(exe.binaryInfo());
+ }
+
+ if (itor->workingDirectory().isEmpty()) {
+ itor->workingDirectory(exe.workingDirectory());
}
}
}
-void ExecutablesList::addExecutableInternal(const QString &title, const QString &executableName,
- const QString &arguments, const QString &workingDirectory,
- const QString &steamAppID)
+Executable::Executable(QString title)
+ : m_title(title)
{
- QFileInfo file(executableName);
- if (file.exists()) {
- Executable newExe;
- newExe.m_BinaryInfo = file;
- newExe.m_Title = title;
- newExe.m_Arguments = arguments;
- newExe.m_WorkingDirectory = workingDirectory;
- newExe.m_SteamAppID = steamAppID;
- newExe.m_Flags = Executable::UseApplicationIcon;
- m_Executables.push_back(newExe);
- }
}
+Executable::Executable(const MOBase::ExecutableInfo& info, Flags flags) :
+ m_title(info.title()),
+ m_binaryInfo(info.binary()),
+ m_arguments(info.arguments().join(" ")),
+ m_steamAppID(info.steamAppID()),
+ m_workingDirectory(info.workingDirectory().absolutePath()),
+ m_flags(flags)
+{
+}
+
+const QString& Executable::title() const
+{
+ return m_title;
+}
+
+const QFileInfo& Executable::binaryInfo() const
+{
+ return m_binaryInfo;
+}
+
+const QString& Executable::arguments() const
+{
+ return m_arguments;
+}
+
+const QString& Executable::steamAppID() const
+{
+ return m_steamAppID;
+}
+
+const QString& Executable::workingDirectory() const
+{
+ return m_workingDirectory;
+}
+
+Executable::Flags Executable::flags() const
+{
+ return m_flags;
+}
+
+Executable& Executable::title(const QString& s)
+{
+ m_title = s;
+ return *this;
+}
+
+Executable& Executable::binaryInfo(const QFileInfo& fi)
+{
+ m_binaryInfo = fi;
+ return *this;
+}
+
+Executable& Executable::arguments(const QString& s)
+{
+ m_arguments = s;
+ return *this;
+}
+
+Executable& Executable::steamAppID(const QString& s)
+{
+ m_steamAppID = s;
+ return *this;
+}
+
+Executable& Executable::workingDirectory(const QString& s)
+{
+ m_workingDirectory = s;
+ return *this;
+}
+
+Executable& Executable::flags(Flags f)
+{
+ m_flags = f;
+ return *this;
+}
+
+bool Executable::isShownOnToolbar() const
+{
+ return m_flags.testFlag(ShowInToolbar);
+}
-void Executable::showOnToolbar(bool state)
+void Executable::setShownOnToolbar(bool state)
{
if (state) {
- m_Flags |= ShowInToolbar;
+ m_flags |= ShowInToolbar;
} else {
- m_Flags &= ~ShowInToolbar;
+ m_flags &= ~ShowInToolbar;
}
}
+
+bool Executable::usesOwnIcon() const
+{
+ return m_flags.testFlag(UseApplicationIcon);
+}
+
+void Executable::mergeFrom(const Executable& other)
+{
+ // flags on plugin executables that the user is allowed to change
+ const auto allow = ShowInToolbar;
+
+ // this happens after executables are loaded from settings and plugin
+ // executables are being added, or when users are modifying executables
+
+ m_title = other.title();
+ m_binaryInfo = other.binaryInfo();
+ m_arguments = other.arguments();
+ m_steamAppID = other.steamAppID();
+ m_workingDirectory = other.workingDirectory();
+ m_flags = other.flags();
+}
diff --git a/src/executableslist.h b/src/executableslist.h index 0534c09e..2d1dd28e 100644 --- a/src/executableslist.h +++ b/src/executableslist.h @@ -23,41 +23,61 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "executableinfo.h"
#include <vector>
+#include <optional>
#include <QFileInfo>
#include <QMetaType>
-namespace MOBase { class IPluginGame; }
+namespace MOBase { class IPluginGame; class ExecutableInfo; }
/*!
* @brief Information about an executable
**/
-struct Executable {
- QString m_Title;
- QFileInfo m_BinaryInfo;
- QString m_Arguments;
- QString m_SteamAppID;
- QString m_WorkingDirectory;
-
- enum Flag {
- CustomExecutable = 0x01,
+class Executable
+{
+public:
+ enum Flag
+ {
ShowInToolbar = 0x02,
- UseApplicationIcon = 0x04,
-
- AllFlags = 0xff //I know, I know
+ UseApplicationIcon = 0x04
};
- Q_DECLARE_FLAGS(Flags, Flag)
+ Q_DECLARE_FLAGS(Flags, Flag);
+
+ Executable(QString title={});
+
+ /**
+ * @brief Executable from plugin
+ */
+ Executable(const MOBase::ExecutableInfo& info, Flags flags);
- Flags m_Flags;
+ const QString& title() const;
+ const QFileInfo& binaryInfo() const;
+ const QString& arguments() const;
+ const QString& steamAppID() const;
+ const QString& workingDirectory() const;
+ Flags flags() const;
- bool isCustom() const { return m_Flags.testFlag(CustomExecutable); }
+ Executable& title(const QString& s);
+ Executable& binaryInfo(const QFileInfo& fi);
+ Executable& arguments(const QString& s);
+ Executable& steamAppID(const QString& s);
+ Executable& workingDirectory(const QString& s);
+ Executable& flags(Flags f);
- bool isShownOnToolbar() const { return m_Flags.testFlag(ShowInToolbar); }
+ bool isShownOnToolbar() const;
+ void setShownOnToolbar(bool state);
+ bool usesOwnIcon() const;
- void showOnToolbar(bool state);
+ void mergeFrom(const Executable& other);
- bool usesOwnIcon() const { return m_Flags.testFlag(UseApplicationIcon); }
+private:
+ QString m_title;
+ QFileInfo m_binaryInfo;
+ QString m_arguments;
+ QString m_steamAppID;
+ QString m_workingDirectory;
+ Flags m_flags;
};
@@ -66,28 +86,44 @@ struct Executable { **/
class ExecutablesList {
public:
+ using vector_type = std::vector<Executable>;
+ using iterator = vector_type::iterator;
+ using const_iterator = vector_type::const_iterator;
/**
- * @brief constructor
- *
- **/
- ExecutablesList();
+ * standard container interface
+ */
+ iterator begin();
+ const_iterator begin() const;
+ iterator end();
+ const_iterator end() const;
+ std::size_t size() const;
+ bool empty() const;
- ~ExecutablesList();
+ /**
+ * @brief initializes the list from the settings and the given plugin
+ **/
+ void load(const MOBase::IPluginGame* game, QSettings& settings);
/**
- * @brief initialise the list with the executables preconfigured for this game
+ * @brief re-adds all the executables from the plugin and renames existing
+ * executables that are in the way
**/
- void init(MOBase::IPluginGame const *game);
+ void resetFromPlugin(MOBase::IPluginGame const *game);
/**
- * @brief find an executable by its name
+ * @brief writes the current list to the settings
+ */
+ void store(QSettings& settings);
+
+ /**
+ * @brief get an executable by name
*
* @param title the title of the executable to look up
* @return the executable
- * @exception runtime_error will throw an exception if the name is not correct
+ * @exception runtime_error will throw an exception if the executable is not found
**/
- const Executable &find(const QString &title) const;
+ const Executable &get(const QString &title) const;
/**
* @brief find an executable by its name
@@ -96,7 +132,7 @@ public: * @return the executable
* @exception runtime_error will throw an exception if the name is not correct
**/
- Executable &find(const QString &title);
+ Executable &get(const QString &title);
/**
* @brief find an executable by a fileinfo structure
@@ -104,7 +140,13 @@ public: * @return the executable
* @exception runtime_error will throw an exception if the name is not correct
*/
- Executable &findByBinary(const QFileInfo &info);
+ Executable &getByBinary(const QFileInfo &info);
+
+ /**
+ * @brief returns an iterator for the given executable by title, or end()
+ */
+ iterator find(const QString &title);
+ const_iterator find(const QString &title) const;
/**
* @brief determine if an executable exists
@@ -117,85 +159,61 @@ public: * @brief add a new executable to the list
* @param executable
*/
- void addExecutable(const Executable &executable);
+ void setExecutable(const Executable &executable);
/**
- * @brief add a new executable to the list
+ * @brief remove the executable with the specified file name. This needs to
+ * be an absolute file path
*
- * @param title name displayed in the UI
- * @param executableName the actual filename to execute
- * @param arguments arguments to pass to the executable
+ * @param title title of the executable to remove
+ * @note if the executable name is invalid, nothing happens. There is no way
+ * to determine if this was successful
**/
- void addExecutable(const QString &title,
- const QString &executableName,
- const QString &arguments,
- const QString &workingDirectory,
- const QString &steamAppID,
- Executable::Flags flags)
- {
- updateExecutable(title, executableName, arguments, workingDirectory,
- steamAppID, Executable::AllFlags, flags);
- }
+ void remove(const QString &title);
/**
- * @brief Update an executable to the list
- *
- * @param title name displayed in the UI
- * @param executableName the actual filename to execute
- * @param arguments arguments to pass to the executable
- * @param closeMO if true, MO will be closed when the binary is started
- **/
- void updateExecutable(const QString &title,
- const QString &executableName,
- const QString &arguments,
- const QString &workingDirectory,
- const QString &steamAppID,
- Executable::Flags mask,
- Executable::Flags flags);
+ * returns a title that starts with the given prefix and does not clash with
+ * an existing executable, may fail
+ */
+ std::optional<QString> makeNonConflictingTitle(const QString& prefix);
+
+private:
+ enum SetFlags
+ {
+ // executables having the same name as existing ones are ignored
+ IgnoreExisting = 1,
+
+ // executables having the same name are merged
+ MergeExisting,
+
+ // an existing executable with the same name is renamed
+ MoveExisting
+ };
+
+ std::vector<Executable> m_Executables;
+
/**
- * @brief remove the executable with the specified file name. This needs to be an absolute file path
- *
- * @param title title of the executable to remove
- * @note if the executable name is invalid, nothing happens. There is no way to determine if this was successful
+ * @brief add the executables preconfigured for this game
**/
- void remove(const QString &title);
+ void addFromPlugin(MOBase::IPluginGame const *game, SetFlags flags);
/**
- * @brief retrieve begin and end iterators of the configured executables
- *
- * @param begin iterator to the first executable
- * @param end iterator one past the last executable
- **/
- void getExecutables(std::vector<Executable>::const_iterator &begin, std::vector<Executable>::const_iterator &end) const;
+ * @brief add a new executable to the list
+ * @param executable
+ */
+ void setExecutable(const Executable &exe, SetFlags flags);
/**
- * @brief retrieve begin and end iterators of the configured executables
- *
- * @param begin iterator to the first executable
- * @param end iterator one past the last executable
+ * returns the executables provided by the game plugin
**/
- void getExecutables(std::vector<Executable>::iterator &begin, std::vector<Executable>::iterator &end);
+ std::vector<Executable> getPluginExecutables(
+ MOBase::IPluginGame const *game) const;
/**
- * @brief get the number of executables (custom or otherwise)
+ * called when MO is still using the old custom executables from 2.2.0
**/
- size_t size() const {
- return m_Executables.size();
- }
-
-private:
-
- std::vector<Executable>::iterator findExe(const QString &title);
-
- void addExecutableInternal(const QString &title, const QString &executableName, const QString &arguments,
- const QString &workingDirectory,
- const QString &steamAppID);
-
-private:
-
- std::vector<Executable> m_Executables;
-
+ void upgradeFromCustom(const MOBase::IPluginGame* game);
};
Q_DECLARE_OPERATORS_FOR_FLAGS(Executable::Flags)
diff --git a/src/expanderwidget.cpp b/src/expanderwidget.cpp new file mode 100644 index 00000000..2f47da5b --- /dev/null +++ b/src/expanderwidget.cpp @@ -0,0 +1,54 @@ +#include "expanderwidget.h" + +ExpanderWidget::ExpanderWidget() + : m_button(nullptr), m_content(nullptr), opened_(false) +{ +} + +ExpanderWidget::ExpanderWidget(QToolButton* button, QWidget* content) + : ExpanderWidget() +{ + set(button, content); +} + +void ExpanderWidget::set(QToolButton* button, QWidget* content, bool o) +{ + m_button = button; + m_content = content; + + m_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + QObject::connect(m_button, &QToolButton::clicked, [&]{ toggle(); }); + + toggle(o); +} + +void ExpanderWidget::toggle() +{ + if (opened()) { + toggle(false); + } + else { + toggle(true); + } +} + +void ExpanderWidget::toggle(bool b) +{ + if (b) { + m_button->setArrowType(Qt::DownArrow); + m_content->show(); + } else { + m_button->setArrowType(Qt::RightArrow); + m_content->hide(); + } + + // the state has to be remembered instead of using m_content's visibility + // because saving the state in saveConflictExpandersState() happens after the + // dialog is closed, which marks all the widgets hidden + opened_ = b; +} + +bool ExpanderWidget::opened() const +{ + return opened_; +} diff --git a/src/expanderwidget.h b/src/expanderwidget.h new file mode 100644 index 00000000..da3eb9d6 --- /dev/null +++ b/src/expanderwidget.h @@ -0,0 +1,46 @@ +#ifndef EXPANDERWIDGET_H +#define EXPANDERWIDGET_H + +#include <QToolButton> + +/* Takes a QToolButton and a widget and creates an expandable widget. +**/ +class ExpanderWidget +{ +public: + /** empty expander, use set() + **/ + ExpanderWidget(); + + /** see set() + **/ + ExpanderWidget(QToolButton* button, QWidget* content); + + /** @brief sets the button and content widgets to use + * the button will be given an arrow icon, clicking it will toggle the + * visibility of the given widget + * @param button the button that toggles the content + * @param content the widget that will be shown or hidden + * @param opened initial state, defaults to closed + **/ + void set(QToolButton* button, QWidget* content, bool opened=false); + + /** either opens or closes the expander depending on the current state + **/ + void toggle(); + + /** sets the current state of the expander + **/ + void toggle(bool b); + + /** returns whether the expander is currently opened + **/ + bool opened() const; + +private: + QToolButton* m_button; + QWidget* m_content; + bool opened_; +}; + +#endif // EXPANDERWIDGET_H diff --git a/src/filedialogmemory.cpp b/src/filedialogmemory.cpp index 6b440d0f..0e3e9793 100644 --- a/src/filedialogmemory.cpp +++ b/src/filedialogmemory.cpp @@ -56,33 +56,51 @@ void FileDialogMemory::restore(QSettings &settings) }
-QString FileDialogMemory::getOpenFileName(const QString &dirID, QWidget *parent,
- const QString &caption, const QString &dir,
- const QString &filter, QString *selectedFilter,
- QFileDialog::Options options)
+QString FileDialogMemory::getOpenFileName(
+ const QString &dirID, QWidget *parent, const QString &caption,
+ const QString &dir, const QString &filter, QString *selectedFilter,
+ QFileDialog::Options options)
{
- std::pair<std::map<QString, QString>::iterator, bool> currentDir =
- instance().m_Cache.insert(std::make_pair(dirID, dir));
+ QString currentDir = dir;
+
+ if (currentDir.isEmpty()) {
+ auto itor = instance().m_Cache.find(dirID);
+ if (itor != instance().m_Cache.end()) {
+ currentDir = itor->second;
+ }
+ }
+
+ QString result = QFileDialog::getOpenFileName(
+ parent, caption, currentDir, filter, selectedFilter, options);
- QString result = QFileDialog::getOpenFileName(parent, caption, currentDir.first->second,
- filter, selectedFilter, options);
if (!result.isNull()) {
instance().m_Cache[dirID] = QFileInfo(result).path();
}
+
return result;
}
-QString FileDialogMemory::getExistingDirectory(const QString &dirID, QWidget *parent,
- const QString &caption, const QString &dir, QFileDialog::Options options)
+QString FileDialogMemory::getExistingDirectory(
+ const QString &dirID, QWidget *parent, const QString &caption,
+ const QString &dir, QFileDialog::Options options)
{
- std::pair<std::map<QString, QString>::iterator, bool> currentDir =
- instance().m_Cache.insert(std::make_pair(dirID, dir));
+ QString currentDir = dir;
+
+ if (currentDir.isEmpty()) {
+ auto itor = instance().m_Cache.find(dirID);
+ if (itor != instance().m_Cache.end()) {
+ currentDir = itor->second;
+ }
+ }
+
+ QString result = QFileDialog::getExistingDirectory(
+ parent, caption, currentDir, options);
- QString result = QFileDialog::getExistingDirectory(parent, caption, currentDir.first->second, options);
if (!result.isNull()) {
instance().m_Cache[dirID] = QFileInfo(result).path();
}
+
return result;
}
diff --git a/src/filedialogmemory.h b/src/filedialogmemory.h index b2bfdb53..81d7ba40 100644 --- a/src/filedialogmemory.h +++ b/src/filedialogmemory.h @@ -29,30 +29,25 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. class FileDialogMemory
{
-
public:
-
static void save(QSettings &settings);
static void restore(QSettings &settings);
- static QString getOpenFileName(const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
- const QString &dir = QString(), const QString &filter = QString(),
- QString *selectedFilter = 0, QFileDialog::Options options = 0);
+ static QString getOpenFileName(
+ const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
+ const QString &dir = QString(), const QString &filter = QString(),
+ QString *selectedFilter = 0, QFileDialog::Options options = 0);
- static QString getExistingDirectory(const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
- const QString &dir = QString(),
- QFileDialog::Options options = QFileDialog::ShowDirsOnly);
+ static QString getExistingDirectory(
+ const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
+ const QString &dir = QString(),
+ QFileDialog::Options options = QFileDialog::ShowDirsOnly);
private:
+ std::map<QString, QString> m_Cache;
FileDialogMemory();
-
static FileDialogMemory &instance();
-
-private:
-
- std::map<QString, QString> m_Cache;
-
};
#endif // FILEDIALOGMEMORY_H
diff --git a/src/filerenamer.cpp b/src/filerenamer.cpp new file mode 100644 index 00000000..c5c6782b --- /dev/null +++ b/src/filerenamer.cpp @@ -0,0 +1,189 @@ +#include "filerenamer.h" +#include <QMessageBox> +#include <QFileInfo> + +FileRenamer::FileRenamer(QWidget* parent, QFlags<RenameFlags> flags) + : m_parent(parent), m_flags(flags) +{ + // sanity check for flags + if ((m_flags & (HIDE|UNHIDE)) == 0) { + qCritical("renameFile() missing hide flag"); + // doesn't really matter, it's just for text + m_flags = HIDE; + } +} + +FileRenamer::RenameResults FileRenamer::rename(const QString& oldName, const QString& newName) +{ + qDebug().nospace() << "renaming " << oldName << " to " << newName; + + if (QFileInfo(newName).exists()) { + qDebug().nospace() << newName << " already exists"; + + // target file already exists, confirm replacement + auto answer = confirmReplace(newName); + + switch (answer) { + case DECISION_SKIP: { + // user wants to skip this file + qDebug().nospace() << "skipping " << oldName; + return RESULT_SKIP; + } + + case DECISION_REPLACE: { + qDebug().nospace() << "removing " << newName; + // user wants to replace the file, so remove it + if (!QFile(newName).remove()) { + qWarning().nospace() << "failed to remove " << newName; + // removal failed, warn the user and allow canceling + if (!removeFailed(newName)) { + qDebug().nospace() << "canceling " << oldName; + // user wants to cancel + return RESULT_CANCEL; + } + + // ignore this file and continue on + qDebug().nospace() << "skipping " << oldName; + return RESULT_SKIP; + } + + break; + } + + case DECISION_CANCEL: // fall-through + default: { + // user wants to stop + qDebug().nospace() << "canceling"; + return RESULT_CANCEL; + } + } + } + + // target either didn't exist or was removed correctly + + if (!QFile::rename(oldName, newName)) { + qWarning().nospace() << "failed to rename " << oldName << " to " << newName; + + // renaming failed, warn the user and allow canceling + if (!renameFailed(oldName, newName)) { + // user wants to cancel + qDebug().nospace() << "canceling"; + return RESULT_CANCEL; + } + + // ignore this file and continue on + qDebug().nospace() << "skipping " << oldName; + return RESULT_SKIP; + } + + // everything worked + qDebug().nospace() << "successfully renamed " << oldName << " to " << newName; + return RESULT_OK; +} + +FileRenamer::RenameDecision FileRenamer::confirmReplace(const QString& newName) +{ + if (m_flags & REPLACE_ALL) { + // user wants to silently replace all + qDebug().nospace() << "user has selected replace all"; + return DECISION_REPLACE; + } + else if (m_flags & REPLACE_NONE) { + // user wants to silently skip all + qDebug().nospace() << "user has selected replace none"; + return DECISION_SKIP; + } + + QString text; + + if (m_flags & HIDE) { + text = QObject::tr("The hidden file \"%1\" already exists. Replace it?").arg(newName); + } + else if (m_flags & UNHIDE) { + text = QObject::tr("The visible file \"%1\" already exists. Replace it?").arg(newName); + } + + auto buttons = QMessageBox::Yes | QMessageBox::No; + if (m_flags & MULTIPLE) { + // only show these buttons when there are multiple files to replace + buttons |= QMessageBox::YesToAll | QMessageBox::NoToAll | QMessageBox::Cancel; + } + + const auto answer = QMessageBox::question( + m_parent, QObject::tr("Replace file?"), text, buttons); + + switch (answer) { + case QMessageBox::Yes: + qDebug().nospace() << "user wants to replace"; + return DECISION_REPLACE; + + case QMessageBox::No: + qDebug().nospace() << "user wants to skip"; + return DECISION_SKIP; + + case QMessageBox::YesToAll: + qDebug().nospace() << "user wants to replace all"; + // remember the answer + m_flags |= REPLACE_ALL; + return DECISION_REPLACE; + + case QMessageBox::NoToAll: + qDebug().nospace() << "user wants to replace none"; + // remember the answer + m_flags |= REPLACE_NONE; + return DECISION_SKIP; + + case QMessageBox::Cancel: // fall-through + default: + qDebug().nospace() << "user wants to cancel"; + return DECISION_CANCEL; + } +} + +bool FileRenamer::removeFailed(const QString& name) +{ + QMessageBox::StandardButtons buttons = QMessageBox::Ok; + if (m_flags & MULTIPLE) { + // only show cancel for multiple files + buttons |= QMessageBox::Cancel; + } + + const auto answer = QMessageBox::critical( + m_parent, QObject::tr("File operation failed"), + QObject::tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(name), + buttons); + + if (answer == QMessageBox::Cancel) { + // user wants to stop + qDebug().nospace() << "user wants to cancel"; + return false; + } + + // skip this one and continue + qDebug().nospace() << "user wants to skip"; + return true; +} + +bool FileRenamer::renameFailed(const QString& oldName, const QString& newName) +{ + QMessageBox::StandardButtons buttons = QMessageBox::Ok; + if (m_flags & MULTIPLE) { + // only show cancel for multiple files + buttons |= QMessageBox::Cancel; + } + + const auto answer = QMessageBox::critical( + m_parent, QObject::tr("File operation failed"), + QObject::tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName)), + buttons); + + if (answer == QMessageBox::Cancel) { + // user wants to stop + qDebug().nospace() << "user wants to cancel"; + return false; + } + + // skip this one and continue + qDebug().nospace() << "user wants to skip"; + return true; +} diff --git a/src/filerenamer.h b/src/filerenamer.h new file mode 100644 index 00000000..cd57244c --- /dev/null +++ b/src/filerenamer.h @@ -0,0 +1,140 @@ +#ifndef FILERENAMER_H +#define FILERENAMER_H + +#include <QWidget> + +/** +* Renames individual files and handles dialog boxes to confirm replacements and +* failures with the user +**/ +class FileRenamer +{ +public: + /** + * controls appearance and replacement behaviour; if RENAME_REPLACE_ALL and + * RENAME_REPLACE_NONE are not provided, the user will have the option to + * choose on the first replacement + **/ + enum RenameFlags + { + /** + * this renamer will be used on multiple files, so display additional + * buttons to replace all and for canceling + **/ + MULTIPLE = 0x01, + + /** + * customizes some of the text shown on dialog to mention that files are + * being hidden + **/ + HIDE = 0x02, + + /** + * customizes some of the text shown on dialog to mention that files are + * being unhidden + **/ + UNHIDE = 0x04, + + /** + * silently replaces all existing files + **/ + REPLACE_ALL = 0x08, + + /** + * silently skips all existing files + **/ + REPLACE_NONE = 0x10, + }; + + + /** result of a single rename + * + **/ + enum RenameResults + { + /** + * the user skipped this file + */ + RESULT_SKIP, + + /** + * the file was successfully renamed + */ + RESULT_OK, + + /** + * the user wants to cancel + */ + RESULT_CANCEL + }; + + + /** + * @param parent Parent widget for dialog boxes + **/ + FileRenamer(QWidget* parent, QFlags<RenameFlags> flags); + + /** + * renames the given file + * @param oldName current filename + * @param newName new filename + * @return whether the file was renamed, skipped or the user wants to cancel + **/ + RenameResults rename(const QString& oldName, const QString& newName); + +private: + /** + *user's decision when replacing + **/ + enum RenameDecision + { + /** + * replace the file + **/ + DECISION_REPLACE, + + /** + * skip the file + **/ + DECISION_SKIP, + + /** + * cancel the whole thing + **/ + DECISION_CANCEL + }; + + /** + * parent widget for dialog boxes + **/ + QWidget* m_parent; + + /** + * flags + **/ + QFlags<RenameFlags> m_flags; + + /** + * asks the user to replace an existing file, may return early if the user + * has already selected to replace all/none + * @return whether to replace, skip or cancel + **/ + RenameDecision confirmReplace(const QString& newName); + + /** + * removal of a file failed, ask the user to continue or cancel + * @param name The name of the file that failed to be removed + * @return true to continue, false to stop + **/ + bool removeFailed(const QString& name); + + /** + * renaming a file failed, ask the user to continue or cancel + * @param oldName current filename + * @param newName new filename + * @return true to continue, false to stop + **/ + bool renameFailed(const QString& oldName, const QString& newName); +}; + +#endif // FILERENAMER_H diff --git a/src/filterwidget.cpp b/src/filterwidget.cpp new file mode 100644 index 00000000..44cbb274 --- /dev/null +++ b/src/filterwidget.cpp @@ -0,0 +1,218 @@ +#include "filterwidget.h" +#include "eventfilter.h" + +FilterWidgetProxyModel::FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent) + : QSortFilterProxyModel(parent), m_filter(fw) +{ + connect(&fw, &FilterWidget::changed, [&]{ invalidateFilter(); }); +} + +bool FilterWidgetProxyModel::filterAcceptsRow( + int sourceRow, const QModelIndex& sourceParent) const +{ + const auto cols = sourceModel()->columnCount(); + + const auto m = m_filter.matches([&](auto&& what) { + for (int c=0; c<cols; ++c) { + QModelIndex index = sourceModel()->index(sourceRow, c, sourceParent); + const auto text = sourceModel()->data(index, Qt::DisplayRole).toString(); + + if (text.contains(what, Qt::CaseInsensitive)) { + return true; + } + } + + return false; + }); + + return m; +} + + +FilterWidget::FilterWidget() : + m_edit(nullptr), m_list(nullptr), m_proxy(nullptr), + m_eventFilter(nullptr), m_clear(nullptr) +{ +} + +void FilterWidget::setEdit(QLineEdit* edit) +{ + unhook(); + + m_edit = edit; + + if (!m_edit) { + return; + } + + m_edit->setPlaceholderText(QObject::tr("Filter")); + + createClear(); + hookEvents(); + clear(); +} + +void FilterWidget::setList(QAbstractItemView* list) +{ + m_list = list; + + m_proxy = new FilterWidgetProxyModel(*this); + m_proxy->setSourceModel(m_list->model()); + m_list->setModel(m_proxy); +} + +void FilterWidget::clear() +{ + if (!m_edit) { + return; + } + + m_edit->clear(); +} + +bool FilterWidget::empty() const +{ + return m_text.isEmpty(); +} + +QModelIndex FilterWidget::map(const QModelIndex& index) +{ + if (m_proxy) { + return m_proxy->mapToSource(index); + } else { + qCritical() << "FilterWidget::map() called, but proxy isn't set up"; + return index; + } +} + +void FilterWidget::compile() +{ + m_compiled.clear(); + + const QStringList ORList = [&] { + QString filterCopy = QString(m_text); + filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";"); + return filterCopy.split(";", QString::SkipEmptyParts); + }(); + + // split in ORSegments that internally use AND logic + for (auto& ORSegment : ORList) { + m_compiled.push_back(ORSegment.split(" ", QString::SkipEmptyParts)); + } +} + +bool FilterWidget::matches(std::function<bool (const QString& what)> pred) const +{ + if (m_compiled.isEmpty() || !pred) { + return true; + } + + for (auto& ANDKeywords : m_compiled) { + bool segmentGood = true; + + // check each word in the segment for match, each word needs to be matched + // but it doesn't matter where. + for (auto& currentKeyword : ANDKeywords) { + if (!pred(currentKeyword)) { + segmentGood = false; + } + } + + if (segmentGood) { + // the last AND loop didn't break so the ORSegments is true so mod + // matches filter + return true; + } + } + + return false; +} + +void FilterWidget::unhook() +{ + if (m_clear) { + delete m_clear; + m_clear = nullptr; + } + + if (m_edit) { + m_edit->removeEventFilter(m_eventFilter); + } + + if (m_proxy && m_list) { + auto* model = m_proxy->sourceModel(); + m_proxy->setSourceModel(nullptr); + delete m_proxy; + + m_list->setModel(model); + } +} + +void FilterWidget::createClear() +{ + m_clear = new QToolButton(m_edit); + + QPixmap pixmap(":/MO/gui/edit_clear"); + m_clear->setIcon(QIcon(pixmap)); + m_clear->setIconSize(pixmap.size()); + m_clear->setCursor(Qt::ArrowCursor); + m_clear->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + m_clear->hide(); + + QObject::connect(m_clear, &QToolButton::clicked, [&]{ clear(); }); + QObject::connect(m_edit, &QLineEdit::textChanged, [&]{ onTextChanged(); }); + + repositionClearButton(); +} + +void FilterWidget::hookEvents() +{ + m_eventFilter = new EventFilter(m_edit, [&](auto* w, auto* e) { + if (e->type() == QEvent::Resize) { + onResized(); + } + + return false; + }); + + m_edit->installEventFilter(m_eventFilter); +} + +void FilterWidget::onTextChanged() +{ + m_clear->setVisible(!m_edit->text().isEmpty()); + + const auto text = m_edit->text(); + + if (text != m_text) { + m_text = text; + compile(); + + if (m_proxy) { + m_proxy->invalidateFilter(); + } + + emit changed(); + } +} + +void FilterWidget::onResized() +{ + repositionClearButton(); +} + +void FilterWidget::repositionClearButton() +{ + if (!m_clear) { + return; + } + + const QSize sz = m_clear->sizeHint(); + const int frame = m_edit->style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + const auto r = m_edit->rect(); + + const auto x = r.right() - frame - sz.width(); + const auto y = (r.bottom() + 1 - sz.height()) / 2; + + m_clear->move(x, y); +} diff --git a/src/filterwidget.h b/src/filterwidget.h new file mode 100644 index 00000000..4fb9831f --- /dev/null +++ b/src/filterwidget.h @@ -0,0 +1,70 @@ +#ifndef FILTERWIDGET_H +#define FILTERWIDGET_H + +#include <QObject> +#include <QLineEdit> +#include <QToolButton> +#include <QList> +#include <QAbstractItemView> + +class EventFilter; +class FilterWidget; + +class FilterWidgetProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT; + +public: + FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent=nullptr); + using QSortFilterProxyModel::invalidateFilter; + +protected: + bool filterAcceptsRow(int row, const QModelIndex& parent) const override; + +private: + FilterWidget& m_filter; +}; + + +class FilterWidget : public QObject +{ + Q_OBJECT; + +public: + using predFun = std::function<bool (const QString& what)>; + + FilterWidget(); + + void setEdit(QLineEdit* edit); + void setList(QAbstractItemView* list); + void clear(); + bool empty() const; + + QModelIndex map(const QModelIndex& index); + + bool matches(predFun pred) const; + +signals: + void changed(); + +private: + QLineEdit* m_edit; + QAbstractItemView* m_list; + FilterWidgetProxyModel* m_proxy; + EventFilter* m_eventFilter; + QToolButton* m_clear; + QString m_text; + QList<QList<QString>> m_compiled; + + void unhook(); + void createClear(); + void hookEvents(); + void repositionClearButton(); + + void onTextChanged(); + void onResized(); + + void compile(); +}; + +#endif // FILTERWIDGET_H diff --git a/src/finddialog.ui b/src/finddialog.ui deleted file mode 100644 index 2c4c80b8..00000000 --- a/src/finddialog.ui +++ /dev/null @@ -1,76 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>FindDialog</class>
- <widget class="QDialog" name="FindDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>400</width>
- <height>72</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Find</string>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Find what:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="patternEdit">
- <property name="toolTip">
- <string>Search term</string>
- </property>
- <property name="whatsThis">
- <string>Search term</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QPushButton" name="nextBtn">
- <property name="toolTip">
- <string>Find next occurence from current file position.</string>
- </property>
- <property name="whatsThis">
- <string>Find next occurence from current file position.</string>
- </property>
- <property name="text">
- <string>&Find Next</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="closeBtn">
- <property name="toolTip">
- <string>Close</string>
- </property>
- <property name="whatsThis">
- <string>Close</string>
- </property>
- <property name="text">
- <string>Close</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/forcedloaddialogwidget.cpp b/src/forcedloaddialogwidget.cpp index 10069c35..b92838c3 100644 --- a/src/forcedloaddialogwidget.cpp +++ b/src/forcedloaddialogwidget.cpp @@ -54,12 +54,12 @@ void ForcedLoadDialogWidget::setForced(bool forced) ui->processEdit->setEnabled(!forced); } -void ForcedLoadDialogWidget::setLibraryPath(QString &path) +void ForcedLoadDialogWidget::setLibraryPath(const QString &path) { ui->libraryPathEdit->setText(path); } -void ForcedLoadDialogWidget::setProcess(QString &name) +void ForcedLoadDialogWidget::setProcess(const QString &name) { ui->processEdit->setText(name); } @@ -71,7 +71,7 @@ void ForcedLoadDialogWidget::on_enabledBox_toggled() void ForcedLoadDialogWidget::on_libraryPathBrowseButton_clicked() { - QDir gameDir(m_GamePlugin->gameDirectory()); + QDir gameDir(m_GamePlugin->gameDirectory()); QString startPath = gameDir.absolutePath(); QString result = QFileDialog::getOpenFileName(nullptr, "Select a library...", startPath, "Dynamic link library (*.dll)", nullptr, QFileDialog::ReadOnly); if (!result.isEmpty()) { @@ -100,7 +100,7 @@ void ForcedLoadDialogWidget::on_processBrowseButton_clicked() QString fileName = fileInfo.fileName(); if (fileInfo.exists()) { - ui->processEdit->setText(fileName); + ui->processEdit->setText(fileName); } else { qCritical("%ls does not exist", fileInfo.filePath().toStdWString().c_str()); } diff --git a/src/forcedloaddialogwidget.h b/src/forcedloaddialogwidget.h index eb7c3f4d..239653c8 100644 --- a/src/forcedloaddialogwidget.h +++ b/src/forcedloaddialogwidget.h @@ -23,8 +23,8 @@ public: void setEnabled(bool enabled); void setForced(bool forced); - void setLibraryPath(QString &path); - void setProcess(QString &name); + void setLibraryPath(const QString &path); + void setProcess(const QString &name); private slots: void on_enabledBox_toggled(); diff --git a/src/gameinfoimpl.cpp b/src/gameinfoimpl.cpp deleted file mode 100644 index 61a4c523..00000000 --- a/src/gameinfoimpl.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "gameinfoimpl.h"
-#include <gameinfo.h>
-#include <utility.h>
-#include <QDir>
-
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-GameInfoImpl::GameInfoImpl()
-{
-}
-
-IGameInfo::Type GameInfoImpl::type() const
-{
- switch (GameInfo::instance().getType()) {
- case GameInfo::TYPE_OBLIVION: return IGameInfo::TYPE_OBLIVION;
- case GameInfo::TYPE_FALLOUT3: return IGameInfo::TYPE_FALLOUT3;
- case GameInfo::TYPE_FALLOUT4: return IGameInfo::TYPE_FALLOUT4;
- case GameInfo::TYPE_FALLOUTNV: return IGameInfo::TYPE_FALLOUTNV;
- case GameInfo::TYPE_SKYRIM: return IGameInfo::TYPE_SKYRIM;
- case GameInfo::TYPE_SKYRIMSE: return IGameInfo::TYPE_SKYRIMSE;
- default: throw MyException(QObject::tr("invalid game type %1").arg(GameInfo::instance().getType()));
- }
-}
-
-
-QString GameInfoImpl::path() const
-{
- return QDir::fromNativeSeparators(ToQString(GameInfo::instance().getGameDirectory()));
-}
-
-QString GameInfoImpl::binaryName() const
-{
- return ToQString(GameInfo::instance().getBinaryName());
-}
diff --git a/src/installdialog.ui b/src/installdialog.ui deleted file mode 100644 index 88019c77..00000000 --- a/src/installdialog.ui +++ /dev/null @@ -1,165 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>InstallDialog</class>
- <widget class="QDialog" name="InstallDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>516</width>
- <height>407</height>
- </rect>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="windowTitle">
- <string>Install Mods</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QGroupBox" name="groupBox">
- <property name="acceptDrops">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string/>
- </property>
- <property name="title">
- <string>New Mod</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="QLabel" name="label">
- <property name="minimumSize">
- <size>
- <width>50</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string>Name</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="editName">
- <property name="toolTip">
- <string>Pick a name for the mod</string>
- </property>
- <property name="whatsThis">
- <string>Pick a name for the mod. This is also used as a directory name, so please don't use characters that are illegal in file names.</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string>Content</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="ArchiveTree" name="treeContent">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>Content of the archive. You can change the directory structure by using drag&drop. Hint: Also try right clicking...</string>
- </property>
- <property name="whatsThis">
- <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This displays the content of the archive. &lt;data&gt; represents the base directory which will map to the game's data directory. You can change the base directory via the right-click context menu and you can move around files via drag&amp;drop</span></p></body></html></string>
- </property>
- <property name="locale">
- <locale language="English" country="UnitedStates"/>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <attribute name="headerVisible">
- <bool>false</bool>
- </attribute>
- <column>
- <property name="text">
- <string notr="true">1</string>
- </property>
- </column>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QLabel" name="problemLabel">
- <property name="font">
- <font>
- <pointsize>12</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="text">
- <string>Placeholder</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="okButton">
- <property name="text">
- <string>OK</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="cancelButton">
- <property name="text">
- <string>Cancel</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <customwidgets>
- <customwidget>
- <class>ArchiveTree</class>
- <extends>QTreeWidget</extends>
- <header>archivetree.h</header>
- </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 1c65f635..ddc2d067 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -102,7 +102,7 @@ QString InstanceManager::manageInstances(const QStringList &instanceList) const SelectionDialog selection( QString("<h3>%1</h3><br>%2") .arg(QObject::tr("Choose Instance to Delete")) - .arg(QObject::tr("Be Carefull! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.")), + .arg(QObject::tr("Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.")), nullptr); for (const QString &instance : instanceList) { @@ -217,6 +217,8 @@ QString InstanceManager::chooseInstance(const QStringList &instanceList) const QObject::tr("Delete an Instance."), static_cast<uint8_t>(Special::Manage)); + selection.setWindowFlags(selection.windowFlags() | Qt::WindowStaysOnTopHint); + if (selection.exec() == QDialog::Rejected) { qDebug("rejected"); throw MOBase::MyException(QObject::tr("Canceled")); @@ -227,7 +229,7 @@ QString InstanceManager::chooseInstance(const QStringList &instanceList) const if (choice.type() == QVariant::String) { return choice.toString(); } else { - switch (choice.value<uint8_t>()) { + switch (static_cast<Special>(choice.value<uint8_t>())) { case Special::NewInstance: return queryInstanceName(instanceList); case Special::Portable: return QString(); case Special::Manage: { diff --git a/src/iuserinterface.h b/src/iuserinterface.h index 034fa029..bba8de2b 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -2,7 +2,7 @@ #define IUSERINTERFACE_H
-#include "modinfo.h"
+#include "modinfodialogfwd.h"
#include "ilockedwaitingforprocess.h"
#include <iplugintool.h>
#include <ipluginmodpage.h>
@@ -29,7 +29,8 @@ public: virtual bool closeWindow() = 0;
virtual void setWindowEnabled(bool enabled) = 0;
- virtual void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab) = 0;
+ virtual void displayModInformation(
+ ModInfoPtr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) = 0;
virtual void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives) = 0;
diff --git a/src/main.cpp b/src/main.cpp index c012b037..f2571685 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -303,6 +303,11 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett gamePath = game->gameDirectory().absolutePath(); } QDir gameDir(gamePath); + QFileInfo directoryInfo(gameDir.path()); + if (directoryInfo.isSymLink()) { + reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. " + "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath)); + } if (game->looksValid(gameDir)) { return selectGame(settings, gameDir, game); } @@ -335,15 +340,26 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett while (selection.exec() != QDialog::Rejected) { IPluginGame * game = selection.getChoiceData().value<IPluginGame *>(); + QString gamePath = selection.getChoiceDescription(); + QFileInfo directoryInfo(gamePath); + if (directoryInfo.isSymLink()) { + reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. " + "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath)); + } if (game != nullptr) { return selectGame(settings, game->gameDirectory(), game); } - QString gamePath = QFileDialog::getExistingDirectory(nullptr, gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName) + gamePath = QFileDialog::getExistingDirectory(nullptr, gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName) : QObject::tr("Please select the game to manage"), QString(), QFileDialog::ShowDirsOnly); if (!gamePath.isEmpty()) { QDir gameDir(gamePath); + QFileInfo directoryInfo(gamePath); + if (directoryInfo.isSymLink()) { + reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. " + "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath)); + } QList<IPluginGame *> possibleGames; for (IPluginGame * const game : plugins.plugins<IPluginGame>()) { //If a game is already configured, skip any plugins that are not for that game @@ -371,8 +387,25 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett } else if(possibleGames.count() == 1) { return selectGame(settings, gameDir, possibleGames[0]); } else { - reportError(gameConfigured ? QObject::tr("%1 not identified in \"%2\". The directory is required to contain the game binary.").arg(gameName).arg(gamePath) - : QObject::tr("No game identified in \"%1\". The directory is required to contain the game binary.").arg(gamePath)); + if (gameConfigured) { + reportError(QObject::tr("%1 not identified in \"%2\". The directory is required to contain the game binary.").arg(gameName).arg(gamePath)); + } else { + QString supportedGames; + + for (IPluginGame * const game : plugins.plugins<IPluginGame>()) { + supportedGames += "<li>" + game->gameName() + "</li>"; + } + + QString text = QObject::tr( + "No game identified in \"%1\". The directory is required to " + "contain the game binary.<br><br>" + "<b>These are the games supported by Mod Organizer:</b>" + "<ul>%2</ul>") + .arg(gamePath) + .arg(supportedGames); + + reportError(text); + } } } } @@ -411,29 +444,46 @@ void setupPath() ::SetEnvironmentVariableW(L"PATH", newPath.c_str()); } -static void preloadSsl() +void preloadDll(const QString& filename) { - QString appPath = QDir::toNativeSeparators(QCoreApplication::applicationDirPath()); - if (GetModuleHandleA("libeay32.dll")) - qWarning("libeay32.dll already loaded?!"); - else { - QString libeay32 = appPath + "\\libeay32.dll"; - if (!QFile::exists(libeay32)) - qWarning("libeay32.dll not found: %s", qUtf8Printable(libeay32)); - else if (!LoadLibraryW(libeay32.toStdWString().c_str())) - qWarning("failed to load: %s, %d", qUtf8Printable(libeay32), GetLastError()); + qDebug().nospace() << "preloading " << filename; + + if (GetModuleHandleW(filename.toStdWString().c_str())) { + // already loaded, this can happen when "restarting" MO by switching + // instances, for example + return; + } + + const auto appPath = QDir::toNativeSeparators( + QCoreApplication::applicationDirPath()); + + const auto dllPath = appPath + "\\" + filename; + + if (!QFile::exists(dllPath)) { + qWarning().nospace() << dllPath << "not found"; + return; } - if (GetModuleHandleA("ssleay32.dll")) - qWarning("ssleay32.dll already loaded?!"); - else { - QString ssleay32 = appPath + "\\ssleay32.dll"; - if (!QFile::exists(ssleay32)) - qWarning("ssleay32.dll not found: %s", qUtf8Printable(ssleay32)); - else if (!LoadLibraryW(ssleay32.toStdWString().c_str())) - qWarning("failed to load: %s, %d", qUtf8Printable(ssleay32), GetLastError()); + + if (!LoadLibraryW(dllPath.toStdWString().c_str())) { + const auto e = GetLastError(); + + qWarning().nospace() + << "failed to load " << dllPath << ": " + << formatSystemMessage(e); } } +void preloadSsl() +{ +#if Q_PROCESSOR_WORDSIZE == 8 + preloadDll("libcrypto-1_1-x64.dll"); + preloadDll("libssl-1_1-x64.dll"); +#elif Q_PROCESSOR_WORDSIZE == 4 + preloadDll("libcrypto-1_1.dll"); + preloadDll("libssl-1_1.dll"); +#endif +} + static QString getVersionDisplayString() { return createVersionInfo().displayString(3); @@ -443,15 +493,9 @@ int runApplication(MOApplication &application, SingleInstance &instance, const QString &splashPath) { - qDebug("Starting Mod Organizer version %s revision %s", qUtf8Printable(getVersionDisplayString()), -#if defined(HGID) - HGID -#elif defined(GITID) - GITID -#else - "unknown" -#endif - ); + qDebug().nospace() + << "Starting Mod Organizer version " + << getVersionDisplayString() << " revision " << GITID; #if !defined(QT_NO_SSL) preloadSsl(); @@ -460,6 +504,27 @@ int runApplication(MOApplication &application, SingleInstance &instance, qDebug("non-ssl build"); #endif + { + env::Environment env; + + qDebug().nospace().noquote() + << "windows: " << env.windowsInfo().toString(); + + if (env.windowsInfo().compatibilityMode()) { + qWarning() << "MO seems to be running in compatibility mode"; + } + + qDebug().nospace().noquote() << "security products:"; + for (const auto& sp : env.securityProducts()) { + qDebug().nospace().noquote() << " . " << sp.toString(); + } + + qDebug() << "modules loaded in process:"; + for (const auto& m : env.loadedModules()) { + qDebug().nospace().noquote() << " . " << m.toString(); + } + } + QString dataPath = application.property("dataPath").toString(); qDebug("data path: %s", qUtf8Printable(dataPath)); @@ -597,6 +662,17 @@ int runApplication(MOApplication &application, SingleInstance &instance, QPixmap pixmap(splashPath); QSplashScreen splash(pixmap); + + if (settings.contains("window_monitor")) { + const int monitor = settings.value("window_monitor").toInt(); + + if (monitor != -1) { + QDesktopWidget* desktop = QApplication::desktop(); + const QPoint center = desktop->availableGeometry(monitor).center(); + splash.move(center - splash.rect().center()); + } + } + splash.show(); splash.activateWindow(); @@ -642,9 +718,52 @@ int runApplication(MOApplication &application, SingleInstance &instance, } } +int doCoreDump(env::CoreDumpTypes type) +{ + // open a console + AllocConsole(); + + // redirect stdin, stdout and stderr to it + FILE* in=nullptr; + FILE* out=nullptr; + FILE* err=nullptr; + freopen_s(&in, "CONIN$", "r", stdin); + freopen_s(&out, "CONOUT$", "w", stdout); + freopen_s(&err, "CONOUT$", "w", stderr); + + // dump + const auto b = env::coredumpOther(type); + if (!b) { + std::wcerr << L"\n>>>> a minidump file was not written\n\n"; + } + + std::wcerr << L"Press enter to continue..."; + std::wcin.get(); + + // close redirected handles + std::fclose(err); + std::fclose(out); + std::fclose(in); + + // close console + FreeConsole(); + + return (b ? 0 : 1); +} int main(int argc, char *argv[]) { + // handle --crashdump first + for (int i=1; i<argc; ++i) { + if (std::strcmp(argv[i], "--crashdump") == 0) { + return doCoreDump(env::CoreDumpTypes::Mini); + } else if (std::strcmp(argv[i], "--crashdump-data") == 0) { + return doCoreDump(env::CoreDumpTypes::Data); + } else if (std::strcmp(argv[i], "--crashdump-full") == 0) { + return doCoreDump(env::CoreDumpTypes::Full); + } + } + //Make sure the configured temp folder exists QDir tempDir = QDir::temp(); if (!tempDir.exists()) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 389e65ca..451ec688 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -77,6 +77,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "nxmaccessmanager.h" #include "appconfig.h" #include "eventfilter.h" +#include "statusbar.h" #include <utility.h> #include <dataarchives.h> #include <bsainvalidation.h> @@ -189,6 +190,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase; using namespace MOShared; +const QSize SmallToolbarSize(24, 24); +const QSize MediumToolbarSize(32, 32); +const QSize LargeToolbarSize(42, 36); + MainWindow::MainWindow(QSettings &initSettings , OrganizerCore &organizerCore @@ -197,6 +202,9 @@ MainWindow::MainWindow(QSettings &initSettings : QMainWindow(parent) , ui(new Ui::MainWindow) , m_WasVisible(false) + , m_menuBarVisible(true) + , m_statusBarVisible(true) + , m_linksSeparator(nullptr) , m_Tutorial(this, "MainWindow") , m_OldProfileIndex(-1) , m_ModListGroupingProxy(nullptr) @@ -206,18 +214,48 @@ MainWindow::MainWindow(QSettings &initSettings , m_ContextItem(nullptr) , m_ContextAction(nullptr) , m_ContextRow(-1) + , m_browseModPage(nullptr) , m_CurrentSaveView(nullptr) , m_OrganizerCore(organizerCore) , m_PluginContainer(pluginContainer) , m_DidUpdateMasterList(false) , m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this)) + , m_LinkToolbar(nullptr) + , m_LinkDesktop(nullptr) + , m_LinkStartMenu(nullptr) { QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); QWebEngineProfile::defaultProfile()->setHttpCacheMaximumSize(52428800); QWebEngineProfile::defaultProfile()->setCachePath(m_OrganizerCore.settings().getCacheDirectory()); QWebEngineProfile::defaultProfile()->setPersistentStoragePath(m_OrganizerCore.settings().getCacheDirectory()); + ui->setupUi(this); - updateWindowTitle(QString(), false); + m_statusBar.reset(new StatusBar(statusBar(), ui)); + + { + auto* ni = NexusInterface::instance(&m_PluginContainer); + + // there are two ways to get here: + // 1) the user just started MO, and + // 2) the user has changed some setting that required a restart + // + // "restarting" MO doesn't actually re-execute the binary, it just basically + // executes most of main() again, so a bunch of things are actually not + // reset + // + // one of these things is the api status, which will have fired its events + // long before the execution gets here because stuff is still cached and no + // real request to nexus is actually done + // + // therefore, when the user starts MO normally, the user account and stats + // will be empty (which is fine) and populated later on when the api key + // check has finished + // + // in the rare case where the user restarts MO through the settings, this + // will correctly pick up the previous values + updateWindowTitle(ni->getAPIUserAccount()); + m_statusBar->setAPI(ni->getAPIStats(), ni->getAPIUserAccount()); + } languageChange(m_OrganizerCore.settings().language()); @@ -235,45 +273,11 @@ MainWindow::MainWindow(QSettings &initSettings connect(ui->logList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), ui->logList, SLOT(scrollToBottom())); - m_RefreshProgress = new QProgressBar(statusBar()); - m_RefreshProgress->setTextVisible(true); - m_RefreshProgress->setRange(0, 100); - m_RefreshProgress->setValue(0); - m_RefreshProgress->setVisible(false); - statusBar()->addWidget(m_RefreshProgress, 1000); - statusBar()->clearMessage(); - statusBar()->hide(); - updateProblemsButton(); - // Setup toolbar - QWidget *spacer = new QWidget(ui->toolBar); - spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - QWidget *widget = ui->toolBar->widgetForAction(ui->actionTool); - QToolButton *toolBtn = qobject_cast<QToolButton*>(widget); - - if (toolBtn->menu() == nullptr) { - actionToToolButton(ui->actionTool); - } - - actionToToolButton(ui->actionHelp); - createHelpWidget(); - - actionToToolButton(ui->actionEndorseMO); - createEndorseWidget(); - + setupToolbar(); toggleMO2EndorseState(); - for (QAction *action : ui->toolBar->actions()) { - if (action->isSeparator()) { - // insert spacers - ui->toolBar->insertWidget(action, spacer); - m_Sep = action; - // m_Sep would only use the last separator anyway, and we only have the one anyway? - break; - } - } - TaskProgressManager::instance().tryCreateTaskbar(); // set up mod list @@ -334,9 +338,9 @@ MainWindow::MainWindow(QSettings &initSettings resizeLists(modListAdjusted, pluginListAdjusted); QMenu *linkMenu = new QMenu(this); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Toolbar"), this, SLOT(linkToolbar())); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); + m_LinkToolbar = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Toolbar and Menu"), this, SLOT(linkToolbar())); + m_LinkDesktop = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); + m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); ui->linkButton->setMenu(linkMenu); QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); @@ -364,13 +368,6 @@ MainWindow::MainWindow(QSettings &initSettings ui->bossButton->setToolTip(tr("There is no supported sort mechanism for this game. You will probably have to use a third-party tool.")); } - ui->apiRequests->setAutoFillBackground(true); - QPalette palette = ui->apiRequests->palette(); - palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen); - palette.setColor(ui->apiRequests->foregroundRole(), Qt::white); - ui->apiRequests->setPalette(palette); - ui->apiRequests->setVisible(!m_OrganizerCore.settings().hideAPICounter()); - connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, SLOT(updateProblemsButton())); connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*))); @@ -403,24 +400,36 @@ MainWindow::MainWindow(QSettings &initSettings connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi())); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString))); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple<int, int, int, int>)), - this, SLOT(updateWindowTitle(const QString&, bool))); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple<int, int, int, int>)), - NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool, std::tuple<int, int, int, int>))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestsChanged(int, std::tuple<int, int, int, int>)), this, SLOT(updateAPICounter(int, std::tuple<int, int, int, int>))); + + connect( + NexusInterface::instance(&pluginContainer)->getAccessManager(), + SIGNAL(credentialsReceived(const APIUserAccount&)), + this, + SLOT(updateWindowTitle(const APIUserAccount&))); + + connect( + NexusInterface::instance(&pluginContainer)->getAccessManager(), + SIGNAL(credentialsReceived(const APIUserAccount&)), + NexusInterface::instance(&m_PluginContainer), + SLOT(setUserAccount(const APIUserAccount&))); + + connect( + NexusInterface::instance(&pluginContainer), + SIGNAL(requestsChanged(const APIStats&, const APIUserAccount&)), + this, + SLOT(onRequestsChanged(const APIStats&, const APIUserAccount&))); connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int))); connect(ui->modList->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), this, SLOT(modListSortIndicatorChanged(int,Qt::SortOrder))); connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint))); + connect(ui->menuToolbars, &QMenu::aboutToShow, [&]{ toolbarMenu_aboutToShow(); }); connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled); connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close); connect(&m_IntegratedBrowser, SIGNAL(requestDownload(QUrl,QNetworkReply*)), &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*))); - connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); - m_CheckBSATimer.setSingleShot(true); connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList())); @@ -482,18 +491,77 @@ MainWindow::MainWindow(QSettings &initSettings } refreshExecutablesList(); - updateToolBar(); + updatePinnedExecutables(); + resetActionIcons(); + updatePluginCount(); + updateModCount(); +} + +void MainWindow::resetActionIcons() +{ + // this is a bit of a hack + // + // the .qss files have historically set qproperty-icon by id and these ids + // correspond to the QActions created in the .ui file + // + // the problem is that QActions do not support having their icon property + // set from a .qss because they're not widgets (they don't inherit from + // QWidget), and styling only works on widget + // + // a QAction _does_ have an associated icon, it just can't be set from a .qss + // file + // + // so here, a dummy QToolButton widget is created for each QAction and is + // given the same name as the action, which makes it pick up the icon + // specified in the .qss file + // + // that icon is then given to the widget used by the QAction (if it's some + // sort of button, which typically happens on the toolbar) _and_ to the + // QAction itself, which is used in the menu bar + + // clearing the notification, will be set below if the stylesheet has set + // anything for it + m_originalNotificationIcon = {}; - for (QAction *action : ui->toolBar->actions()) { - // set the name of the widget to the name of the action to allow styling - QWidget *actionWidget = ui->toolBar->widgetForAction(action); - actionWidget->setObjectName(action->objectName()); - actionWidget->style()->unpolish(actionWidget); - actionWidget->style()->polish(actionWidget); + // QActions created from the .ui file are children of the main window + for (QAction* action : findChildren<QAction*>()) { + // creating a dummy button + auto dummy = std::make_unique<QToolButton>(); + + // reusing the action name + dummy->setObjectName(action->objectName()); + + // styling the button, this has to be done manually because the button is + // never added anywhere + style()->polish(dummy.get()); + + // the button's icon may be null if it wasn't specified in the .qss file, + // which can happen if the stylesheet just doesn't override icons, or for + // other actions like the pinned custom executables + const auto icon = dummy->icon(); + if (icon.isNull()) { + continue; + } + + // button associated with the action on the toolbar + QWidget* actionWidget = ui->toolBar->widgetForAction(action); + + if (auto* actionButton=dynamic_cast<QAbstractButton*>(actionWidget)) { + actionButton->setIcon(icon); + } + + // the action's icon is used by the menu bar + action->setIcon(icon); + + if (action == ui->actionNotifications) { + // if the stylesheet has set a notification icon, remember it here so it + // can be used in updateProblemsButton() + m_originalNotificationIcon = icon; + } } - updatePluginCount(); - updateModCount(); + // update the button for the potentially new icon + updateProblemsButton(); } @@ -514,20 +582,27 @@ MainWindow::~MainWindow() } -void MainWindow::updateWindowTitle(const QString &accountName, bool premium) +void MainWindow::updateWindowTitle(const APIUserAccount& user) { QString title = QString("%1 Mod Organizer v%2").arg( m_OrganizerCore.managedGame()->gameName(), m_OrganizerCore.getVersion().displayString(3)); - if (!accountName.isEmpty()) { - title.append(QString(" (%1%2)").arg(accountName, premium ? "*" : "")); + if (!user.name().isEmpty()) { + const QString premium = (user.type() == APIUserAccountTypes::Premium ? "*" : ""); + title.append(QString(" (%1%2)").arg(user.name(), premium)); } this->setWindowTitle(title); } +void MainWindow::onRequestsChanged(const APIStats& stats, const APIUserAccount& user) +{ + m_statusBar->setAPI(stats, user); +} + + void MainWindow::disconnectPlugins() { if (ui->actionTool->menu() != nullptr) { @@ -583,8 +658,7 @@ void MainWindow::allowListResize() void MainWindow::updateStyle(const QString&) { - // no effect? - ensurePolished(); + resetActionIcons(); } void MainWindow::resizeEvent(QResizeEvent *event) @@ -612,55 +686,223 @@ static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex return result; } +void MainWindow::setupToolbar() +{ + setupActionMenu(ui->actionTool); + setupActionMenu(ui->actionHelp); + setupActionMenu(ui->actionEndorseMO); + + createHelpMenu(); + createEndorseMenu(); + + // find last separator, add a spacer just before it so the icons are + // right-aligned + m_linksSeparator = nullptr; + for (auto* a : ui->toolBar->actions()) { + if (a->isSeparator()) { + m_linksSeparator = a; + } + } + + if (m_linksSeparator) { + auto* spacer = new QWidget(ui->toolBar); + spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + ui->toolBar->insertWidget(m_linksSeparator, spacer); + + } else { + qWarning("no separator found on the toolbar, icons won't be right-aligned"); + } +} -void MainWindow::actionToToolButton(QAction *&sourceAction) +void MainWindow::setupActionMenu(QAction* a) { - QToolButton *button = new QToolButton(ui->toolBar); - button->setObjectName(sourceAction->objectName()); - button->setIcon(sourceAction->icon()); - button->setText(sourceAction->text()); - button->setPopupMode(QToolButton::InstantPopup); - button->setToolButtonStyle(ui->toolBar->toolButtonStyle()); - button->setToolTip(sourceAction->toolTip()); - button->setShortcut(sourceAction->shortcut()); - QMenu *buttonMenu = new QMenu(sourceAction->text(), button); - button->setMenu(buttonMenu); - QAction *newAction = ui->toolBar->insertWidget(sourceAction, button); - newAction->setObjectName(sourceAction->objectName()); - newAction->setIcon(sourceAction->icon()); - newAction->setText(sourceAction->text()); - newAction->setToolTip(sourceAction->toolTip()); - newAction->setShortcut(sourceAction->shortcut()); - ui->toolBar->removeAction(sourceAction); - sourceAction->deleteLater(); - sourceAction = newAction; + a->setMenu(new QMenu(this)); + + auto* w = ui->toolBar->widgetForAction(a); + if (auto* tb=dynamic_cast<QToolButton*>(w)) + tb->setPopupMode(QToolButton::InstantPopup); } -void MainWindow::updateToolBar() +void MainWindow::updatePinnedExecutables() { - for (QAction *action : ui->toolBar->actions()) { - if (action->objectName().startsWith("custom__")) { - ui->toolBar->removeAction(action); - action->deleteLater(); + for (auto* a : ui->toolBar->actions()) { + if (a->objectName().startsWith("custom__")) { + ui->toolBar->removeAction(a); + a->deleteLater(); } } - std::vector<Executable>::iterator begin, end; - m_OrganizerCore.executablesList()->getExecutables(begin, end); - for (auto iter = begin; iter != end; ++iter) { - if (iter->isShownOnToolbar()) { - QAction *exeAction = new QAction(iconForExecutable(iter->m_BinaryInfo.filePath()), - iter->m_Title, - ui->toolBar); - exeAction->setObjectName(QString("custom__") + iter->m_Title); + ui->menuRun->clear(); + + bool hasLinks = false; + + for (const auto& exe : *m_OrganizerCore.executablesList()) { + if (exe.isShownOnToolbar()) { + hasLinks = true; + + QAction *exeAction = new QAction( + iconForExecutable(exe.binaryInfo().filePath()), exe.title()); + + exeAction->setObjectName(QString("custom__") + exe.title()); + exeAction->setStatusTip(exe.binaryInfo().filePath()); + if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) { qDebug("failed to connect trigger?"); } - ui->toolBar->insertAction(m_Sep, exeAction); + + if (m_linksSeparator) { + ui->toolBar->insertAction(m_linksSeparator, exeAction); + } else { + // separator wasn't found, add it to the end + ui->toolBar->addAction(exeAction); + } + + ui->menuRun->addAction(exeAction); } } + + // don't show the menu if there are no links + ui->menuRun->menuAction()->setVisible(hasLinks); } +void MainWindow::toolbarMenu_aboutToShow() +{ + // well, this is a bit of a hack to allow the same toolbar menu to be shown + // in both the main menu and the context menu + // + // the toolbar menu is returned by createPopupMenu(), but Qt takes ownership + // of it and deletes it by setting the WA_DeleteOnClose attribute on it + // + // to avoid deleting the menu, the attribute is removed here + ui->menuToolbars->setAttribute(Qt::WA_DeleteOnClose, false); + + ui->actionMainMenuToggle->setChecked(ui->menuBar->isVisible()); + ui->actionToolBarMainToggle->setChecked(ui->toolBar->isVisible()); + ui->actionStatusBarToggle->setChecked(ui->statusBar->isVisible()); + + ui->actionToolBarSmallIcons->setChecked(ui->toolBar->iconSize() == SmallToolbarSize); + ui->actionToolBarMediumIcons->setChecked(ui->toolBar->iconSize() == MediumToolbarSize); + ui->actionToolBarLargeIcons->setChecked(ui->toolBar->iconSize() == LargeToolbarSize); + + ui->actionToolBarIconsOnly->setChecked(ui->toolBar->toolButtonStyle() == Qt::ToolButtonIconOnly); + ui->actionToolBarTextOnly->setChecked(ui->toolBar->toolButtonStyle() == Qt::ToolButtonTextOnly); + ui->actionToolBarIconsAndText->setChecked(ui->toolBar->toolButtonStyle() == Qt::ToolButtonTextUnderIcon); +} + +QMenu* MainWindow::createPopupMenu() +{ + return ui->menuToolbars; +} + +void MainWindow::on_actionMainMenuToggle_triggered() +{ + showMenuBar(!ui->menuBar->isVisible()); +} + +void MainWindow::on_actionToolBarMainToggle_triggered() +{ + ui->toolBar->setVisible(!ui->toolBar->isVisible()); +} + +void MainWindow::on_actionStatusBarToggle_triggered() +{ + showStatusBar(!ui->statusBar->isVisible()); +} + +void MainWindow::on_actionToolBarSmallIcons_triggered() +{ + setToolbarSize(SmallToolbarSize); +} + +void MainWindow::on_actionToolBarMediumIcons_triggered() +{ + setToolbarSize(MediumToolbarSize); +} + +void MainWindow::on_actionToolBarLargeIcons_triggered() +{ + setToolbarSize(LargeToolbarSize); +} + +void MainWindow::on_actionToolBarIconsOnly_triggered() +{ + setToolbarButtonStyle(Qt::ToolButtonIconOnly); +} + +void MainWindow::on_actionToolBarTextOnly_triggered() +{ + setToolbarButtonStyle(Qt::ToolButtonTextOnly); +} + +void MainWindow::on_actionToolBarIconsAndText_triggered() +{ + setToolbarButtonStyle(Qt::ToolButtonTextUnderIcon); +} + +void MainWindow::setToolbarSize(const QSize& s) +{ + for (auto* tb : findChildren<QToolBar*>()) { + tb->setIconSize(s); + } +} + +void MainWindow::setToolbarButtonStyle(Qt::ToolButtonStyle s) +{ + for (auto* tb : findChildren<QToolBar*>()) { + tb->setToolButtonStyle(s); + } +} + +void MainWindow::showMenuBar(bool b) +{ + ui->menuBar->setVisible(b); + m_menuBarVisible = b; +} + +void MainWindow::showStatusBar(bool b) +{ + ui->statusBar->setVisible(b); + m_statusBarVisible = b; + + // the central widget typically has no bottom padding because the status bar + // is more than enough, but when it's hidden, the bottom widget (currently + // the log) touches the bottom border of the window, which looks ugly + // + // when hiding the statusbar, the central widget is given the same border + // margin as it has on the top (which is typically 6, as it's the default from + // the qt designer) + + auto m = ui->centralWidget->layout()->contentsMargins(); + + if (b) { + m.setBottom(0); + } else { + m.setBottom(m.top()); + } + + ui->centralWidget->layout()->setContentsMargins(m); +} + +void MainWindow::on_centralWidget_customContextMenuRequested(const QPoint &pos) +{ + // this allows for getting the context menu even if both the menubar and all + // the toolbars are hidden; an alternative is the Alt key handled in + // keyPressEvent() below + + // the custom context menu event bubbles up to here if widgets don't actually + // process this, which would show the menu when right-clicking button, labels, + // etc. + // + // only show the context menu when right-clicking on the central widget + // itself, which is basically just the outer edges of the main window + auto* w = childAt(pos); + if (w != ui->centralWidget) { + return; + } + + auto* m = createPopupMenu(); + m->exec(ui->centralWidget->mapToGlobal(pos)); +} void MainWindow::scheduleUpdateButton() { @@ -669,27 +911,61 @@ void MainWindow::scheduleUpdateButton() } } - void MainWindow::updateProblemsButton() { - size_t numProblems = checkForProblems(); + // if the current stylesheet doesn't provide an icon, this is used instead + const char* DefaultIconName = ":/MO/gui/warning"; + + const std::size_t numProblems = checkForProblems(); + + // original icon without a count painted on it + const QIcon original = m_originalNotificationIcon.isNull() ? + QIcon(DefaultIconName) : m_originalNotificationIcon; + + // final icon + QIcon final; + if (numProblems > 0) { - ui->actionNotifications->setEnabled(true); - ui->actionNotifications->setIconText(tr("Notifications")); ui->actionNotifications->setToolTip(tr("There are notifications to read")); - QPixmap mergedIcon = QPixmap(":/MO/gui/warning").scaled(64, 64); + // will contain the original icon, plus a notification count; this also + // makes sure the pixmap is exactly 64x64 by requesting the icon that's + // as close to 64x64 as possible, and then scaling it up if it's too small + QPixmap merged = original.pixmap(64, 64).scaled(64, 64); + { - QPainter painter(&mergedIcon); - std::string badgeName = std::string(":/MO/gui/badge_") + (numProblems < 10 ? std::to_string(static_cast<long long>(numProblems)) : "more"); + QPainter painter(&merged); + + const std::string badgeName = + std::string(":/MO/gui/badge_") + + (numProblems < 10 ? std::to_string(static_cast<long long>(numProblems)) : "more"); + painter.drawPixmap(32, 32, 32, 32, QPixmap(badgeName.c_str())); } - ui->actionNotifications->setIcon(QIcon(mergedIcon)); + + final = QIcon(merged); } else { - ui->actionNotifications->setEnabled(false); - ui->actionNotifications->setIconText(tr("No Notifications")); ui->actionNotifications->setToolTip(tr("There are no notifications")); - ui->actionNotifications->setIcon(QIcon(":/MO/gui/warning")); + + // no change + final = original; + } + + ui->actionNotifications->setEnabled(numProblems > 0); + + // setting the icon on the action (shown on the menu) + ui->actionNotifications->setIcon(final); + + // setting the icon on the toolbar button + if (auto* actionWidget=ui->toolBar->widgetForAction(ui->actionNotifications)) { + if (auto* button=dynamic_cast<QAbstractButton*>(actionWidget)) { + button->setIcon(final); + } + } + + // updating the status bar, may be null very early when MO is starting + if (m_statusBar) { + m_statusBar->setNotifications(numProblems > 0); } } @@ -746,51 +1022,54 @@ void MainWindow::about() } -void MainWindow::createEndorseWidget() +void MainWindow::createEndorseMenu() { - QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionEndorseMO)); - QMenu *buttonMenu = toolBtn->menu(); - if (buttonMenu == nullptr) { + auto* menu = ui->actionEndorseMO->menu(); + if (!menu) { + // shouldn't happen return; } - buttonMenu->clear(); - QAction *endorseAction = new QAction(tr("Endorse"), buttonMenu); + menu->clear(); + + QAction *endorseAction = new QAction(tr("Endorse"), menu); connect(endorseAction, SIGNAL(triggered()), this, SLOT(actionEndorseMO())); - buttonMenu->addAction(endorseAction); + menu->addAction(endorseAction); - QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu); + QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), menu); connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(actionWontEndorseMO())); - buttonMenu->addAction(wontEndorseAction); + menu->addAction(wontEndorseAction); } -void MainWindow::createHelpWidget() +void MainWindow::createHelpMenu() { - QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionHelp)); - QMenu *buttonMenu = toolBtn->menu(); - if (buttonMenu == nullptr) { + auto* menu = ui->actionHelp->menu(); + if (!menu) { + // this happens on startup because languageChanged() (which calls this) is + // called before the menus are actually created return; } - buttonMenu->clear(); - QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu); + menu->clear(); + + QAction *helpAction = new QAction(tr("Help on UI"), menu); connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered())); - buttonMenu->addAction(helpAction); + menu->addAction(helpAction); - QAction *wikiAction = new QAction(tr("Documentation"), buttonMenu); + QAction *wikiAction = new QAction(tr("Documentation"), menu); connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered())); - buttonMenu->addAction(wikiAction); + menu->addAction(wikiAction); - QAction *discordAction = new QAction(tr("Chat on Discord"), buttonMenu); + QAction *discordAction = new QAction(tr("Chat on Discord"), menu); connect(discordAction, SIGNAL(triggered()), this, SLOT(discordTriggered())); - buttonMenu->addAction(discordAction); + menu->addAction(discordAction); - QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu); + QAction *issueAction = new QAction(tr("Report Issue"), menu); connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered())); - buttonMenu->addAction(issueAction); + menu->addAction(issueAction); - QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu); + QMenu *tutorialMenu = new QMenu(tr("Tutorials"), menu); typedef std::vector<std::pair<int, QAction*> > ActionList; @@ -828,9 +1107,9 @@ void MainWindow::createHelpWidget() tutorialMenu->addAction(iter->second); } - buttonMenu->addMenu(tutorialMenu); - buttonMenu->addAction(tr("About"), this, SLOT(about())); - buttonMenu->addAction(tr("About Qt"), qApp, SLOT(aboutQt())); + menu->addMenu(tutorialMenu); + menu->addAction(tr("About"), this, SLOT(about())); + menu->addAction(tr("About Qt"), qApp, SLOT(aboutQt())); } void MainWindow::modFilterActive(bool filterActive) @@ -934,6 +1213,16 @@ void MainWindow::showEvent(QShowEvent *event) QMainWindow::showEvent(event); if (!m_WasVisible) { + // this needs to be connected here instead of in the constructor because the + // actual changing of the stylesheet is done by MOApplication, which + // connects its signal in runApplication() (in main.cpp), and that happens + // _after_ the MainWindow is constructed, but _before_ it is shown + // + // by connecting the event here, changing the style setting will first be + // handled by MOApplication, and then in updateStyle(), at which point the + // stylesheet has already been set correctly + connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); + // only the first time the window becomes visible m_Tutorial.registerControl(); @@ -976,14 +1265,21 @@ void MainWindow::showEvent(QShowEvent *event) void MainWindow::closeEvent(QCloseEvent* event) { + if (!confirmExit()) { + event->ignore(); + } +} + +bool MainWindow::confirmExit() +{ m_closing = true; if (m_OrganizerCore.downloadManager()->downloadsInProgressNoPause()) { if (QMessageBox::question(this, tr("Downloads in progress"), tr("There are still downloads in progress, do you really want to quit?"), QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) { - event->ignore(); - return; + m_closing = false; + return false; } else { m_OrganizerCore.downloadManager()->pauseAll(); } @@ -996,12 +1292,12 @@ void MainWindow::closeEvent(QCloseEvent* event) { m_OrganizerCore.waitForApplication(injected_process_still_running); if (!m_closing) { // if operation cancelled - event->ignore(); - return; + return false; } } setCursor(Qt::WaitCursor); + return true; } void MainWindow::cleanup() @@ -1025,6 +1321,15 @@ void MainWindow::setBrowserGeometry(const QByteArray &geometry) void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem) { + // don't display the widget if the main window doesn't have focus + // + // this goes against the standard behaviour for tooltips, which are displayed + // on hover regardless of focus, but this widget is so large and busy that + // it's probably better this way + if (!isActiveWindow()){ + return; + } + QString const &save = newItem->data(Qt::UserRole).toString(); if (m_CurrentSaveView == nullptr) { IPluginGame const *game = m_OrganizerCore.managedGame(); @@ -1056,8 +1361,6 @@ void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem) m_CurrentSaveView->show(); m_CurrentSaveView->setProperty("displayItem", qVariantFromValue(static_cast<void *>(newItem))); - - ui->savegameList->activateWindow(); } @@ -1120,21 +1423,20 @@ void MainWindow::modPagePluginInvoke() void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu) { + if (!menu) { + menu = ui->actionTool->menu(); + } + if (name.isEmpty()) name = tool->displayName(); - QAction *action = new QAction(tool->icon(), name, ui->toolBar); + QAction *action = new QAction(tool->icon(), name, menu); action->setToolTip(tool->tooltip()); tool->setParentWidget(this); action->setData(qVariantFromValue((QObject*)tool)); connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke()), Qt::QueuedConnection); - if (menu == nullptr) { - QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionTool)); - toolBtn->menu()->addAction(action); - } else { - menu->addAction(action); - } + menu->addAction(action); } void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins) @@ -1168,8 +1470,7 @@ void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins) for (auto info : submenuMap[submenuKey]) { registerPluginTool(info.second, info.first, submenu); } - QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionTool)); - toolBtn->menu()->addMenu(submenu); + ui->actionTool->menu()->addMenu(submenu); } else { registerPluginTool(submenuMap[submenuKey].front().second); @@ -1180,49 +1481,68 @@ void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins) void MainWindow::registerModPage(IPluginModPage *modPage) { // turn the browser action into a drop-down menu if necessary - if (ui->actionNexus->menu() == nullptr) { - QAction *nexusAction = ui->actionNexus; - // TODO: use a different icon for nexus! - ui->actionNexus = new QAction(nexusAction->icon(), tr("Browse Mod Page"), ui->toolBar); - ui->toolBar->insertAction(nexusAction, ui->actionNexus); - ui->toolBar->removeAction(nexusAction); - actionToToolButton(ui->actionNexus); + if (!m_browseModPage) { + m_browseModPage = new QAction(ui->actionNexus->icon(), tr("Browse Mod Page"), this); + setupActionMenu(m_browseModPage); + + m_browseModPage->menu()->addAction(ui->actionNexus); - QToolButton *browserBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus)); - browserBtn->menu()->addAction(nexusAction); + ui->toolBar->insertAction(ui->actionNexus, m_browseModPage); + ui->toolBar->removeAction(ui->actionNexus); } - QAction *action = new QAction(modPage->icon(), modPage->displayName(), ui->toolBar); + QAction *action = new QAction(modPage->icon(), modPage->displayName(), this); modPage->setParentWidget(this); action->setData(qVariantFromValue(reinterpret_cast<QObject*>(modPage))); connect(action, SIGNAL(triggered()), this, SLOT(modPagePluginInvoke()), Qt::QueuedConnection); - QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus)); - toolBtn->menu()->addAction(action); + + m_browseModPage->menu()->addAction(action); } void MainWindow::startExeAction() { QAction *action = qobject_cast<QAction*>(sender()); - if (action != nullptr) { - const Executable &selectedExecutable(m_OrganizerCore.executablesList()->find(action->text())); - QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.m_Title).toString(); - auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.m_Title); - if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.m_Title)) { - forcedLibraries.clear(); - } - m_OrganizerCore.spawnBinary( - selectedExecutable.m_BinaryInfo, selectedExecutable.m_Arguments, - selectedExecutable.m_WorkingDirectory.length() != 0 - ? selectedExecutable.m_WorkingDirectory - : selectedExecutable.m_BinaryInfo.absolutePath(), - selectedExecutable.m_SteamAppID, - customOverwrite, - forcedLibraries); - } else { + + if (action == nullptr) { qCritical("not an action?"); + return; + } + + const auto& list = *m_OrganizerCore.executablesList(); + + const auto title = action->text(); + auto itor = list.find(title); + + if (itor == list.end()) { + qWarning().nospace() + << "startExeAction(): executable '" << title << "' not found"; + + return; + } + + action->setEnabled(false); + const Executable& exe = *itor; + auto& profile = *m_OrganizerCore.currentProfile(); + + QString customOverwrite = profile.setting("custom_overwrites", exe.title()).toString(); + auto forcedLibraries = profile.determineForcedLibraries(exe.title()); + + if (!profile.forcedLibrariesEnabled(exe.title())) { + forcedLibraries.clear(); } + + m_OrganizerCore.spawnBinary( + exe.binaryInfo(), exe.arguments(), + exe.workingDirectory().length() != 0 + ? exe.workingDirectory() + : exe.binaryInfo().absolutePath(), + exe.steamAppID(), + customOverwrite, + forcedLibraries); + action->setEnabled(true); + } @@ -1490,12 +1810,12 @@ void MainWindow::refreshExecutablesList() QAbstractItemModel *model = executablesList->model(); - std::vector<Executable>::const_iterator current, end; - m_OrganizerCore.executablesList()->getExecutables(current, end); - for(int i = 0; current != end; ++current, ++i) { - QIcon icon = iconForExecutable(current->m_BinaryInfo.filePath()); - executablesList->addItem(icon, current->m_Title); + int i = 0; + for (const auto& exe : *m_OrganizerCore.executablesList()) { + QIcon icon = iconForExecutable(exe.binaryInfo().filePath()); + executablesList->addItem(icon, exe.title()); model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole); + ++i; } setExecutableIndex(1); @@ -1846,6 +2166,27 @@ void MainWindow::readSettings() restoreGeometry(settings.value("window_geometry").toByteArray()); } + if (settings.contains("window_state")) { + restoreState(settings.value("window_state").toByteArray()); + } + + if (settings.contains("toolbar_size")) { + setToolbarSize(settings.value("toolbar_size").toSize()); + } + + if (settings.contains("toolbar_button_style")) { + setToolbarButtonStyle(static_cast<Qt::ToolButtonStyle>( + settings.value("toolbar_button_style").toInt())); + } + + if (settings.contains("menubar_visible")) { + showMenuBar(settings.value("menubar_visible").toBool()); + } + + if (settings.contains("statusbar_visible")) { + showStatusBar(settings.value("statusbar_visible").toBool()); + } + if (settings.contains("window_split")) { ui->splitter->restoreState(settings.value("window_split").toByteArray()); } @@ -1897,6 +2238,12 @@ void MainWindow::processUpdates() { instance.remove(""); instance.endGroup(); } + if (lastVersion < QVersionNumber(2, 2, 1)) { + // hide new columns by default + for (int i=DownloadList::COL_MODNAME; i<DownloadList::COL_COUNT; ++i) { + ui->downloadView->header()->hideSection(i); + } + } } if (currentVersion > lastVersion) { @@ -1916,7 +2263,12 @@ void MainWindow::storeSettings(QSettings &settings) { if (settings.value("reset_geometry", false).toBool()) { settings.remove("window_geometry"); + settings.remove("window_state"); + settings.remove("toolbar_size"); + settings.remove("toolbar_button_style"); + settings.remove("menubar_visible"); settings.remove("window_split"); + settings.remove("window_monitor"); settings.remove("log_split"); settings.remove("filters_visible"); settings.remove("browser_geometry"); @@ -1924,7 +2276,13 @@ void MainWindow::storeSettings(QSettings &settings) { settings.remove("reset_geometry"); } else { settings.setValue("window_geometry", saveGeometry()); + settings.setValue("window_state", saveState()); + settings.setValue("toolbar_size", ui->toolBar->iconSize()); + settings.setValue("toolbar_button_style", static_cast<int>(ui->toolBar->toolButtonStyle())); + settings.setValue("menubar_visible", m_menuBarVisible); + settings.setValue("statusbar_visible", m_statusBarVisible); settings.setValue("window_split", ui->splitter->saveState()); + settings.setValue("window_monitor", QApplication::desktop()->screenNumber(this)); settings.setValue("log_split", ui->topLevelSplitter->saveState()); settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry()); settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); @@ -2022,17 +2380,17 @@ void MainWindow::on_startButton_clicked() { ui->startButton->setEnabled(false); try { const Executable &selectedExecutable(getSelectedExecutable()); - QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.m_Title).toString(); - auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.m_Title); - if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.m_Title)) { + QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.title()).toString(); + auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.title()); + if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.title())) { forcedLibraries.clear(); } m_OrganizerCore.spawnBinary( - selectedExecutable.m_BinaryInfo, selectedExecutable.m_Arguments, - selectedExecutable.m_WorkingDirectory.length() != 0 - ? selectedExecutable.m_WorkingDirectory - : selectedExecutable.m_BinaryInfo.absolutePath(), - selectedExecutable.m_SteamAppID, + selectedExecutable.binaryInfo(), selectedExecutable.arguments(), + selectedExecutable.workingDirectory().length() != 0 + ? selectedExecutable.workingDirectory() + : selectedExecutable.binaryInfo().absolutePath(), + selectedExecutable.steamAppID(), customOverwrite, forcedLibraries); } catch (...) { @@ -2042,107 +2400,27 @@ void MainWindow::on_startButton_clicked() { ui->startButton->setEnabled(true); } -static HRESULT CreateShortcut(LPCWSTR targetFileName, LPCWSTR arguments, - LPCSTR linkFileName, LPCWSTR description, - LPCTSTR iconFileName, int iconNumber, - LPCWSTR currentDirectory) -{ - HRESULT result = E_INVALIDARG; - if ((targetFileName != nullptr) && (wcslen(targetFileName) > 0) && - (arguments != nullptr) && - (linkFileName != nullptr) && (strlen(linkFileName) > 0) && - (description != nullptr) && - (currentDirectory != nullptr)) { - - IShellLink* shellLink; - result = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, - IID_IShellLink, (LPVOID*)&shellLink); - - if (!SUCCEEDED(result)) { - qCritical("failed to create IShellLink instance"); - return result; - } - - result = shellLink->SetPath(targetFileName); - if (!SUCCEEDED(result)) { - qCritical("failed to set target path %ls", targetFileName); - shellLink->Release(); - return result; - } - - result = shellLink->SetArguments(arguments); - if (!SUCCEEDED(result)) { - qCritical("failed to set arguments: %ls", arguments); - shellLink->Release(); - return result; - } - - if (wcslen(description) > 0) { - result = shellLink->SetDescription(description); - if (!SUCCEEDED(result)) { - qCritical("failed to set description: %ls", description); - shellLink->Release(); - return result; - } - } - - if (wcslen(currentDirectory) > 0) { - result = shellLink->SetWorkingDirectory(currentDirectory); - if (!SUCCEEDED(result)) { - qCritical("failed to set working directory: %ls", currentDirectory); - shellLink->Release(); - return result; - } - } - - if (iconFileName != nullptr) { - result = shellLink->SetIconLocation(iconFileName, iconNumber); - if (!SUCCEEDED(result)) { - qCritical("failed to load program icon: %ls %d", iconFileName, iconNumber); - shellLink->Release(); - return result; - } - } - - IPersistFile *persistFile; - result = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); - if (SUCCEEDED(result)) { - wchar_t linkFileNameW[MAX_PATH]; - if (MultiByteToWideChar(CP_ACP, 0, linkFileName, -1, linkFileNameW, MAX_PATH) > 0) { - result = persistFile->Save(linkFileNameW, TRUE); - } else { - qCritical("failed to create link: %s", linkFileName); - } - persistFile->Release(); - } else { - qCritical("failed to create IPersistFile instance"); - } - - shellLink->Release(); - } - return result; -} - - bool MainWindow::modifyExecutablesDialog() { bool result = false; + try { - EditExecutablesDialog dialog(*m_OrganizerCore.executablesList(), - *m_OrganizerCore.modList(), - m_OrganizerCore.currentProfile(), - m_OrganizerCore.managedGame()); + const auto oldExecutables = *m_OrganizerCore.executablesList(); + + EditExecutablesDialog dialog(m_OrganizerCore, this); + QSettings &settings = m_OrganizerCore.settings().directInterface(); QString key = QString("geometry/%1").arg(dialog.objectName()); + if (settings.contains(key)) { dialog.restoreGeometry(settings.value(key).toByteArray()); } - if (dialog.exec() == QDialog::Accepted) { - m_OrganizerCore.setExecutablesList(dialog.getExecutablesList()); - result = true; - } + + result = (dialog.exec() == QDialog::Accepted); + settings.setValue(key, dialog.saveGeometry()); refreshExecutablesList(); + updatePinnedExecutables(); } catch (const std::exception &e) { reportError(e.what()); } @@ -2277,26 +2555,22 @@ void MainWindow::setESPListSorting(int index) void MainWindow::refresher_progress(int percent) { - if (percent == 100) { - m_RefreshProgress->setVisible(false); - statusBar()->hide(); - this->setEnabled(true); - } else if (!m_RefreshProgress->isVisible()) { - this->setEnabled(false); - statusBar()->show(); - m_RefreshProgress->setVisible(true); - m_RefreshProgress->setRange(0, 100); - m_RefreshProgress->setValue(percent); - } + setEnabled(percent == 100); + m_statusBar->setProgress(percent); } void MainWindow::directory_refreshed() { // some problem-reports may rely on the virtual directory tree so they need to be updated // now - refreshDataTreeKeepExpandedNodes(); updateProblemsButton(); - statusBar()->hide(); + + + //Some better check for the current tab is needed. + if (ui->tabWidget->currentIndex() == 2) { + refreshDataTreeKeepExpandedNodes(); + } + } void MainWindow::esplist_changed() @@ -2628,18 +2902,34 @@ void MainWindow::modListSectionResized(int logicalIndex, int oldSize, int newSiz void MainWindow::removeMod_clicked() { + const int max_items = 20; + try { QItemSelectionModel *selection = ui->modList->selectionModel(); if (selection->hasSelection() && selection->selectedRows().count() > 1) { QString mods; QStringList modNames; + + int i = 0; for (QModelIndex idx : selection->selectedRows()) { QString name = idx.data().toString(); if (!ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->isRegular()) { continue; } - mods += "<li>" + name + "</li>"; + + // adds an item for the mod name until `i` reaches `max_items`, which + // adds one "..." item; subsequent mods are not shown on the list but + // are still added to `modNames` below so they can be removed correctly + + if (i < max_items) { + mods += "<li>" + name + "</li>"; + } + else if (i == max_items) { + mods += "<li>...</li>"; + } + modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name()); + ++i; } if (QMessageBox::question(this, tr("Confirm"), tr("Remove the following mods?<br><ul>%1</ul>").arg(mods), @@ -2710,66 +3000,34 @@ void MainWindow::backupMod_clicked() void MainWindow::resumeDownload(int downloadIndex) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_OrganizerCore.loggedInAction(this, [this, downloadIndex] { m_OrganizerCore.downloadManager()->resumeDownload(downloadIndex); - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([this, downloadIndex] () { - this->resumeDownload(downloadIndex); - }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to resume a download"), this); - } - } + }); } void MainWindow::endorseMod(ModInfo::Ptr mod) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_OrganizerCore.loggedInAction(this, [this, mod] { mod->endorse(true); - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([&]() { this->endorseMod(mod); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - } - } + }); } void MainWindow::endorse_clicked() { QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + + m_OrganizerCore.loggedInAction(this, [this] { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(true); - } } - else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.doAfterLogin([&]() { this->endorseMod(modInfo); }); - } - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - return; - } + + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(true); } - } - else { - endorseMod(ModInfo::getByIndex(m_ContextRow)); - } + }); } void MainWindow::dontendorse_clicked() @@ -2788,120 +3046,58 @@ void MainWindow::dontendorse_clicked() void MainWindow::unendorseMod(ModInfo::Ptr mod) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - ModInfo::getByIndex(m_ContextRow)->endorse(false); - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([&]() { this->unendorseMod(mod); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - } - } + m_OrganizerCore.loggedInAction(this, [mod] { + mod->endorse(false); + }); } void MainWindow::unendorse_clicked() { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_OrganizerCore.loggedInAction(this, [this] { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(false); - } - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.doAfterLogin([&]() { this->unendorseMod(modInfo); }); - } - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - return; - } } - } - else { - unendorseMod(ModInfo::getByIndex(m_ContextRow)); - } + + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(false); + } + }); } void MainWindow::trackMod(ModInfo::Ptr mod, bool doTrack) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - ModInfo::getByIndex(m_ContextRow)->track(doTrack); - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([&]() { this->trackMod(mod, doTrack); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to track"), this); - } - } + m_OrganizerCore.loggedInAction(this, [mod, doTrack] { + mod->track(doTrack); + }); } void MainWindow::track_clicked() { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - for (auto idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(true); - } - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - for (auto idx : selection->selectedRows()) { - auto modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.doAfterLogin([&]() { this->trackMod(modInfo, true); }); - } - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to track"), this); - } + m_OrganizerCore.loggedInAction(this, [this] { + QItemSelectionModel *selection = ui->modList->selectionModel(); + for (auto idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(true); } - } else { - trackMod(ModInfo::getByIndex(m_ContextRow), true); - } + }); } void MainWindow::untrack_clicked() { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - for (auto idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(false); - } - } else { - QString apiKey; - if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - for (auto idx : selection->selectedRows()) { - auto modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.doAfterLogin([&]() { this->trackMod(modInfo, false); }); - } - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to track"), this); - } + m_OrganizerCore.loggedInAction(this, [this] { + QItemSelectionModel *selection = ui->modList->selectionModel(); + for (auto idx : selection->selectedRows()) { + ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(false); } - } else { - trackMod(ModInfo::getByIndex(m_ContextRow), false); - } + }); } void MainWindow::validationFailed(const QString &error) { qDebug("Nexus API validation failed: %s", qUtf8Printable(error)); - statusBar()->hide(); } void MainWindow::windowTutorialFinished(const QString &windowName) @@ -2923,7 +3119,8 @@ void MainWindow::overwriteClosed(int) } -void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab) +void MainWindow::displayModInformation( + ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) { if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) { qDebug("A different mod information dialog is open. If this is incorrect, please restart MO"); @@ -2953,39 +3150,24 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, } } else { modInfo->saveMeta(); - ModInfoDialog dialog(modInfo, m_OrganizerCore.directoryStructure(), modInfo->hasFlag(ModInfo::FLAG_FOREIGN), &m_OrganizerCore, &m_PluginContainer, this); - connect(&dialog, SIGNAL(linkActivated(QString)), this, SLOT(linkClicked(QString))); - connect(&dialog, SIGNAL(downloadRequest(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); - connect(&dialog, SIGNAL(modOpen(QString, int)), this, SLOT(displayModInformation(QString, int)), Qt::QueuedConnection); - connect(&dialog, SIGNAL(modOpenNext(int)), this, SLOT(modOpenNext(int)), Qt::QueuedConnection); - connect(&dialog, SIGNAL(modOpenPrev(int)), this, SLOT(modOpenPrev(int)), Qt::QueuedConnection); - connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); - connect(&dialog, SIGNAL(endorseMod(ModInfo::Ptr)), this, SLOT(endorseMod(ModInfo::Ptr))); - //Open the tab first if we want to use the standard indexes of the tabs. - if (tab != -1) { - dialog.openTab(tab); - } + ModInfoDialog dialog(this, &m_OrganizerCore, &m_PluginContainer, modInfo); + connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); - dialog.restoreTabState(m_OrganizerCore.settings().directInterface().value("mod_info_tabs").toByteArray()); - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(dialog.objectName()); - if (settings.contains(key)) { - dialog.restoreGeometry(settings.value(key).toByteArray()); - } + //Open the tab first if we want to use the standard indexes of the tabs. + if (tabID != ModInfoTabIDs::None) { + dialog.selectTab(tabID); + } - //If no tab was specified use the first tab from the left based on the user order. - if (tab == -1) { - for (int i = 0; i < dialog.findChild<QTabWidget*>("tabWidget")->count(); ++i) { - if (dialog.findChild<QTabWidget*>("tabWidget")->isTabEnabled(i)) { - dialog.findChild<QTabWidget*>("tabWidget")->setCurrentIndex(i); - break; - } - } - } + dialog.restoreState(m_OrganizerCore.settings()); + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(dialog.objectName()); + if (settings.contains(key)) { + dialog.restoreGeometry(settings.value(key).toByteArray()); + } dialog.exec(); - m_OrganizerCore.settings().directInterface().setValue("mod_info_tabs", dialog.saveTabState()); + dialog.saveState(m_OrganizerCore.settings()); settings.setValue(key, dialog.saveGeometry()); modInfo->saveMeta(); @@ -2993,7 +3175,7 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, m_OrganizerCore.modList()->modInfoChanged(modInfo); } - if (m_OrganizerCore.currentProfile()->modEnabled(index) + if (m_OrganizerCore.currentProfile()->modEnabled(modIndex) && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); origin.enable(false); @@ -3004,7 +3186,7 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, m_OrganizerCore.directoryRefresher()->addModToStructure(m_OrganizerCore.directoryStructure() , modInfo->name() - , m_OrganizerCore.currentProfile()->getModPriority(index) + , m_OrganizerCore.currentProfile()->getModPriority(modIndex) , modInfo->absolutePath() , modInfo->stealFiles() , modInfo->archives()); @@ -3026,46 +3208,74 @@ void MainWindow::setWindowEnabled(bool enabled) } -void MainWindow::modOpenNext(int tab) +ModInfo::Ptr MainWindow::nextModInList() { - QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0)); - index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0); + const QModelIndex start = m_ModListSortProxy->mapFromSource( + m_OrganizerCore.modList()->index(m_ContextRow, 0)); + + auto index = start; + + for (;;) { + index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0); + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + + if (index == start || !index.isValid()) { + // wrapped around, give up + break; + } + + ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - std::vector<ModInfo::EFlag> flags = mod->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())) { // skip overwrite and backups and separators - modOpenNext(tab); - } else { - displayModInformation(m_ContextRow,tab); + if (mod->hasFlag(ModInfo::FLAG_OVERWRITE) || + mod->hasFlag(ModInfo::FLAG_BACKUP) || + mod->hasFlag(ModInfo::FLAG_SEPARATOR)) { + continue; + } + + return mod; } + + return {}; } -void MainWindow::modOpenPrev(int tab) +ModInfo::Ptr MainWindow::previousModInList() { - QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0)); - int row = index.row() - 1; - if (row == -1) { - row = m_ModListSortProxy->rowCount() - 1; - } + const QModelIndex start = m_ModListSortProxy->mapFromSource( + m_OrganizerCore.modList()->index(m_ContextRow, 0)); + + auto index = start; + + for (;;) { + int row = index.row() - 1; + if (row == -1) { + row = m_ModListSortProxy->rowCount() - 1; + } + + index = m_ModListSortProxy->index(row, 0); + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + + if (index == start || !index.isValid()) { + // wrapped around, give up + break; + } - m_ContextRow = m_ModListSortProxy->mapToSource(m_ModListSortProxy->index(row, 0)).row(); - ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - std::vector<ModInfo::EFlag> flags = mod->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) || - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())) { // skip overwrite and backups and separators - modOpenPrev(tab); - } else { - displayModInformation(m_ContextRow,tab); + ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); + + if (mod->hasFlag(ModInfo::FLAG_OVERWRITE) || + mod->hasFlag(ModInfo::FLAG_BACKUP) || + mod->hasFlag(ModInfo::FLAG_SEPARATOR)) { + continue; + } + + return mod; } + + return {}; } -void MainWindow::displayModInformation(const QString &modName, int tab) +void MainWindow::displayModInformation(const QString &modName, ModInfoTabIDs tabID) { unsigned int index = ModInfo::getIndex(modName); if (index == UINT_MAX) { @@ -3074,14 +3284,14 @@ void MainWindow::displayModInformation(const QString &modName, int tab) } ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - displayModInformation(modInfo, index, tab); + displayModInformation(modInfo, index, tabID); } -void MainWindow::displayModInformation(int row, int tab) +void MainWindow::displayModInformation(int row, ModInfoTabIDs tabID) { ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - displayModInformation(modInfo, row, tab); + displayModInformation(modInfo, row, tabID); } @@ -3145,18 +3355,16 @@ void MainWindow::visitOnNexus_clicked() int row_idx; ModInfo::Ptr info; QString gameName; - QString webUrl; + for (QModelIndex idx : selection->selectedRows()) { row_idx = idx.data(Qt::UserRole + 1).toInt(); info = ModInfo::getByIndex(row_idx); int modID = info->getNexusID(); - webUrl = info->getURL(); gameName = info->getGameName(); if (modID > 0) { linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); - } - else if (webUrl != "") { - linkClicked(webUrl); + } else { + qCritical() << "mod '" << info->name() << "' has no nexus id"; } } } @@ -3166,14 +3374,13 @@ void MainWindow::visitOnNexus_clicked() if (modID > 0) { linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); } else { - MessageDialog::showMessage(tr("Nexus ID for this Mod is unknown"), this); + MessageDialog::showMessage(tr("Nexus ID for this mod is unknown"), this); } } } void MainWindow::visitWebPage_clicked() { - QItemSelectionModel *selection = ui->modList->selectionModel(); if (selection->hasSelection() && selection->selectedRows().count() > 1) { int count = selection->selectedRows().count(); @@ -3187,28 +3394,22 @@ void MainWindow::visitWebPage_clicked() int row_idx; ModInfo::Ptr info; QString gameName; - QString webUrl; for (QModelIndex idx : selection->selectedRows()) { row_idx = idx.data(Qt::UserRole + 1).toInt(); info = ModInfo::getByIndex(row_idx); - int modID = info->getNexusID(); - webUrl = info->getURL(); - gameName = info->getGameName(); - if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); - } - else if (webUrl != "") { - linkClicked(webUrl); + + const auto url = info->parseCustomURL(); + if (url.isValid()) { + linkClicked(url.toString()); } } } else { ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - if (info->getURL() != "") { - linkClicked(info->getURL()); - } - else { - MessageDialog::showMessage(tr("Web page for this mod is unknown"), this); + + const auto url = info->parseCustomURL(); + if (url.isValid()) { + linkClicked(url.toString()); } } } @@ -3219,16 +3420,16 @@ void MainWindow::openExplorer_clicked() if (selection->hasSelection() && selection->selectedRows().count() > 1) { for (QModelIndex idx : selection->selectedRows()) { ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - ::ShellExecuteW(nullptr, L"explore", ToWString(info->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(info->absolutePath()); } } else { ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(modInfo->absolutePath()); } } -void MainWindow::openOriginExplorer_clicked() +void MainWindow::openPluginOriginExplorer_clicked() { QItemSelectionModel *selection = ui->espList->selectionModel(); if (selection->hasSelection() && selection->selectedRows().count() > 0) { @@ -3239,18 +3440,14 @@ void MainWindow::openOriginExplorer_clicked() continue; } ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(modInfo->absolutePath()); } } else { QModelIndex idx = selection->currentIndex(); QString fileName = idx.data().toString(); ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(modInfo->absolutePath()); } } @@ -3265,7 +3462,7 @@ void MainWindow::openExplorer_activated() std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(modInfo->absolutePath()); } } @@ -3286,7 +3483,7 @@ void MainWindow::openExplorer_activated() std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(modInfo->absolutePath()); } } } @@ -3779,16 +3976,18 @@ void MainWindow::on_modList_doubleClicked(const QModelIndex &index) try { m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); sourceIdx.column(); - int tab = -1; + + auto tab = ModInfoTabIDs::None; + switch (sourceIdx.column()) { - case ModList::COL_NOTES: tab = ModInfoDialog::TAB_NOTES; break; - case ModList::COL_VERSION: tab = ModInfoDialog::TAB_NEXUS; break; - case ModList::COL_MODID: tab = ModInfoDialog::TAB_NEXUS; break; - case ModList::COL_GAME: tab = ModInfoDialog::TAB_NEXUS; break; - case ModList::COL_CATEGORY: tab = ModInfoDialog::TAB_CATEGORIES; break; - case ModList::COL_FLAGS: tab = ModInfoDialog::TAB_CONFLICTS; break; - default: tab = -1; + case ModList::COL_NOTES: tab = ModInfoTabIDs::Notes; break; + case ModList::COL_VERSION: tab = ModInfoTabIDs::Nexus; break; + case ModList::COL_MODID: tab = ModInfoTabIDs::Nexus; break; + case ModList::COL_GAME: tab = ModInfoTabIDs::Nexus; break; + case ModList::COL_CATEGORY: tab = ModInfoTabIDs::Categories; break; + case ModList::COL_FLAGS: tab = ModInfoTabIDs::Conflicts; break; } + displayModInformation(sourceIdx.row(), tab); // workaround to cancel the editor that might have opened because of // selection-click @@ -4090,7 +4289,6 @@ void MainWindow::checkModsForUpdates() QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); - statusBar()->show(); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); } else { qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); @@ -4253,64 +4451,61 @@ void MainWindow::disableVisibleMods() void MainWindow::openInstanceFolder() { QString dataPath = qApp->property("dataPath").toString(); - ::ShellExecuteW(nullptr, L"explore", ToWString(dataPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - - //opens BaseDirectory instead - //::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getBaseDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(dataPath); } void MainWindow::openLogsFolder() { QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()); - ::ShellExecuteW(nullptr, L"explore", ToWString(logsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(logsPath); } void MainWindow::openInstallFolder() { - ::ShellExecuteW(nullptr, L"explore", ToWString(qApp->applicationDirPath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(qApp->applicationDirPath()); } void MainWindow::openPluginsFolder() { QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); - ::ShellExecuteW(nullptr, L"explore", ToWString(pluginsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(pluginsPath); } void MainWindow::openProfileFolder() { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.currentProfile()->absolutePath()); } void MainWindow::openIniFolder() { if (m_OrganizerCore.currentProfile()->localSettingsEnabled()) { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.currentProfile()->absolutePath()); } else { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.managedGame()->documentsDirectory()); } } void MainWindow::openDownloadsFolder() { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getDownloadDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.settings().getDownloadDirectory()); } void MainWindow::openModsFolder() { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getModDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.settings().getModDirectory()); } void MainWindow::openGameFolder() { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.managedGame()->gameDirectory()); } void MainWindow::openMyGamesFolder() { - ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + shell::ExploreFile(m_OrganizerCore.managedGame()->documentsDirectory()); } @@ -4363,6 +4558,7 @@ void MainWindow::exportModListCSV() mod_Name->setChecked(true); QCheckBox *mod_Note = new QCheckBox(tr("Notes_column")); QCheckBox *mod_Status = new QCheckBox(tr("Mod_Status")); + mod_Status->setChecked(true); QCheckBox *primary_Category = new QCheckBox(tr("Primary_Category")); QCheckBox *nexus_ID = new QCheckBox(tr("Nexus_ID")); QCheckBox *mod_Nexus_URL = new QCheckBox(tr("Mod_Nexus_URL")); @@ -4410,10 +4606,10 @@ void MainWindow::exportModListCSV() std::vector<std::pair<QString, CSVBuilder::EFieldType> > fields; if (mod_Priority->isChecked()) fields.push_back(std::make_pair(QString("#Mod_Priority"), CSVBuilder::TYPE_STRING)); + if (mod_Status->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING)); if (mod_Name->isChecked()) fields.push_back(std::make_pair(QString("#Mod_Name"), CSVBuilder::TYPE_STRING)); - if (mod_Status->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING)); if (mod_Note->isChecked()) fields.push_back(std::make_pair(QString("#Note"), CSVBuilder::TYPE_STRING)); if (primary_Category->isChecked()) @@ -4433,9 +4629,10 @@ void MainWindow::exportModListCSV() builder.writeHeader(); - for (unsigned int i = 0; i < numMods; ++i) { - ModInfo::Ptr info = ModInfo::getByIndex(i); - bool enabled = m_OrganizerCore.currentProfile()->modEnabled(i); + auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); + for (auto& iter : indexesByPriority) { + ModInfo::Ptr info = ModInfo::getByIndex(iter.second); + bool enabled = m_OrganizerCore.currentProfile()->modEnabled(iter.second); if ((selectedRowID == 1) && !enabled) { continue; } @@ -4446,11 +4643,11 @@ void MainWindow::exportModListCSV() if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) { if (mod_Priority->isChecked()) - builder.setRowField("#Mod_Priority", QString("%1").arg(m_OrganizerCore.currentProfile()->getModPriority(i), 4, 10, QChar('0'))); + builder.setRowField("#Mod_Priority", QString("%1").arg(iter.first, 4, 10, QChar('0'))); + if (mod_Status->isChecked()) + builder.setRowField("#Mod_Status", (enabled) ? "+" : "-"); if (mod_Name->isChecked()) builder.setRowField("#Mod_Name", info->name()); - if (mod_Status->isChecked()) - builder.setRowField("#Mod_Status", (enabled)? "Enabled" : "Disabled"); if (mod_Note->isChecked()) builder.setRowField("#Note", QString("%1").arg(info->comments().remove(','))); if (primary_Category->isChecked()) @@ -4580,7 +4777,7 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) // no selection QMenu menu(this); initModListContextMenu(&menu); - menu.exec(modList->mapToGlobal(pos)); + menu.exec(modList->viewport()->mapToGlobal(pos)); } else { QMenu menu(this); @@ -4713,8 +4910,13 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) if (info->getNexusID() > 0) { menu.addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); - } else if ((info->getURL() != "")) { - menu.addAction(tr("Visit web page"), this, SLOT(visitWebPage_clicked())); + } + + const auto url = info->parseCustomURL(); + if (url.isValid()) { + menu.addAction( + tr("Visit on %1").arg(url.host()), + this, SLOT(visitWebPage_clicked())); } menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); @@ -4725,7 +4927,7 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) menu.setDefaultAction(infoAction); } - menu.exec(modList->mapToGlobal(pos)); + menu.exec(modList->viewport()->mapToGlobal(pos)); } } catch (const std::exception &e) { reportError(tr("Exception: ").arg(e.what())); @@ -4864,79 +5066,44 @@ void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos) menu.addAction(deleteMenuLabel, this, SLOT(deleteSavegame_clicked())); - menu.exec(ui->savegameList->mapToGlobal(pos)); + menu.exec(ui->savegameList->viewport()->mapToGlobal(pos)); } void MainWindow::linkToolbar() { - Executable &exe(getSelectedExecutable()); - exe.showOnToolbar(!exe.isShownOnToolbar()); - ui->linkButton->menu()->actions().at(static_cast<int>(ShortcutType::Toolbar))->setIcon(exe.isShownOnToolbar() ? QIcon(":/MO/gui/remove") : QIcon(":/MO/gui/link")); - updateToolBar(); -} + Executable& exe = getSelectedExecutable(); -namespace { -QString getLinkfile(const QString &dir, const Executable &exec) -{ - return QDir::fromNativeSeparators(dir) + "/" + exec.m_Title + ".lnk"; + exe.setShownOnToolbar(!exe.isShownOnToolbar()); + updatePinnedExecutables(); } -QString getDesktopLinkfile(const Executable &exec) +void MainWindow::linkDesktop() { - return getLinkfile(getDesktopDirectory(), exec); + env::Shortcut(getSelectedExecutable()).toggle(env::Shortcut::Desktop); } -QString getStartMenuLinkfile(const Executable &exec) +void MainWindow::linkMenu() { - return getLinkfile(getStartMenuDirectory(), exec); -} + env::Shortcut(getSelectedExecutable()).toggle(env::Shortcut::StartMenu); } -void MainWindow::addWindowsLink(const ShortcutType mapping) +void MainWindow::on_linkButton_pressed() { - const Executable &selectedExecutable(getSelectedExecutable()); - QString const linkName = getLinkfile(mapping == ShortcutType::Desktop ? getDesktopDirectory() : getStartMenuDirectory(), - selectedExecutable); + const Executable& exe = getSelectedExecutable(); - if (QFile::exists(linkName)) { - if (QFile::remove(linkName)) { - ui->linkButton->menu()->actions().at(static_cast<int>(mapping))->setIcon(QIcon(":/MO/gui/link")); - } else { - reportError(tr("failed to remove %1").arg(linkName)); - } - } else { - QFileInfo const exeInfo(qApp->applicationFilePath()); - // create link - QString executable = QDir::toNativeSeparators(selectedExecutable.m_BinaryInfo.absoluteFilePath()); + const QIcon addIcon(":/MO/gui/link"); + const QIcon removeIcon(":/MO/gui/remove"); - std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); - std::wstring parameter = ToWString( - QString("\"moshortcut://%1:%2\"").arg(InstanceManager::instance().currentInstance(),selectedExecutable.m_Title)); - std::wstring description = ToWString(QString("Run %1 with ModOrganizer").arg(selectedExecutable.m_Title)); - std::wstring iconFile = ToWString(executable); - std::wstring currentDirectory = ToWString(QDir::toNativeSeparators(qApp->applicationDirPath())); + env::Shortcut shortcut(exe); - if (CreateShortcut(targetFile.c_str() - , parameter.c_str() - , QDir::toNativeSeparators(linkName).toUtf8().constData() - , description.c_str() - , (selectedExecutable.usesOwnIcon() ? iconFile.c_str() : nullptr), 0 - , currentDirectory.c_str()) == 0) { - ui->linkButton->menu()->actions().at(static_cast<int>(mapping))->setIcon(QIcon(":/MO/gui/remove")); - } else { - reportError(tr("failed to create %1").arg(linkName)); - } - } -} + m_LinkToolbar->setIcon( + exe.isShownOnToolbar() ? removeIcon : addIcon); -void MainWindow::linkDesktop() -{ - addWindowsLink(ShortcutType::Desktop); -} + m_LinkDesktop->setIcon( + shortcut.exists(env::Shortcut::Desktop) ? removeIcon : addIcon); -void MainWindow::linkMenu() -{ - addWindowsLink(ShortcutType::StartMenu); + m_LinkStartMenu->setIcon( + shortcut.exists(env::Shortcut::StartMenu) ? removeIcon : addIcon); } void MainWindow::on_actionSettings_triggered() @@ -5018,12 +5185,13 @@ void MainWindow::on_actionSettings_triggered() activateProxy(settings.useProxy()); } - ui->apiRequests->setVisible(!settings.hideAPICounter()); - + m_statusBar->checkSettings(m_OrganizerCore.settings()); updateDownloadView(); m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist()); m_OrganizerCore.cycleDiagnostics(); + + toggleMO2EndorseState(); } @@ -5078,7 +5246,7 @@ void MainWindow::languageChange(const QString &newLanguage) ui->profileBox->setItemText(0, QObject::tr("<Manage...>")); - createHelpWidget(); + createHelpMenu(); updateDownloadView(); updateProblemsButton(); @@ -5131,91 +5299,49 @@ void MainWindow::writeDataToFile() } } - -int MainWindow::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) +void MainWindow::addAsExecutable() { - QString extension = targetInfo.suffix(); - if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || - (extension.compare("com", Qt::CaseInsensitive) == 0) || - (extension.compare("bat", Qt::CaseInsensitive) == 0)) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - return 1; - } else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { - binaryInfo = targetInfo; - return 1; - } else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { - // types that need to be injected into - std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); - QString binaryPath; - - { // try to find java automatically - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(buffer, &binaryType)) { - qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); - } else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty() && (extension == "jar")) { - // second attempt: look to the registry - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - } - if (binaryPath.isEmpty()) { - binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); - } - if (binaryPath.isEmpty()) { - return 0; - } - binaryInfo = QFileInfo(binaryPath); - if (extension == "jar") { - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } else { - arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - return 1; - } else { - return 2; + if (m_ContextItem == nullptr) { + return; } -} + using FileExecutionTypes = OrganizerCore::FileExecutionTypes; -void MainWindow::addAsExecutable() -{ - if (m_ContextItem != nullptr) { - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { + QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + FileExecutionTypes type; + + if (!OrganizerCore::getFileExecutionContext(this, targetInfo, binaryInfo, arguments, type)) { + return; + } + + switch (type) + { + case FileExecutionTypes::Executable: { QString name = QInputDialog::getText(this, tr("Enter Name"), tr("Please enter a name for the executable"), QLineEdit::Normal, targetInfo.baseName()); + if (!name.isEmpty()) { //Note: If this already exists, you'll lose custom settings - m_OrganizerCore.executablesList()->addExecutable(name, - binaryInfo.absoluteFilePath(), - arguments, - targetInfo.absolutePath(), - QString(), - Executable::CustomExecutable); + m_OrganizerCore.executablesList()->setExecutable(Executable() + .title(name) + .binaryInfo(binaryInfo) + .arguments(arguments) + .workingDirectory(targetInfo.absolutePath())); + refreshExecutablesList(); } - } break; - case 2: { + + break; + } + + case FileExecutionTypes::Other: // fall-through + default: { QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable.")); - } break; - default: { - // nop - } break; - } + break; + } } } @@ -5333,103 +5459,43 @@ void MainWindow::disableSelectedMods_clicked() void MainWindow::previewDataFile() { QString fileName = QDir::fromNativeSeparators(m_ContextItem->data(0, Qt::UserRole).toString()); + m_OrganizerCore.previewFileWithAlternatives(this, fileName); +} - // what we have is an absolute path to the file in its actual location (for the primary origin) - // what we want is the path relative to the virtual data directory - - // we need to look in the virtual directory for the file to make sure the info is up to date. - - // check if the file comes from the actual data folder instead of a mod - QDir gameDirectory = m_OrganizerCore.managedGame()->dataDirectory().absolutePath(); - QString relativePath = gameDirectory.relativeFilePath(fileName); - QDir dirRelativePath = gameDirectory.relativeFilePath(fileName); - // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case - if (!dirRelativePath.isAbsolute() && !relativePath.startsWith("..")) { - fileName = relativePath; - } - else { - // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory - int offset = m_OrganizerCore.settings().getModDirectory().size() + 1; - offset = fileName.indexOf("/", offset); - fileName = fileName.mid(offset + 1); +void MainWindow::openDataFile() +{ + if (m_ContextItem == nullptr) { + return; } + QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); + m_OrganizerCore.executeFileVirtualized(this, targetInfo); +} - - const FileEntry::Ptr file = m_OrganizerCore.directoryStructure()->searchFile(ToWString(fileName), nullptr); - - if (file.get() == nullptr) { - reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); +void MainWindow::openDataOriginExplorer_clicked() +{ + if (m_ContextItem == nullptr) { return; } - // set up preview dialog - PreviewDialog preview(fileName); - auto addFunc = [&] (int originId) { - FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId); - QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; - if (QFile::exists(filePath)) { - // it's very possible the file doesn't exist, because it's inside an archive. we don't support that - QWidget *wid = m_PluginContainer.previewGenerator().genPreview(filePath); - if (wid == nullptr) { - reportError(tr("failed to generate preview for %1").arg(filePath)); - } else { - preview.addVariant(ToQString(origin.getName()), wid); - } - } - }; + const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool(); + const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool(); - addFunc(file->getOrigin()); - for (auto alt : file->getAlternatives()) { - addFunc(alt.first); - } - if (preview.numVariants() > 0) { - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(preview.objectName()); - if (settings.contains(key)) { - preview.restoreGeometry(settings.value(key).toByteArray()); - } - preview.exec(); - settings.setValue(key, preview.saveGeometry()); - } else { - QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); + if (isArchive || isDirectory) { + return; } -} -void MainWindow::openDataFile() -{ - if (m_ContextItem != nullptr) { - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - m_OrganizerCore.spawnBinaryDirect( - binaryInfo, arguments, m_OrganizerCore.currentProfile()->name(), - targetInfo.absolutePath(), "", ""); - } break; - case 2: { - ::ShellExecuteW(nullptr, L"open", - ToWString(targetInfo.absoluteFilePath()).c_str(), - nullptr, nullptr, SW_SHOWNORMAL); - } break; - default: { - // nop - } break; - } - } -} + const auto fullPath = m_ContextItem->data(0, Qt::UserRole).toString(); + qDebug().nospace() << "opening in explorer: " << fullPath; + shell::ExploreFile(fullPath); +} void MainWindow::updateAvailable() { - for (QAction *action : ui->toolBar->actions()) { - if (action->text() == tr("Update")) { - action->setEnabled(true); - action->setToolTip(tr("Update available")); - break; - } - } + ui->actionUpdate->setEnabled(true); + ui->actionUpdate->setToolTip(tr("Update available")); + m_statusBar->setUpdateAvailable(true); } @@ -5463,8 +5529,15 @@ void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); } + const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool(); + const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool(); + + if (!isArchive && !isDirectory) { + menu.addAction("Open Origin in Explorer", this, SLOT(openDataOriginExplorer_clicked())); + } + // offer to hide/unhide file, but not for files from archives - if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) { + if (!isArchive) { if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile())); } else { @@ -5477,7 +5550,7 @@ void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile())); menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked())); - menu.exec(dataTree->mapToGlobal(pos)); + menu.exec(dataTree->viewport()->mapToGlobal(pos)); } void MainWindow::on_conflictsCheckBox_toggled(bool) @@ -5485,12 +5558,17 @@ void MainWindow::on_conflictsCheckBox_toggled(bool) refreshDataTreeKeepExpandedNodes(); } - void MainWindow::on_actionUpdate_triggered() { m_OrganizerCore.startMOUpdate(); } +void MainWindow::on_actionExit_triggered() +{ + if (confirmExit()) { + qApp->exit(); + } +} void MainWindow::actionEndorseMO() { @@ -5589,20 +5667,19 @@ void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs) void MainWindow::toggleMO2EndorseState() { - QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionEndorseMO)); if (Settings::instance().endorsementIntegration()) { ui->actionEndorseMO->setVisible(true); if (Settings::instance().directInterface().contains("endorse_state")) { - ui->actionEndorseMO->setEnabled(false); + ui->actionEndorseMO->menu()->setEnabled(false); if (Settings::instance().directInterface().value("endorse_state").toString() == "Endorsed") { ui->actionEndorseMO->setToolTip(tr("Thank you for endorsing MO2! :)")); - toolBtn->setToolTip(tr("Thank you for endorsing MO2! :)")); + ui->actionEndorseMO->setStatusTip(tr("Thank you for endorsing MO2! :)")); } else if (Settings::instance().directInterface().value("endorse_state").toString() == "Abstained") { ui->actionEndorseMO->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!")); - toolBtn->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!")); + ui->actionEndorseMO->setStatusTip(tr("Please reconsider endorsing MO2 on Nexus!")); } } else { - ui->actionEndorseMO->setEnabled(true); + ui->actionEndorseMO->menu()->setEnabled(true); } } else ui->actionEndorseMO->setVisible(false); @@ -5903,26 +5980,6 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in } -void MainWindow::updateAPICounter(int queueCount, std::tuple<int, int, int, int> limits) -{ - ui->apiRequests->setText(QString("API: Q: %1 | D: %2 | H: %3").arg(queueCount).arg(std::get<0>(limits)).arg(std::get<2>(limits))); - int requestsRemaining = std::get<0>(limits) + std::get<2>(limits); - if (requestsRemaining > 300) { - QPalette palette = ui->apiRequests->palette(); - palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen); - ui->apiRequests->setPalette(palette); - } else if (requestsRemaining < 150) { - QPalette palette = ui->apiRequests->palette(); - palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkRed); - ui->apiRequests->setPalette(palette); - } else { - QPalette palette = ui->apiRequests->palette(); - palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkYellow); - ui->apiRequests->setPalette(palette); - } -} - - BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &progress) { @@ -6055,7 +6112,7 @@ void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos) QMenu menu; menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered())); - menu.exec(ui->bsaList->mapToGlobal(pos)); + menu.exec(ui->bsaList->viewport()->mapToGlobal(pos)); } void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int) @@ -6068,16 +6125,15 @@ void MainWindow::on_actionNotifications_triggered() { updateProblemsButton(); ProblemsDialog problems(m_PluginContainer.plugins<QObject>(), this); - if (problems.hasProblems()) { - QSettings &settings = m_OrganizerCore.settings().directInterface(); - QString key = QString("geometry/%1").arg(problems.objectName()); - if (settings.contains(key)) { - problems.restoreGeometry(settings.value(key).toByteArray()); - } - problems.exec(); - settings.setValue(key, problems.saveGeometry()); - updateProblemsButton(); + + QSettings &settings = m_OrganizerCore.settings().directInterface(); + QString key = QString("geometry/%1").arg(problems.objectName()); + if (settings.contains(key)) { + problems.restoreGeometry(settings.value(key).toByteArray()); } + problems.exec(); + settings.setValue(key, problems.saveGeometry()); + updateProblemsButton(); } void MainWindow::on_actionChange_Game_triggered() @@ -6133,7 +6189,7 @@ void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters())); - menu.exec(ui->categoriesList->mapToGlobal(pos)); + menu.exec(ui->categoriesList->viewport()->mapToGlobal(pos)); } @@ -6166,28 +6222,39 @@ void MainWindow::unlockESPIndex() void MainWindow::removeFromToolbar() { - try { - Executable &exe = m_OrganizerCore.executablesList()->find(m_ContextAction->text()); - exe.showOnToolbar(false); - } catch (const std::runtime_error&) { - qDebug("executable doesn't exist any more"); + const auto& title = m_ContextAction->text(); + auto& list = *m_OrganizerCore.executablesList(); + + auto itor = list.find(title); + if (itor == list.end()) { + qWarning().nospace() + << "removeFromToolbar(): executable '" << title << "' not found"; + + return; } - updateToolBar(); + itor->setShownOnToolbar(false); + updatePinnedExecutables(); } void MainWindow::toolBar_customContextMenuRequested(const QPoint &point) { QAction *action = ui->toolBar->actionAt(point); + if (action != nullptr) { if (action->objectName().startsWith("custom_")) { m_ContextAction = action; QMenu menu; - menu.addAction(tr("Remove"), this, SLOT(removeFromToolbar())); + menu.addAction(tr("Remove '%1' from the toolbar").arg(action->text()), this, SLOT(removeFromToolbar())); menu.exec(ui->toolBar->mapToGlobal(point)); + return; } } + + // did not click a link button, show the default context menu + auto* m = createPopupMenu(); + m->exec(ui->toolBar->mapToGlobal(point)); } void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) @@ -6235,7 +6302,7 @@ void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(idx.data().toString())); //this is to avoid showing the option on game files like skyrim.esm if (modInfoIndex != UINT_MAX) { - menu.addAction(tr("Open Origin in Explorer"), this, SLOT(openOriginExplorer_clicked())); + menu.addAction(tr("Open Origin in Explorer"), this, SLOT(openPluginOriginExplorer_clicked())); ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); @@ -6246,7 +6313,7 @@ void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) } try { - menu.exec(ui->espList->mapToGlobal(pos)); + menu.exec(ui->espList->viewport()->mapToGlobal(pos)); } catch (const std::exception &e) { reportError(tr("Exception: ").arg(e.what())); } catch (...) { @@ -6291,29 +6358,14 @@ void MainWindow::on_groupCombo_currentIndexChanged(int index) const Executable &MainWindow::getSelectedExecutable() const { - QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); - return m_OrganizerCore.executablesList()->find(name); + const QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); + return m_OrganizerCore.executablesList()->get(name); } Executable &MainWindow::getSelectedExecutable() { - QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); - return m_OrganizerCore.executablesList()->find(name); -} - -void MainWindow::on_linkButton_pressed() -{ - const Executable &selectedExecutable(getSelectedExecutable()); - - const QIcon addIcon(":/MO/gui/link"); - const QIcon removeIcon(":/MO/gui/remove"); - - const QFileInfo linkDesktopFile(getDesktopLinkfile(selectedExecutable)); - const QFileInfo linkMenuFile(getStartMenuLinkfile(selectedExecutable)); - - ui->linkButton->menu()->actions().at(static_cast<int>(ShortcutType::Toolbar))->setIcon(selectedExecutable.isShownOnToolbar() ? removeIcon : addIcon); - ui->linkButton->menu()->actions().at(static_cast<int>(ShortcutType::Desktop))->setIcon(linkDesktopFile.exists() ? removeIcon : addIcon); - ui->linkButton->menu()->actions().at(static_cast<int>(ShortcutType::StartMenu))->setIcon(linkMenuFile.exists() ? removeIcon : addIcon); + const QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex()); + return m_OrganizerCore.executablesList()->get(name); } void MainWindow::on_showHiddenBox_toggled(bool checked) @@ -6540,7 +6592,7 @@ void MainWindow::on_bossButton_clicked() } if (errorMessages.length() > 0) { - QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occured"), errorMessages.c_str(), QMessageBox::Ok, this); + QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occurred"), errorMessages.c_str(), QMessageBox::Ok, this); warn->setModal(false); warn->show(); } @@ -6643,7 +6695,7 @@ void MainWindow::on_saveModsButton_clicked() m_OrganizerCore.currentProfile()->writeModlistNow(true); QDateTime now = QDateTime::currentDateTime(); if (createBackup(m_OrganizerCore.currentProfile()->getModlistFileName(), now)) { - MessageDialog::showMessage(tr("Backup of modlist created"), this); + MessageDialog::showMessage(tr("Backup of mod list created"), this); } } @@ -6822,6 +6874,17 @@ void MainWindow::dropEvent(QDropEvent *event) event->accept(); } +void MainWindow::keyReleaseEvent(QKeyEvent *event) +{ + // if the menubar is hidden, pressing Alt will make it visible + if (event->key() == Qt::Key_Alt) { + if (!ui->menuBar->isVisible()) { + showMenuBar(true); + } + } + + QMainWindow::keyReleaseEvent(event); +} void MainWindow::on_clickBlankButton_clicked() { diff --git a/src/mainwindow.h b/src/mainwindow.h index d119f49c..eee269cf 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -33,10 +33,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. //Note the commented headers here can be replaced with forward references, //when I get round to cleaning up main.cpp -struct Executable; +class Executable; class CategoryFactory; class LockedDialogBase; class OrganizerCore; +class StatusBar; #include "plugincontainer.h" //class PluginContainer; class PluginListSortProxy; namespace BSA { class Archive; } @@ -75,7 +76,6 @@ class QListWidgetItem; class QMenu; class QModelIndex; class QPoint; -class QProgressBar; class QProgressDialog; class QTranslator; class QTreeWidgetItem; @@ -151,13 +151,19 @@ public: virtual void disconnectPlugins(); - void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab); + void displayModInformation( + ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) override; + + bool confirmExit(); virtual bool closeWindow(); virtual void setWindowEnabled(bool enabled); virtual MOBase::DelayedFileWriterBase &archivesWriter() override { return m_ArchiveListWriter; } + ModInfo::Ptr nextModInList(); + ModInfo::Ptr previousModInList(); + public slots: void displayColumnSelection(const QPoint &pos); @@ -193,6 +199,7 @@ protected: virtual void resizeEvent(QResizeEvent *event); virtual void dragEnterEvent(QDragEnterEvent *event); virtual void dropEvent(QDropEvent *event); + void keyReleaseEvent(QKeyEvent *event) override; private slots: void on_actionChange_Game_triggered(); @@ -204,9 +211,17 @@ private: void cleanup(); - void actionToToolButton(QAction *&sourceAction); + void setupToolbar(); + void setupActionMenu(QAction* a); + void createHelpMenu(); + void createEndorseMenu(); + + void updatePinnedExecutables(); + void setToolbarSize(const QSize& s); + void setToolbarButtonStyle(Qt::ToolButtonStyle s); + void toolbarMenu_aboutToShow(); - void updateToolBar(); + QMenu* createPopupMenu() override; void activateSelectedProfile(); void setExecutableIndex(int index); @@ -221,7 +236,7 @@ private: QList<MOBase::IOrganizer::FileInfo> findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const; bool modifyExecutablesDialog(); - void displayModInformation(int row, int tab = -1); + void displayModInformation(int row, ModInfoTabIDs tab=ModInfoTabIDs::None); void testExtractBSA(int modIndex); void writeDataToFile(QFile &file, const QString &directory, const MOShared::DirectoryEntry &directoryEntry); @@ -253,14 +268,10 @@ private: // remove invalid category-references from mods void fixCategories(); - void createEndorseWidget(); - void createHelpWidget(); - bool extractProgress(QProgressDialog &extractProgress, int percentage, std::string fileName); size_t checkForProblems(); - int getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments); QTreeWidgetItem *addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type); void addContentFilters(); void addCategoryFilters(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID); @@ -316,16 +327,23 @@ private: Ui::MainWindow *ui; - QAction *m_Sep; // Executable Shortcuts are added after this. Non owning. - bool m_WasVisible; + // this has to be remembered because by the time storeSettings() is called, + // the window is closed and the all bars are hidden + bool m_menuBarVisible, m_statusBarVisible; + + std::unique_ptr<StatusBar> m_statusBar; + + // last separator on the toolbar, used to add spacer for right-alignment and + // as an insert point for executables + QAction* m_linksSeparator; + MOBase::TutorialControl m_Tutorial; int m_OldProfileIndex; std::vector<QString> m_ModNameList; // the mod-list to go with the directory structure - QProgressBar *m_RefreshProgress; bool m_Refreshing; QStringList m_DefaultArchives; @@ -342,6 +360,8 @@ private: QTreeWidgetItem *m_ContextItem; QAction *m_ContextAction; + QAction* m_browseModPage; + CategoryFactory &m_CategoryFactory; bool m_LoginAttempted; @@ -383,21 +403,20 @@ private: MOBase::DelayedFileWriter m_ArchiveListWriter; - enum class ShortcutType { - Toolbar, - Desktop, - StartMenu - }; + QAction* m_LinkToolbar; + QAction* m_LinkDesktop; + QAction* m_LinkStartMenu; - void addWindowsLink(ShortcutType const); + // icon set by the stylesheet, used to remember its original appearance + // when painting the count + QIcon m_originalNotificationIcon; Executable const &getSelectedExecutable() const; Executable &getSelectedExecutable(); private slots: - void updateWindowTitle(const QString &accountName, bool premium); - + void updateWindowTitle(const APIUserAccount& user); void showMessage(const QString &message); void showError(const QString &message); @@ -435,7 +454,7 @@ private slots: void visitOnNexus_clicked(); void visitWebPage_clicked(); void openExplorer_clicked(); - void openOriginExplorer_clicked(); + void openPluginOriginExplorer_clicked(); void openOriginInformation_clicked(); void information_clicked(); void enableSelectedMods_clicked(); @@ -454,6 +473,7 @@ private slots: void previewDataFile(); void hideFile(); void unhideFile(); + void openDataOriginExplorer_clicked(); // pluginlist context menu void enableSelectedPlugins_clicked(); @@ -523,14 +543,12 @@ private slots: void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); - void updateAPICounter(int queueCount, std::tuple<int, int, int, int> limits); + void onRequestsChanged(const APIStats& stats, const APIUserAccount& user); void editCategories(); void deselectFilters(); - void displayModInformation(const QString &modName, int tab); - void modOpenNext(int tab=-1); - void modOpenPrev(int tab=-1); + void displayModInformation(const QString &modName, ModInfoTabIDs tabID); void modRenamed(const QString &oldName, const QString &newName); void modRemoved(const QString &fileName); @@ -615,6 +633,7 @@ private slots: void search_activated(); void searchClear_activated(); + void resetActionIcons(); void updateModCount(); void updatePluginCount(); @@ -627,7 +646,18 @@ private slots: // ui slots void on_actionNotifications_triggered(); void on_actionSettings_triggered(); void on_actionUpdate_triggered(); + void on_actionExit_triggered(); + void on_actionMainMenuToggle_triggered(); + void on_actionToolBarMainToggle_triggered(); + void on_actionStatusBarToggle_triggered(); + void on_actionToolBarSmallIcons_triggered(); + void on_actionToolBarMediumIcons_triggered(); + void on_actionToolBarLargeIcons_triggered(); + void on_actionToolBarIconsOnly_triggered(); + void on_actionToolBarTextOnly_triggered(); + void on_actionToolBarIconsAndText_triggered(); + void on_centralWidget_customContextMenuRequested(const QPoint &pos); void on_bsaList_customContextMenuRequested(const QPoint &pos); void on_clearFiltersButton_clicked(); void on_btnRefreshData_clicked(); @@ -663,6 +693,9 @@ private slots: // ui slots void on_categoriesAndBtn_toggled(bool checked); void on_categoriesOrBtn_toggled(bool checked); void on_managedArchiveLabel_linkHovered(const QString &link); + + void showMenuBar(bool b); + void showStatusBar(bool b); }; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index d876b54a..70d1cf39 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -30,7 +30,22 @@ <verstretch>0</verstretch> </sizepolicy> </property> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> <layout class="QHBoxLayout" name="horizontalLayout_8"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="topMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> <item> <widget class="QSplitter" name="topLevelSplitter"> <property name="orientation"> @@ -206,8 +221,8 @@ <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different play throughs.</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept seperate for different profiles.</span></p></body></html></string> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different playthroughs.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept separate for different profiles.</span></p></body></html></string> </property> </widget> </item> @@ -397,7 +412,7 @@ p, li { white-space: pre-wrap; } </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,0,1,1,1"> + <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,1,1"> <item> <widget class="QPushButton" name="displayCategoriesBtn"> <property name="maximumSize"> @@ -456,37 +471,6 @@ p, li { white-space: pre-wrap; } </property> </spacer> </item> - <item> - <widget class="QLabel" name="apiRequests"> - <property name="toolTip"> - <string>Nexus API Queued and Remaining Requests</string> - </property> - <property name="whatsThis"> - <string><html><head/><body><p>This tracks the number of queued Nexus API requests on the left (<span style=" font-weight:600;">Q</span>) and the remaining daily (<span style=" font-weight:600;">D</span>) and hourly (<span style=" font-weight:600;">H</span>) requests on the right. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.</p></body></html></string> - </property> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Sunken</enum> - </property> - <property name="lineWidth"> - <number>2</number> - </property> - <property name="midLineWidth"> - <number>1</number> - </property> - <property name="text"> - <string>API: Q: 0 | D: 0 | H: 0</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - <property name="margin"> - <number>2</number> - </property> - </widget> - </item> <item alignment="Qt::AlignLeft"> <widget class="QPushButton" name="clearFiltersButton"> <property name="sizePolicy"> @@ -532,19 +516,6 @@ p, li { white-space: pre-wrap; } </widget> </item> <item> - <spacer name="horizontalSpacer_4"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> <widget class="QComboBox" name="groupCombo"> <property name="baseSize"> <size> @@ -1337,14 +1308,11 @@ p, li { white-space: pre-wrap; } </layout> </widget> <widget class="QToolBar" name="toolBar"> - <property name="enabled"> - <bool>true</bool> - </property> <property name="contextMenuPolicy"> <enum>Qt::CustomContextMenu</enum> </property> <property name="windowTitle"> - <string>Tool Bar</string> + <string>Main ToolBar</string> </property> <property name="movable"> <bool>false</bool> @@ -1355,12 +1323,6 @@ p, li { white-space: pre-wrap; } <height>36</height> </size> </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonIconOnly</enum> - </property> - <property name="floatable"> - <bool>false</bool> - </property> <attribute name="toolBarArea"> <enum>TopToolBarArea</enum> </attribute> @@ -1381,13 +1343,87 @@ p, li { white-space: pre-wrap; } <addaction name="actionHelp"/> </widget> <widget class="QStatusBar" name="statusBar"/> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>926</width> + <height>21</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>&File</string> + </property> + <addaction name="actionChange_Game"/> + <addaction name="actionInstallMod"/> + <addaction name="actionNexus"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuTools"> + <property name="title"> + <string>&Tools</string> + </property> + <addaction name="actionAdd_Profile"/> + <addaction name="actionModify_Executables"/> + <addaction name="separator"/> + <addaction name="actionTool"/> + <addaction name="separator"/> + <addaction name="actionSettings"/> + </widget> + <widget class="QMenu" name="menuHelp"> + <property name="title"> + <string>&Help</string> + </property> + <addaction name="actionHelp"/> + <addaction name="actionUpdate"/> + <addaction name="separator"/> + <addaction name="actionEndorseMO"/> + </widget> + <widget class="QMenu" name="menuView"> + <property name="title"> + <string>&View</string> + </property> + <widget class="QMenu" name="menuToolbars"> + <property name="title"> + <string>&Toolbars</string> + </property> + <addaction name="actionMainMenuToggle"/> + <addaction name="actionToolBarMainToggle"/> + <addaction name="actionStatusBarToggle"/> + <addaction name="separator"/> + <addaction name="actionToolBarSmallIcons"/> + <addaction name="actionToolBarMediumIcons"/> + <addaction name="actionToolBarLargeIcons"/> + <addaction name="separator"/> + <addaction name="actionToolBarIconsOnly"/> + <addaction name="actionToolBarTextOnly"/> + <addaction name="actionToolBarIconsAndText"/> + </widget> + <addaction name="menuToolbars"/> + <addaction name="separator"/> + <addaction name="actionNotifications"/> + </widget> + <widget class="QMenu" name="menuRun"> + <property name="title"> + <string>&Run</string> + </property> + </widget> + <addaction name="menuFile"/> + <addaction name="menuView"/> + <addaction name="menuTools"/> + <addaction name="menuRun"/> + <addaction name="menuHelp"/> + </widget> <action name="actionInstallMod"> <property name="icon"> <iconset resource="resources.qrc"> <normaloff>:/MO/gui/resources/system-installer.png</normaloff>:/MO/gui/resources/system-installer.png</iconset> </property> <property name="text"> - <string>Install Mod</string> + <string>Install &Mod...</string> </property> <property name="iconText"> <string>Install &Mod</string> @@ -1395,6 +1431,9 @@ p, li { white-space: pre-wrap; } <property name="toolTip"> <string>Install a new mod from an archive</string> </property> + <property name="statusTip"> + <string>Install a new mod from an archive</string> + </property> <property name="shortcut"> <string>Ctrl+M</string> </property> @@ -1405,13 +1444,16 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/profiles</normaloff>:/MO/gui/profiles</iconset> </property> <property name="text"> - <string>Profiles</string> + <string>&Profiles...</string> </property> <property name="iconText"> <string>&Profiles</string> </property> <property name="toolTip"> - <string>Configure Profiles</string> + <string>Configure profiles</string> + </property> + <property name="statusTip"> + <string>Configure profiles</string> </property> <property name="shortcut"> <string>Ctrl+P</string> @@ -1423,7 +1465,7 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/icon_executable</normaloff>:/MO/gui/icon_executable</iconset> </property> <property name="text"> - <string>Executables</string> + <string>&Executables...</string> </property> <property name="iconText"> <string>&Executables</string> @@ -1431,6 +1473,9 @@ p, li { white-space: pre-wrap; } <property name="toolTip"> <string>Configure the executables that can be started through Mod Organizer</string> </property> + <property name="statusTip"> + <string>Configure the executables that can be started through Mod Organizer</string> + </property> <property name="shortcut"> <string>Ctrl+E</string> </property> @@ -1441,7 +1486,7 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/plugins</normaloff>:/MO/gui/plugins</iconset> </property> <property name="text"> - <string>Tools</string> + <string>&Tool Plugins</string> </property> <property name="iconText"> <string>&Tools</string> @@ -1459,7 +1504,7 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset> </property> <property name="text"> - <string>Settings</string> + <string>&Settings...</string> </property> <property name="iconText"> <string>&Settings</string> @@ -1467,6 +1512,9 @@ p, li { white-space: pre-wrap; } <property name="toolTip"> <string>Configure settings and workarounds</string> </property> + <property name="statusTip"> + <string>Configure settings and workarounds</string> + </property> <property name="shortcut"> <string>Ctrl+S</string> </property> @@ -1477,10 +1525,16 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/resources/internet-web-browser.png</normaloff>:/MO/gui/resources/internet-web-browser.png</iconset> </property> <property name="text"> - <string>Nexus</string> + <string>Visit &Nexus</string> + </property> + <property name="iconText"> + <string>Visit &Nexus</string> </property> <property name="toolTip"> - <string>Search nexus network for more mods</string> + <string>Visit the Nexus website in your browser for more mods</string> + </property> + <property name="statusTip"> + <string>Visit the Nexus website in your browser for more mods</string> </property> <property name="shortcut"> <string>Ctrl+N</string> @@ -1495,25 +1549,34 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/update</normaloff>:/MO/gui/update</iconset> </property> <property name="text"> - <string>Update</string> + <string>&Update Mod Organizer</string> + </property> + <property name="iconText"> + <string>&Update Mod Organizer</string> </property> <property name="toolTip"> <string>Mod Organizer is up-to-date</string> </property> + <property name="statusTip"> + <string>Mod Organizer is up-to-date</string> + </property> </action> <action name="actionNotifications"> - <property name="enabled"> - <bool>false</bool> - </property> <property name="icon"> <iconset resource="resources.qrc"> <normaloff>:/MO/gui/warning</normaloff>:/MO/gui/warning</iconset> </property> <property name="text"> - <string>No Notifications</string> + <string>&Notifications...</string> + </property> + <property name="toolTip"> + <string>Open the notifications dialog</string> + </property> + <property name="statusTip"> + <string>Open the notifications dialog</string> </property> <property name="whatsThis"> - <string>This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them.</string> + <string>This button will be highlighted on the toolbar if MO discovered potential problems in your setup and provide tips on how to fix them.</string> </property> </action> <action name="actionHelp"> @@ -1522,10 +1585,16 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/help</normaloff>:/MO/gui/help</iconset> </property> <property name="text"> - <string>Help</string> + <string>&Help</string> + </property> + <property name="iconText"> + <string>&Help</string> </property> <property name="toolTip"> - <string>Help</string> + <string>Show help options</string> + </property> + <property name="statusTip"> + <string>Show help options</string> </property> <property name="shortcut"> <string>Ctrl+H</string> @@ -1537,15 +1606,30 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/icon_favorite</normaloff>:/MO/gui/icon_favorite</iconset> </property> <property name="text"> - <string>Endorse MO</string> + <string>&Endorse ModOrganizer</string> + </property> + <property name="iconText"> + <string>&Endorse ModOrganizer</string> </property> <property name="toolTip"> <string>Endorse Mod Organizer</string> </property> + <property name="statusTip"> + <string>Endorse Mod Organizer</string> + </property> </action> <action name="actionCopy_Log_to_Clipboard"> <property name="text"> - <string>Copy Log to Clipboard</string> + <string>Copy &Log</string> + </property> + <property name="iconText"> + <string>Copy &Log</string> + </property> + <property name="toolTip"> + <string>Copy log to clipboard</string> + </property> + <property name="statusTip"> + <string>Copy log to clipboard</string> </property> </action> <action name="actionChange_Game"> @@ -1554,11 +1638,103 @@ p, li { white-space: pre-wrap; } <normaloff>:/MO/gui/instance_switch</normaloff>:/MO/gui/instance_switch</iconset> </property> <property name="text"> - <string>Change Game</string> + <string>&Change Game...</string> + </property> + <property name="iconText"> + <string>&Change Game</string> </property> <property name="toolTip"> <string>Open the Instance selection dialog to manage a different Game</string> </property> + <property name="statusTip"> + <string>Open the Instance selection dialog to manage a different Game</string> + </property> + </action> + <action name="actionExit"> + <property name="text"> + <string>E&xit</string> + </property> + <property name="iconText"> + <string>E&xit</string> + </property> + <property name="toolTip"> + <string>Exits Mod Organizer</string> + </property> + <property name="statusTip"> + <string>Exits Mod Organizer</string> + </property> + </action> + <action name="actionToolBarMainToggle"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>M&ain Toolbar</string> + </property> + </action> + <action name="actionToolBarSmallIcons"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>&Small Icons</string> + </property> + </action> + <action name="actionToolBarLargeIcons"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Lar&ge Icons</string> + </property> + </action> + <action name="actionToolBarIconsOnly"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>&Icons Only</string> + </property> + </action> + <action name="actionToolBarTextOnly"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>&Text Only</string> + </property> + </action> + <action name="actionToolBarIconsAndText"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>I&cons and Text</string> + </property> + </action> + <action name="actionToolBarMediumIcons"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>M&edium Icons</string> + </property> + </action> + <action name="actionMainMenuToggle"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>&Menu</string> + </property> + </action> + <action name="actionStatusBarToggle"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Status &bar</string> + </property> </action> </widget> <layoutdefault spacing="6" margin="11"/> diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 3a791827..5652833a 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -116,12 +116,12 @@ bool MOApplication::notify(QObject *receiver, QEvent *event) } catch (const std::exception &e) {
qCritical("uncaught exception in handler (object %s, eventtype %d): %s",
receiver->objectName().toUtf8().constData(), event->type(), e.what());
- reportError(tr("an error occured: %1").arg(e.what()));
+ reportError(tr("an error occurred: %1").arg(e.what()));
return false;
} catch (...) {
qCritical("uncaught non-std exception in handler (object %s, eventtype %d)",
receiver->objectName().toUtf8().constData(), event->type());
- reportError(tr("an error occured"));
+ reportError(tr("an error occurred"));
return false;
}
}
diff --git a/src/modinfo.cpp b/src/modinfo.cpp index c1974152..3484b644 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -310,23 +310,27 @@ bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *recei } } - if (latest < QDateTime::currentDateTimeUtc().addDays(-30)) { + if (latest < QDateTime::currentDateTimeUtc().addMonths(-1)) { std::set<std::pair<QString, int>> organizedGames; for (auto mod : s_Collection) { - if (mod->canBeUpdated()) { + if (mod->canBeUpdated() && mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addMonths(-1)) { organizedGames.insert(std::make_pair<QString, int>(mod->getGameName().toLower(), mod->getNexusID())); } } if (organizedGames.empty()) { - qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); + qWarning() << tr("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); updatesAvailable = false; + } else { + qInfo() << tr( + "You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. " + "This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods." + ); } - for (auto game : organizedGames) { + for (auto game : organizedGames) NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); - } - } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-30)) { + } else if (earliest < QDateTime::currentDateTimeUtc().addMonths(-1)) { for (auto gameName : games) NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) { @@ -358,7 +362,7 @@ std::set<QSharedPointer<ModInfo>> ModInfo::filteredMods(QString gameName, QVaria if (addOldMods) for (auto mod : s_Collection) - if (mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addDays(-30) && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + if (mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addMonths(-1) && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) finalMods.insert(mod); if (markUpdated) { @@ -516,3 +520,22 @@ void ModInfo::testValid() dirIter.next(); } } + +QUrl ModInfo::parseCustomURL() const +{ + if (!hasCustomURL() || getCustomURL().isEmpty()) { + return {}; + } + + const auto url = QUrl::fromUserInput(getCustomURL()); + + if (!url.isValid()) { + qCritical() + << "mod '" << name() << "' has an invalid custom url " + << "'" << getCustomURL() << "'"; + + return {}; + } + + return url; +} diff --git a/src/modinfo.h b/src/modinfo.h index f1d816fe..e395f45b 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -733,14 +733,31 @@ public: virtual void doConflictCheck() const {} /** - * @brief set the URL for a mod - */ - virtual void setURL(QString const &) {} + * @brief sets whether this mod uses a custom url + **/ + virtual void setHasCustomURL(bool) {} /** - * @returns the URL for a mod - */ - virtual QString getURL() const { return ""; } + * @brief returns whether this mod uses a custom url + **/ + virtual bool hasCustomURL() const { return false; } + + /** + * @brief sets the custom url + **/ + virtual void setCustomURL(QString const &) {} + + /** + * @brief returns the custom url + **/ + virtual QString getCustomURL() const { return ""; } + + /** + * If hasCustomURL() is true and getCustomURL() is not empty, tries to parse + * the url using QUrl::fromUserInput() and returns it. Otherwise, returns an + * empty QUrl. + **/ + QUrl parseCustomURL() const; signals: diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index 0e41b10e..47ac84be 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -19,1493 +19,763 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "modinfodialog.h" #include "ui_modinfodialog.h" -#include "descriptionpage.h" -#include "mainwindow.h" - -#include "modidlineedit.h" -#include "iplugingame.h" -#include "nexusinterface.h" -#include "report.h" -#include "utility.h" -#include "messagedialog.h" -#include "bbcode.h" -#include "questionboxmemory.h" -#include "settings.h" -#include "categories.h" +#include "plugincontainer.h" #include "organizercore.h" -#include "pluginlistsortproxy.h" -#include "previewgenerator.h" -#include "previewdialog.h" - -#include <QDir> -#include <QDirIterator> -#include <QPushButton> -#include <QInputDialog> -#include <QMessageBox> -#include <QMenu> -#include <QFileSystemModel> -#include <QInputDialog> -#include <QPointer> -#include <QFileDialog> -#include <QShortcut> - -#include <Shlwapi.h> - -#include <sstream> - +#include "mainwindow.h" +#include "modinfodialogtextfiles.h" +#include "modinfodialogimages.h" +#include "modinfodialogesps.h" +#include "modinfodialogconflicts.h" +#include "modinfodialogcategories.h" +#include "modinfodialognexus.h" +#include "modinfodialogfiletree.h" +#include <filesystem> using namespace MOBase; using namespace MOShared; +namespace fs = std::filesystem; - -class ModFileListWidget : public QListWidgetItem { - friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); -public: - ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) - : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} -private: - int m_SortValue; -}; +const int max_scan_for_context_menu = 50; -static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) +int naturalCompare(const QString& a, const QString& b) { - return LHS.m_SortValue < RHS.m_SortValue; -} + static QCollator c = []{ + QCollator c; + c.setNumericMode(true); + c.setCaseSensitivity(Qt::CaseInsensitive); + return c; + }(); + return c.compare(a, b); +} -ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent) - : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), - m_ThumbnailMapper(this), m_RequestStarted(false), - m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr), - m_Directory(directory), m_Origin(nullptr), - m_OrganizerCore(organizerCore), m_PluginContainer(pluginContainer) +bool canPreviewFile( + PluginContainer& pluginContainer, bool isArchive, const QString& filename) { - ui->setupUi(this); - this->setWindowTitle(modInfo->name()); - this->setWindowModality(Qt::WindowModal); - - m_RootPath = modInfo->absolutePath(); - - QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); - m_Settings = new QSettings(metaFileName, QSettings::IniFormat); - - QLineEdit *modIDEdit = findChild<QLineEdit*>("modIDEdit"); - ui->modIDEdit->setValidator(new QIntValidator(modIDEdit)); - ui->modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); - - connect(ui->modIDEdit, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); - - QString gameName = modInfo->getGameName(); - ui->sourceGameEdit->addItem(organizerCore->managedGame()->gameName(), organizerCore->managedGame()->gameShortName()); - if (organizerCore->managedGame()->validShortNames().size() == 0) { - ui->sourceGameEdit->setDisabled(true); - } else { - for (auto game : pluginContainer->plugins<IPluginGame>()) { - for (QString gameName : organizerCore->managedGame()->validShortNames()) { - if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - ui->sourceGameEdit->addItem(game->gameName(), game->gameShortName()); - break; - } - } - } - } - ui->sourceGameEdit->setCurrentIndex(ui->sourceGameEdit->findData(gameName)); - - ui->commentsEdit->setText(modInfo->comments()); - ui->notesEdit->setText(modInfo->notes()); - - ui->descriptionView->setPage(new DescriptionPage()); - - connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); - connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); - connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); - connect(ui->descriptionView->page(), SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); - //TODO: No easy way to delegate links - //ui->descriptionView->page()->acceptNavigationRequest(QWebEnginePage::DelegateAllLinks); - - new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated())); - - if (directory->originExists(ToWString(modInfo->name()))) { - m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); - if (m_Origin->isDisabled()) { - m_Origin = nullptr; - } - } - - refreshLists(); - - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) - { - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); - ui->tabWidget->setTabEnabled(TAB_INIFILES, false); - ui->tabWidget->setTabEnabled(TAB_IMAGES, false); - ui->tabWidget->setTabEnabled(TAB_ESPS, false); - ui->tabWidget->setTabEnabled(TAB_CONFLICTS, false); - //ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); - addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - ui->tabWidget->setTabEnabled(TAB_NEXUS, false); - //ui->tabWidget->setTabEnabled(TAB_NOTES, false); - ui->tabWidget->setTabEnabled(TAB_FILETREE, false); - } - else if (unmanaged) - { - ui->tabWidget->setTabEnabled(TAB_INIFILES, false); - ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); - ui->tabWidget->setTabEnabled(TAB_NEXUS, false); - ui->tabWidget->setTabEnabled(TAB_FILETREE, false); - ui->tabWidget->setTabEnabled(TAB_NOTES, false); - ui->tabWidget->setTabEnabled(TAB_ESPS, false); - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); - ui->tabWidget->setTabEnabled(TAB_IMAGES, false); - } else { - initFiletree(modInfo); - addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, ui->textFileList->count() != 0); - ui->tabWidget->setTabEnabled(TAB_IMAGES, ui->thumbnailArea->count() != 0); - ui->tabWidget->setTabEnabled(TAB_ESPS, (ui->inactiveESPList->count() != 0) || (ui->activeESPList->count() != 0)); - } - initINITweaks(); - - ui->tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != nullptr); - - - ui->endorseBtn->setVisible(Settings::instance().endorsementIntegration()); - ui->endorseBtn->setEnabled((m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE) || - (m_ModInfo->endorsedState() == ModInfo::ENDORSED_NEVER)); - - // activate first enabled tab - for (int i = 0; i < ui->tabWidget->count(); ++i) { - if (ui->tabWidget->isTabEnabled(i)) { - ui->tabWidget->setCurrentIndex(i); - break; - } + if (isArchive) { + return false; } - if (ui->tabWidget->currentIndex() == TAB_NEXUS) { - activateNexusTab(); - } + const auto ext = QFileInfo(filename).suffix(); + return pluginContainer.previewGenerator().previewSupported(ext); } - -ModInfoDialog::~ModInfoDialog() +bool canOpenFile(bool isArchive, const QString&) { - m_ModInfo->setComments(ui->commentsEdit->text()); - //Avoid saving html stump if notes field is empty. - if (ui->notesEdit->toPlainText().isEmpty()) - m_ModInfo->setNotes(ui->notesEdit->toPlainText()); - else - m_ModInfo->setNotes(ui->notesEdit->toHtml()); - saveCategories(ui->categoriesTree->invisibleRootItem()); - saveIniTweaks(); // ini tweaks are written to the ini file directly. This is the only information not managed by ModInfo - delete ui->descriptionView->page(); - delete ui->descriptionView; - delete ui; - delete m_Settings; + // can open anything as long as it's not in an archive + return !isArchive; } - -void ModInfoDialog::initINITweaks() +bool canExploreFile(bool isArchive, const QString&) { - int numTweaks = m_Settings->beginReadArray("INI Tweaks"); - for (int i = 0; i < numTweaks; ++i) { - m_Settings->setArrayIndex(i); - QList<QListWidgetItem*> items = ui->iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); - if (items.size() != 0) { - items.at(0)->setCheckState(Qt::Checked); - } - } - m_Settings->endArray(); + // can explore anything as long as it's not in an archive + return !isArchive; } -void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo) +bool canHideFile(bool isArchive, const QString& filename) { - ui->fileTree = findChild<QTreeView*>("fileTree"); + if (isArchive) { + // can't hide files from archives + return false; + } - m_FileSystemModel = new QFileSystemModel(this); - m_FileSystemModel->setReadOnly(false); - m_FileSystemModel->setRootPath(m_RootPath); - ui->fileTree->setModel(m_FileSystemModel); - ui->fileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); - ui->fileTree->setColumnWidth(0, 300); + if (filename.endsWith(ModInfo::s_HiddenExt)) { + // already hidden + return false; + } - m_DeleteAction = new QAction(tr("&Delete"), ui->fileTree); - m_RenameAction = new QAction(tr("&Rename"), ui->fileTree); - m_HideAction = new QAction(tr("&Hide"), ui->fileTree); - m_UnhideAction = new QAction(tr("&Unhide"), ui->fileTree); - m_OpenAction = new QAction(tr("&Open"), ui->fileTree); - m_NewFolderAction = new QAction(tr("&New Folder"), ui->fileTree); - QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); - QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); - QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); - QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); - QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); - connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); + return true; } - -int ModInfoDialog::tabIndex(const QString &tabId) +bool canUnhideFile(bool isArchive, const QString& filename) { - for (int i = 0; i < ui->tabWidget->count(); ++i) { - if (ui->tabWidget->widget(i)->objectName() == tabId) { - return i; - } + if (isArchive) { + // can't unhide files from archives + return false; } - return -1; -} - - -void ModInfoDialog::restoreTabState(const QByteArray &state) -{ - QDataStream stream(state); - int count = 0; - stream >> count; - - QStringList tabIds; - // first, only determine the new mapping - for (int newPos = 0; newPos < count; ++newPos) { - QString tabId; - stream >> tabId; - tabIds.append(tabId); - int oldPos = tabIndex(tabId); - if (oldPos != -1) { - m_RealTabPos[newPos] = oldPos; - } else { - m_RealTabPos[newPos] = newPos; - } - } - // then actually move the tabs - QTabBar *tabBar = ui->tabWidget->findChild<QTabBar*>("qt_tabwidget_tabbar"); // magic name = bad - ui->tabWidget->blockSignals(true); - for (int newPos = 0; newPos < count; ++newPos) { - QString tabId = tabIds.at(newPos); - int oldPos = tabIndex(tabId); - tabBar->moveTab(oldPos, newPos); + if (!filename.endsWith(ModInfo::s_HiddenExt)) { + // already visible + return false; } - ui->tabWidget->blockSignals(false); -} + return true; +} -QByteArray ModInfoDialog::saveTabState() const +FileRenamer::RenameResults hideFile(FileRenamer& renamer, const QString &oldName) { - QByteArray result; - QDataStream stream(&result, QIODevice::WriteOnly); - stream << ui->tabWidget->count(); - for (int i = 0; i < ui->tabWidget->count(); ++i) { - stream << ui->tabWidget->widget(i)->objectName(); - } - - return result; + const QString newName = oldName + ModInfo::s_HiddenExt; + return renamer.rename(oldName, newName); } - -void ModInfoDialog::refreshLists() +FileRenamer::RenameResults unhideFile(FileRenamer& renamer, const QString &oldName) { - int numNonConflicting = 0; - int numOverwrite = 0; - int numOverwritten = 0; - - ui->overwriteTree->clear(); - ui->overwrittenTree->clear(); - - if (m_Origin != nullptr) { - std::vector<FileEntry::Ptr> files = m_Origin->getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); - QString fileName = relativeName.mid(0).prepend(m_RootPath); - bool archive; - if ((*iter)->getOrigin(archive) == m_Origin->getID()) { - std::vector<std::pair<int, std::pair<std::wstring, int>>> alternatives = (*iter)->getAlternatives(); - if (!alternatives.empty()) { - std::wostringstream altString; - for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator altIter = alternatives.begin(); - altIter != alternatives.end(); ++altIter) { - if (altIter != alternatives.begin()) { - altString << ", "; - } - altString << m_Directory->getOriginByID(altIter->first).getName(); - } - QStringList fields(relativeName.prepend("...")); - fields.append(ToQString(altString.str())); - - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives.back().first).getName())); - item->setData(1, Qt::UserRole + 1, alternatives.back().first); - item->setData(1, Qt::UserRole + 2, archive); - if (archive) { - QFont font = item->font(0); - font.setItalic(true); - item->setFont(0, font); - item->setFont(1, font); - } - ui->overwriteTree->addTopLevelItem(item); - ++numOverwrite; - } else {// otherwise don't display the file - ++numNonConflicting; - } - } else { - FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(archive)); - QStringList fields(relativeName); - fields.append(ToQString(realOrigin.getName())); - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); - item->setData(1, Qt::UserRole + 2, archive); - if (archive) { - QFont font = item->font(0); - font.setItalic(true); - item->setFont(0, font); - item->setFont(1, font); - } - ui->overwrittenTree->addTopLevelItem(item); - ++numOverwritten; - } - } - } - - if (m_RootPath.length() > 0) { - QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); - while (dirIterator.hasNext()) { - QString fileName = dirIterator.next(); - - if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { - ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); - } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && - !fileName.endsWith("meta.ini")) { - QString namePart = fileName.mid(m_RootPath.length() + 1); - if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { - QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); - newItem->setData(Qt::UserRole, namePart); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newItem); - } else { - ui->iniFileList->addItem(namePart); - } - } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || - fileName.endsWith(".esm", Qt::CaseInsensitive) || - fileName.endsWith(".esl", Qt::CaseInsensitive)) { - QString relativePath = fileName.mid(m_RootPath.length() + 1); - if (relativePath.contains('/')) { - QFileInfo fileInfo(fileName); - QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); - newItem->setData(Qt::UserRole, relativePath); - ui->inactiveESPList->addItem(newItem); - } else { - ui->activeESPList->addItem(relativePath); - } - } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || - (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { - QImage image = QImage(fileName); - if (!image.isNull()) { - if (static_cast<float>(image.width()) / static_cast<float>(image.height()) > 1.34) { - image = image.scaledToWidth(128); - } else { - image = image.scaledToHeight(96); - } - - QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); - thumbnailButton->setIconSize(QSize(image.width(), image.height())); - connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); - m_ThumbnailMapper.setMapping(thumbnailButton, fileName); - ui->thumbnailArea->addWidget(thumbnailButton); - } - } - } - } - - ui->overwriteCount->display(numOverwrite); - ui->overwrittenCount->display(numOverwritten); - ui->noConflictCount->display(numNonConflicting); + QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); + return renamer.rename(oldName, newName); } -void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set<int> &enabledCategories, QTreeWidgetItem *root, int rootLevel) +ModInfoDialog::TabInfo::TabInfo(std::unique_ptr<ModInfoDialogTab> tab) + : tab(std::move(tab)), realPos(-1), widget(nullptr) { - for (int i = 0; i < static_cast<int>(factory.numCategories()); ++i) { - if (factory.getParentID(i) != rootLevel) { - continue; - } - int categoryID = factory.getCategoryID(i); - QTreeWidgetItem *newItem - = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(0, enabledCategories.find(categoryID) - != enabledCategories.end() - ? Qt::Checked - : Qt::Unchecked); - newItem->setData(0, Qt::UserRole, categoryID); - if (factory.hasChildren(i)) { - addCategories(factory, enabledCategories, newItem, categoryID); - } - root->addChild(newItem); - } } - -void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) +bool ModInfoDialog::TabInfo::isVisible() const { - for (int i = 0; i < currentNode->childCount(); ++i) { - QTreeWidgetItem *childNode = currentNode->child(i); - m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); - saveCategories(childNode); - } + return (realPos != -1); } -void ModInfoDialog::on_closeButton_clicked() +ModInfoDialog::ModInfoDialog( + MainWindow* mw, OrganizerCore* core, PluginContainer* plugin, + ModInfo::Ptr mod) : + TutorableDialog("ModInfoDialog", mw), + ui(new Ui::ModInfoDialog), m_mainWindow(mw), + m_core(core), m_plugin(plugin), m_initialTab(ModInfoTabIDs::None), + m_arrangingTabs(false) { - if (allowNavigateFromTXT() && allowNavigateFromINI()) { - this->close(); - } -} + ui->setupUi(this); + auto* sc = new QShortcut(QKeySequence::Delete, this); + connect(sc, &QShortcut::activated, [&]{ onDeleteShortcut(); }); + setMod(mod); + createTabs(); -QString ModInfoDialog::getModVersion() const -{ - return m_Settings->value("version", "").toString(); + connect(ui->tabWidget, &QTabWidget::currentChanged, [&]{ onTabSelectionChanged(); }); + connect(ui->tabWidget->tabBar(), &QTabBar::tabMoved, [&]{ onTabMoved(); }); + connect(ui->close, &QPushButton::clicked, [&]{ onCloseButton(); }); + connect(ui->previousMod, &QPushButton::clicked, [&]{ onPreviousMod(); }); + connect(ui->nextMod, &QPushButton::clicked, [&]{ onNextMod(); }); } +ModInfoDialog::~ModInfoDialog() = default; -const int ModInfoDialog::getModID() const +template <class T> +std::unique_ptr<ModInfoDialogTab> createTab(ModInfoDialog& d, ModInfoTabIDs id) { - return m_Settings->value("modid", 0).toInt(); + return std::make_unique<T>(ModInfoDialogTabContext( + *d.m_core, *d.m_plugin, &d, d.ui.get(), id, d.m_mod, d.getOrigin())); } -void ModInfoDialog::openTab(int tab) +void ModInfoDialog::createTabs() { - QTabWidget *tabWidget = findChild<QTabWidget*>("tabWidget"); - if (tabWidget->isTabEnabled(tab)) { - tabWidget->setCurrentIndex(tab); - } -} + m_tabs.clear(); -void ModInfoDialog::thumbnailClicked(const QString &fileName) -{ - QLabel *imageLabel = findChild<QLabel*>("imageLabel"); - imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - QImage image(fileName); - if (static_cast<float>(image.width()) / static_cast<float>(image.height()) > 1.34) { - image = image.scaledToWidth(imageLabel->geometry().width()); - } else { - image = image.scaledToHeight(imageLabel->geometry().height()); - } - imageLabel->setPixmap(QPixmap::fromImage(image)); -} + m_tabs.push_back(createTab<TextFilesTab>(*this, ModInfoTabIDs::TextFiles)); + m_tabs.push_back(createTab<IniFilesTab>(*this, ModInfoTabIDs::IniFiles)); + m_tabs.push_back(createTab<ImagesTab>(*this, ModInfoTabIDs::Images)); + m_tabs.push_back(createTab<ESPsTab>(*this, ModInfoTabIDs::Esps)); + m_tabs.push_back(createTab<ConflictsTab>(*this, ModInfoTabIDs::Conflicts)); + m_tabs.push_back(createTab<CategoriesTab>(*this, ModInfoTabIDs::Categories)); + m_tabs.push_back(createTab<NexusTab>(*this, ModInfoTabIDs::Nexus)); + m_tabs.push_back(createTab<NotesTab>(*this, ModInfoTabIDs::Notes)); + m_tabs.push_back(createTab<FileTreeTab>(*this, ModInfoTabIDs::Filetree)); -bool ModInfoDialog::allowNavigateFromTXT() -{ - if (ui->saveTXTButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentTextFile(); - } + // check for tabs in the ui not having a corresponding tab in the list + int count = ui->tabWidget->count(); + if (count < 0 || count > static_cast<int>(m_tabs.size())) { + qCritical() << "mod info dialog has more tabs than expected"; + count = static_cast<int>(m_tabs.size()); } - return true; -} + // for each tab in the widget; connects the widgets with the tab objects + // + for (int i=0; i<count; ++i) { + auto& tabInfo = m_tabs[static_cast<std::size_t>(i)]; -bool ModInfoDialog::allowNavigateFromINI() -{ - if (ui->saveButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentIniFile(); - } - } - return true; -} + // remembering tab information so tabs can be removed and re-added + tabInfo.widget = ui->tabWidget->widget(i); + tabInfo.caption = ui->tabWidget->tabText(i); + tabInfo.icon = ui->tabWidget->tabIcon(i); + connect( + tabInfo.tab.get(), &ModInfoDialogTab::originModified, + [this](int originID){ onOriginModified(originID); }); -void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); + connect( + tabInfo.tab.get(), &ModInfoDialogTab::modOpen, + [&](const QString& name){ setMod(name); update(); }); - QVariant currentFile = ui->textFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } + connect( + tabInfo.tab.get(), &ModInfoDialogTab::hasDataChanged, + [&]{ setTabsColors(); }); - if (allowNavigateFromTXT()) { - openTextFile(fullPath); - } else { - ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); + connect( + tabInfo.tab.get(), &ModInfoDialogTab::wantsFocus, + [&, id=tabInfo.tab->tabID()]{ switchToTab(id); }); } } - -void ModInfoDialog::openTextFile(const QString &fileName) +int ModInfoDialog::exec() { - QString encoding; - ui->textFileView->setText(MOBase::readFileText(fileName, &encoding)); - ui->textFileView->setProperty("currentFile", fileName); - ui->textFileView->setProperty("encoding", encoding); - ui->saveTXTButton->setEnabled(false); -} - + // whether to select the first tab; if the main window requested a specific + // tab, it is selected when encountered in update() + const auto selectFirst = (m_initialTab == ModInfoTabIDs::None); -void ModInfoDialog::openIniFile(const QString &fileName) -{ - QFile iniFile(fileName); - iniFile.open(QIODevice::ReadOnly); - QByteArray buffer = iniFile.readAll(); + update(true); - QTextCodec *codec = QTextCodec::codecForUtfText(buffer, QTextCodec::codecForName("utf-8")); - QTextEdit *iniFileView = findChild<QTextEdit*>("iniFileView"); - iniFileView->setText(codec->toUnicode(buffer)); - iniFileView->setProperty("currentFile", fileName); - iniFileView->setProperty("encoding", codec->name()); - iniFile.close(); + if (selectFirst && ui->tabWidget->count() > 0) { + ui->tabWidget->setCurrentIndex(0); + } - ui->saveButton->setEnabled(false); + return TutorableDialog::exec(); } - -void ModInfoDialog::saveIniTweaks() +void ModInfoDialog::setMod(ModInfo::Ptr mod) { - m_Settings->remove("INI Tweaks"); - m_Settings->beginWriteArray("INI Tweaks"); + Q_ASSERT(mod); + m_mod = mod; - int countEnabled = 0; - for (int i = 0; i < ui->iniTweaksList->count(); ++i) { - if (ui->iniTweaksList->item(i)->checkState() == Qt::Checked) { - m_Settings->setArrayIndex(countEnabled++); - m_Settings->setValue("name", ui->iniTweaksList->item(i)->text()); - } + // resetting the first activation flag so selecting tabs will trigger it + // again + for (auto& tabInfo : m_tabs) { + tabInfo.tab->resetFirstActivation(); } - m_Settings->endArray(); } - -void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +void ModInfoDialog::setMod(const QString& name) { - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation + unsigned int index = ModInfo::getIndex(name); + if (index == UINT_MAX) { + qCritical() << "failed to resolve mod name " << name; return; } - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + auto mod = ModInfo::getByIndex(index); + if (!mod) { + qCritical() << "mod by index " << index << " is null"; + return; } -} + setMod(mod); +} -void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +void ModInfoDialog::selectTab(ModInfoTabIDs id) { - QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation + if (!isVisible()) { + // can't select a tab if the dialog hasn't been properly updated yet + m_initialTab = id; return; } - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } - + switchToTab(id); } - -void ModInfoDialog::on_saveButton_clicked() +ModInfoDialog::TabInfo* ModInfoDialog::currentTab() { - saveCurrentIniFile(); -} + const auto index = ui->tabWidget->currentIndex(); + if (index < 0) { + return nullptr; + } + + // looking for the actual tab at that position + for (auto& tabInfo : m_tabs) { + if (tabInfo.realPos == index) { + return &tabInfo; + } + } + return nullptr; +} -void ModInfoDialog::on_saveTXTButton_clicked() +void ModInfoDialog::update(bool firstTime) { - saveCurrentTextFile(); -} + // remembering the current selection, will be restored if the tab still + // exists + const int oldTab = ui->tabWidget->currentIndex(); + setWindowTitle(m_mod->name()); -void ModInfoDialog::saveCurrentTextFile() -{ - QVariant fileNameVar = ui->textFileView->property("currentFile"); - QVariant encodingVar = ui->textFileView->property("encoding"); - if (fileNameVar.isValid() && encodingVar.isValid()) { - QString fileName = fileNameVar.toString(); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveTXTButton->setEnabled(false); -} + // rebuilding the tab widget if needed depending on what tabs are valid for + // the current mod + setTabsVisibility(firstTime); + // updating the data in all tabs + updateTabs(); -void ModInfoDialog::saveCurrentIniFile() -{ - QVariant fileNameVar = ui->iniFileView->property("currentFile"); - QVariant encodingVar = ui->iniFileView->property("encoding"); - if (fileNameVar.isValid() && !fileNameVar.toString().isEmpty()) { - QString fileName = fileNameVar.toString(); - QDir().mkpath(QFileInfo(fileName).absolutePath()); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); + // switching to the initial tab, if any + if (m_initialTab != ModInfoTabIDs::None) { + switchToTab(m_initialTab); + m_initialTab = ModInfoTabIDs::None; } - ui->saveButton->setEnabled(false); -} - -void ModInfoDialog::on_iniFileView_textChanged() -{ - QPushButton* saveButton = findChild<QPushButton*>("saveButton"); - saveButton->setEnabled(true); + if (ui->tabWidget->currentIndex() == oldTab) { + if (auto* tabInfo=currentTab()) { + // activated() has to be fired manually because the tab index hasn't been + // changed + tabInfo->tab->activated(); + } else { + qCritical() << "tab index " << oldTab << " not found"; + } + } } - -void ModInfoDialog::on_textFileView_textChanged() +void ModInfoDialog::setTabsVisibility(bool firstTime) { - ui->saveTXTButton->setEnabled(true); -} + // this flag is picked up by onTabSelectionChanged() to avoid triggering + // activation events while moving tabs around + QScopedValueRollback arrangingTabs(m_arrangingTabs, true); + // one bool per tab to indicate whether the tab should be visible + std::vector<bool> visibility(m_tabs.size()); -void ModInfoDialog::on_activateESP_clicked() -{ - QListWidget *activeESPList = findChild<QListWidget*>("activeESPList"); - QListWidget *inactiveESPList = findChild<QListWidget*>("inactiveESPList"); + bool changed = false; - int selectedRow = inactiveESPList->currentRow(); - if (selectedRow < 0) { - return; - } + for (std::size_t i=0; i<m_tabs.size(); ++i) { + const auto& tabInfo = m_tabs[i]; - QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); + bool visible = true; - QDir root(m_RootPath); - bool renamed = false; - - while (root.exists(selectedItem->text())) { - bool okClicked = false; - QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); - if (!okClicked) { - inactiveESPList->insertItem(selectedRow, selectedItem); - return; - } else if (newName.size() > 0) { - selectedItem->setText(newName); - renamed = true; + // a tab is visible if it can handle the current mod + if (m_mod->hasFlag(ModInfo::FLAG_FOREIGN)) { + visible = tabInfo.tab->canHandleUnmanaged(); + } else if (m_mod->hasFlag(ModInfo::FLAG_SEPARATOR)) { + visible = tabInfo.tab->canHandleSeparators(); } - } - if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { - activeESPList->addItem(selectedItem); - if (renamed) { - selectedItem->setData(Qt::UserRole, QVariant()); + // if the visibility of this tab is changing, set changed to true because + // the tabs have to be rebuilt + const auto currentlyVisible = (ui->tabWidget->indexOf(tabInfo.widget) != -1); + if (visible != currentlyVisible) { + changed = true; } - } else { - inactiveESPList->insertItem(selectedRow, selectedItem); - reportError(tr("failed to move file")); - } -} - -void ModInfoDialog::on_deactivateESP_clicked() -{ - QListWidget *activeESPList = findChild<QListWidget*>("activeESPList"); - QListWidget *inactiveESPList = findChild<QListWidget*>("inactiveESPList"); + visibility[i] = visible; + } - int selectedRow = activeESPList->currentRow(); - if (selectedRow < 0) { + // the tabs have to be rebuilt the first time the dialog is shown, or when + // the visibility of any tab has changed + if (!firstTime && !changed) { return; } - QDir root(m_RootPath); - - QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); - - // if we moved the file from optional to active in this session, we move the file back to - // where it came from. Otherwise, it is moved to the new folder "optional" - if (selectedItem->data(Qt::UserRole).isNull()) { - selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); - if (!root.exists("optional")) { - if (!root.mkdir("optional")) { - reportError(tr("failed to create directory \"optional\"")); - activeESPList->insertItem(selectedRow, selectedItem); - return; - } - } + // save the current order (if necessary) because some tabs will be removed and + // others added + if (!firstTime) { + // but don't do it the first time visibility is set because the tabs are + // in the default order, which will clobber the current settings + saveTabOrder(Settings::instance()); } - if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { - inactiveESPList->addItem(selectedItem); - } else { - activeESPList->insertItem(selectedRow, selectedItem); + // remember selection, if any + auto sel = ModInfoTabIDs::None; + if (const auto* tabInfo=currentTab()) { + sel = tabInfo->tab->tabID(); } -} -void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) -{ - emit linkActivated(link); + // removes all tabs and re-adds the visible ones + reAddTabs(visibility, sel); } -void ModInfoDialog::linkClicked(const QUrl &url) +void ModInfoDialog::reAddTabs( + const std::vector<bool>& visibility, ModInfoTabIDs sel) { - //Ideally we'd ask the mod for the game and the web service then pass the game - //and URL to the web service - if (NexusInterface::instance(m_PluginContainer)->isURLGameRelated(url)) { + Q_ASSERT(visibility.size() == m_tabs.size()); - emit linkActivated(url.toString()); - } else { - ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } -} + // ordered tab names from settings + const auto orderedNames = getOrderedTabNames(); -void ModInfoDialog::linkClicked(QString url) -{ - emit linkActivated(url); -} + // whether the tabs can be sorted; if the object name of a tab widget is not + // found in orderedNames, the list cannot be sorted safely + bool canSort = true; + // gathering visible tabs + std::vector<TabInfo*> visibleTabs; + for (std::size_t i=0; i<m_tabs.size(); ++i) { + if (!visibility[i]) { + // this tab is not visible, skip it + continue; + } -void ModInfoDialog::refreshNexusData(int modID) -{ - if ((!m_RequestStarted) && (modID > 0)) { - m_RequestStarted = true; + // this tab is visible + visibleTabs.push_back(&m_tabs[i]); - m_ModInfo->updateNXMInfo(); + if (canSort) { + // make sure the widget object name is found in the list + const auto objectName = m_tabs[i].widget->objectName(); + auto itor = std::find(orderedNames.begin(), orderedNames.end(), objectName); - MessageDialog::showMessage(tr("Info requested, please wait"), this); + if (itor == orderedNames.end()) { + // this shouldn't happen, it means there's a tab in the UI that's no + // in the list + qCritical() << "can't sort tabs, '" << objectName << "' not found"; + canSort = false; + } + } } -} + // sorting tabs + if (canSort) { + std::sort(visibleTabs.begin(), visibleTabs.end(), [&](auto&& a, auto&& b){ + // looking the names in the ordered list + auto aItor = std::find( + orderedNames.begin(), orderedNames.end(), + a->widget->objectName()); -QString ModInfoDialog::getFileCategory(int categoryID) -{ - switch (categoryID) { - case 1: return tr("Main"); - case 2: return tr("Update"); - case 3: return tr("Optional"); - case 4: return tr("Old"); - case 5: return tr("Miscellaneous"); - case 6: return tr("Deleted"); - default: return tr("Unknown"); - } -} + auto bItor = std::find( + orderedNames.begin(), orderedNames.end(), + b->widget->objectName()); + // this shouldn't happen, it was checked above + Q_ASSERT(aItor != orderedNames.end() && bItor != orderedNames.end()); -void ModInfoDialog::updateVersionColor() -{ -// QPalette versionColor; - if (m_ModInfo->getVersion() != m_ModInfo->getNewestVersion()) { - ui->versionEdit->setStyleSheet("color: red"); -// versionColor.setColor(QPalette::Text, Qt::red); - ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); - } else { - ui->versionEdit->setStyleSheet("color: green"); -// versionColor.setColor(QPalette::Text, Qt::green); - ui->versionEdit->setToolTip(tr("No update available")); + return (aItor < bItor); + }); } -// ui->versionEdit->setPalette(versionColor); -} -void ModInfoDialog::modDetailsUpdated(bool success) -{ - QString nexusDescription = m_ModInfo->getNexusDescription(); - QString descriptionAsHTML = "<html>" - "<head><style class=\"nexus-description\">body {font-style: sans-serif; background: #707070; } a { color: #5EA2E5; }</style></head>" - "<body>%1</body>" - "</html>"; + // removing all tabs + ui->tabWidget->clear(); - if (!nexusDescription.isEmpty()) { - descriptionAsHTML = descriptionAsHTML.arg(BBCode::convertToHTML(nexusDescription)); - } else { - descriptionAsHTML = descriptionAsHTML.arg(tr("<div style=\"text-align: center;\"><h1>Uh oh!</h1><p>Sorry, there is no description available for this mod. :(</p></div>")); + // reset real positions + for (auto& tabInfo : m_tabs) { + tabInfo.realPos = -1; } - ui->descriptionView->page()->setHtml(descriptionAsHTML); - - updateVersionColor(); -} + // add visible tabs + for (std::size_t i=0; i<visibleTabs.size(); ++i) { + auto& tabInfo = *visibleTabs[i]; + // remembering real position + tabInfo.realPos = static_cast<int>(i); -void ModInfoDialog::activateNexusTab() -{ - QLineEdit *modIDEdit = findChild<QLineEdit*>("modIDEdit"); - int modID = modIDEdit->text().toInt(); - if (modID > 0) { - QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName()); - QLabel *visitNexusLabel = findChild<QLabel*>("visitNexusLabel"); - visitNexusLabel->setText(tr("<a href=\"%1\">Visit on Nexus</a>").arg(nexusLink)); - visitNexusLabel->setToolTip(nexusLink); - m_ModInfo->setURL(nexusLink); + // adding tab + ui->tabWidget->addTab(tabInfo.widget, tabInfo.icon, tabInfo.caption); - if (m_ModInfo->getNexusDescription().isEmpty() || QDateTime::currentDateTimeUtc() >= m_ModInfo->getLastNexusQuery().addDays(1)) { - refreshNexusData(modID); - } else { - modDetailsUpdated(true); + // selecting + if (tabInfo.tab->tabID() == sel) { + ui->tabWidget->setCurrentIndex(static_cast<int>(i)); } - } else - modDetailsUpdated(true); - QLineEdit *versionEdit = findChild<QLineEdit*>("versionEdit"); - QString currentVersion = m_Settings->value("version", "???").toString(); - versionEdit->setText(currentVersion); - ui->customUrlLineEdit->setText(m_ModInfo->getURL()); + } } - -void ModInfoDialog::on_tabWidget_currentChanged(int index) +void ModInfoDialog::updateTabs(bool becauseOriginChanged) { - if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) { - activateNexusTab(); - } -} + auto* origin = getOrigin(); + // list of tabs that should be updated + std::vector<TabInfo*> interestedTabs; -void ModInfoDialog::on_modIDEdit_editingFinished() -{ - int oldID = m_Settings->value("modid", 0).toInt(); - int modID = ui->modIDEdit->text().toInt(); - if (oldID != modID){ - m_ModInfo->setNexusID(modID); + for (auto& tabInfo : m_tabs) { + // don't touch invisible tabs + if (!tabInfo.isVisible()) { + continue; + } - ui->descriptionView->page()->setHtml(""); - if (modID != 0) { - m_RequestStarted = false; - refreshNexusData(modID); + // updateTabs() is also called from onOriginModified() to update all the + // tabs that depend on the origin; if updateTabs() is called because the + // origin changed, but the tab doesn't use origin files, it can be safely + // skipped + // + // this happens for tabs like notes and categories, which don't need to + // be updated when files change + if (becauseOriginChanged && !tabInfo.tab->usesOriginFiles()) { + continue; } + + // this tab should be updated + interestedTabs.push_back(&tabInfo); } -} -void ModInfoDialog::on_sourceGameEdit_currentIndexChanged(int) -{ - for (auto game : m_PluginContainer->plugins<IPluginGame>()) { - if (game->gameName() == ui->sourceGameEdit->currentText()) { - m_ModInfo->setGameName(game->gameShortName()); - return; - } + + for (auto* tabInfo : interestedTabs) { + // set the current mod + tabInfo->tab->setMod(m_mod, origin); + + // clear + tabInfo->tab->clear(); } -} -void ModInfoDialog::on_versionEdit_editingFinished() -{ - VersionInfo version(ui->versionEdit->text()); - m_ModInfo->setVersion(version); - updateVersionColor(); -} + // feed all the files from the filesystem + feedFiles(interestedTabs); -void ModInfoDialog::on_customUrlLineEdit_editingFinished() -{ - m_ModInfo->setURL(ui->customUrlLineEdit->text()); + // call update() on all tabs + for (auto* tabInfo : interestedTabs) { + tabInfo->tab->update(); + } + + // update the text colours + setTabsColors(); } -bool ModInfoDialog::recursiveDelete(const QModelIndex &index) +void ModInfoDialog::feedFiles(std::vector<TabInfo*>& interestedTabs) { - for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { - QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); - if (m_FileSystemModel->isDir(childIndex)) { - if (!recursiveDelete(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } else { - if (!m_FileSystemModel->remove(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } - } - if (!m_FileSystemModel->remove(index)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); - return false; + const auto rootPath = m_mod->absolutePath(); + if (rootPath.isEmpty()) { + return; } - return true; -} + const fs::path fsPath(rootPath.toStdWString()); -void ModInfoDialog::on_openInExplorerButton_clicked() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} + for (const auto& entry : fs::recursive_directory_iterator(fsPath)) { + if (!entry.is_regular_file()) { + // skip directories + continue; + } -void ModInfoDialog::deleteFile(const QModelIndex &index) -{ + const auto filePath = QString::fromStdWString(entry.path().native()); - bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) - : m_FileSystemModel->remove(index); - if (!res) { - QString fileName = m_FileSystemModel->fileName(index); - reportError(tr("Failed to delete %1").arg(fileName)); + // for each tab + for (auto* tabInfo : interestedTabs) { + if (tabInfo->tab->feedFile(rootPath, filePath)) { + break; + } + } } } -void ModInfoDialog::delete_activated() +void ModInfoDialog::setTabsColors() { - if (ui->fileTree->hasFocus()) { - QItemSelectionModel *selection = ui->fileTree->selectionModel(); + const auto p = m_mainWindow->palette(); - if (selection->hasSelection() && selection->selectedRows().count() >= 1) { + for (const auto& tabInfo : m_tabs) { + if (!tabInfo.isVisible()) { + // don't bother with invisible tabs + continue; + } - if (selection->selectedRows().count() == 0) { - return; - } - else if (selection->selectedRows().count() == 1) { - QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } + const QColor color = tabInfo.tab->hasData() ? + QColor::Invalid : + p.color(QPalette::Disabled, QPalette::WindowText); - foreach(QModelIndex index, selection->selectedRows()) { - deleteFile(index); - } - } - } + ui->tabWidget->tabBar()->setTabTextColor(tabInfo.realPos, color); + } } -void ModInfoDialog::deleteTriggered() +void ModInfoDialog::switchToTab(ModInfoTabIDs id) { - if (m_FileSelection.count() == 0) { - return; - } else if (m_FileSelection.count() == 1) { - QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + // look a tab with the given id + for (const auto& tabInfo : m_tabs) { + if (tabInfo.tab->tabID() == id) { + // use realPos to select the proper tab in the widget + ui->tabWidget->setCurrentIndex(tabInfo.realPos); return; } } - foreach(QModelIndex index, m_FileSelection) { - deleteFile(index); - } + // this could happen if the tab is not visible right now + qDebug() + << "can't switch to tab ID " << static_cast<int>(id) + << ", not available"; } - -void ModInfoDialog::renameTriggered() +MOShared::FilesOrigin* ModInfoDialog::getOrigin() { - QModelIndex selection = m_FileSelection.at(0); - QModelIndex index = selection.sibling(selection.row(), 0); - if (!index.isValid() || m_FileSystemModel->isReadOnly()) { - return; + auto* ds = m_core->directoryStructure(); + + if (!ds->originExists(m_mod->name().toStdWString())) { + return nullptr; } - ui->fileTree->edit(index); -} + auto* origin = &ds->getOriginByName(m_mod->name().toStdWString()); + if (origin->isDisabled()) { + return nullptr; + } + return origin; +} -void ModInfoDialog::hideTriggered() +void ModInfoDialog::saveState(Settings& s) const { - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (!path.endsWith(ModInfo::s_HiddenExt)) { - hideFile(path); - } - } -} + saveTabOrder(s); + // remove 2.2.0 settings + s.directInterface().remove("mod_info_tabs"); + s.directInterface().remove("mod_info_conflict_expanders"); + s.directInterface().remove("mod_info_conflicts"); + s.directInterface().remove("mod_info_advanced_conflicts"); + s.directInterface().remove("mod_info_conflicts_overwrite"); + s.directInterface().remove("mod_info_conflicts_noconflict"); + s.directInterface().remove("mod_info_conflicts_overwritten"); -void ModInfoDialog::unhideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (path.endsWith(ModInfo::s_HiddenExt)) { - unhideFile(path); - } + // save state for each tab + for (const auto& tabInfo : m_tabs) { + tabInfo.tab->saveState(s); } } - -void ModInfoDialog::openFile(const QModelIndex &index) +void ModInfoDialog::restoreState(const Settings& s) { - QString fileName = m_FileSystemModel->filePath(index); + // tab order is not restored here, it will be picked up if tabs have to be + // removed and re-added - HINSTANCE res = ::ShellExecuteW(nullptr, L"open", ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOW); - if ((unsigned long long)res <= 32) { - qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res); + // restore state for each tab + for (const auto& tabInfo : m_tabs) { + tabInfo.tab->restoreState(s); } } - -void ModInfoDialog::openTriggered() +void ModInfoDialog::saveTabOrder(Settings& s) const { - foreach(QModelIndex idx, m_FileSelection) { - openFile(idx); + if (static_cast<int>(m_tabs.size()) != ui->tabWidget->count()) { + // only save tab state when all tabs are visible + // + // if not all tabs are visible, it becomes very difficult to figure out in + // what order the user wants these tabs to be, so just avoid saving it + // completely + // + // this means that reordering tabs when not all tabs are visible is not + // saved, but it's better than breaking everything + return; } -} - -void ModInfoDialog::createDirectoryTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - QModelIndex index = m_FileSystemModel->isDir(selection) ? selection - : selection.parent(); - index = index.sibling(index.row(), 0); + QString names; - QString name = tr("New Folder"); - QString path = m_FileSystemModel->filePath(index).append("/"); - - QModelIndex existingIndex = m_FileSystemModel->index(path + name); - int suffix = 1; - while (existingIndex.isValid()) { - name = tr("New Folder") + QString::number(suffix++); - existingIndex = m_FileSystemModel->index(path + name); - } + for (int i=0; i<ui->tabWidget->count(); ++i) { + if (!names.isEmpty()) { + names += " "; + } - QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); - if (!newIndex.isValid()) { - reportError(tr("Failed to create \"%1\"").arg(name)); - return; + names += ui->tabWidget->widget(i)->objectName(); } - ui->fileTree->setCurrentIndex(newIndex); - ui->fileTree->edit(newIndex); + s.directInterface().setValue("mod_info_tab_order", names); } - -void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) +std::vector<QString> ModInfoDialog::getOrderedTabNames() const { - QItemSelectionModel *selectionModel = ui->fileTree->selectionModel(); - m_FileSelection = selectionModel->selectedRows(0); + const auto& settings = Settings::instance().directInterface(); -// m_FileSelection = ui->fileTree->indexAt(pos); - QMenu menu(ui->fileTree); + std::vector<QString> v; - menu.addAction(m_NewFolderAction); + if (settings.contains("mod_info_tabs")) { + // old byte array from 2.2.0 + QDataStream stream(settings.value("mod_info_tabs").toByteArray()); - bool hasFiles = false; + int count = 0; + stream >> count; - foreach(QModelIndex idx, m_FileSelection) { - if (m_FileSystemModel->fileInfo(idx).isFile()) { - hasFiles = true; - break; + for (int i=0; i<count; ++i) { + QString s; + stream >> s; + v.emplace_back(std::move(s)); } - } + } else { + // string list + QString string = settings.value("mod_info_tab_order").toString(); + QTextStream stream(&string); - if (selectionModel->hasSelection()) { - if (hasFiles) { - menu.addAction(m_OpenAction); - } - menu.addAction(m_RenameAction); - menu.addAction(m_DeleteAction); - if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(m_UnhideAction); - } else { - menu.addAction(m_HideAction); + while (!stream.atEnd()) { + QString s; + stream >> s; + v.emplace_back(std::move(s)); } - } else { - m_FileSelection.clear(); - m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); } - menu.exec(ui->fileTree->mapToGlobal(pos)); -} - -void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) -{ - QTreeWidgetItem *parent = item->parent(); - while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { - parent->setCheckState(0, Qt::Checked); - parent = parent->parent(); - } - refreshPrimaryCategoriesBox(); + return v; } - -void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) +void ModInfoDialog::onOriginModified(int originID) { - for (int i = 0; i < tree->childCount(); ++i) { - QTreeWidgetItem *child = tree->child(i); - if (child->checkState(0) == Qt::Checked) { - ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); - addCheckedCategories(child); - } - } -} + // tell the main window the origin changed + emit originModified(originID); + // update tabs that depend on the origin + updateTabs(true); +} -void ModInfoDialog::refreshPrimaryCategoriesBox() +void ModInfoDialog::onDeleteShortcut() { - ui->primaryCategoryBox->clear(); - int primaryCategory = m_ModInfo->getPrimaryCategory(); - addCheckedCategories(ui->categoriesTree->invisibleRootItem()); - for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { - if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { - ui->primaryCategoryBox->setCurrentIndex(i); - break; - } + // forward the request to the current tab + if (auto* tabInfo=currentTab()) { + tabInfo->tab->deleteRequested(); } } - -void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) +void ModInfoDialog::closeEvent(QCloseEvent* e) { - if (index != -1) { - m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); + if (tryClose()) { + e->accept(); + } else { + e->ignore(); } } - -void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) +void ModInfoDialog::onCloseButton() { - this->close(); - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); + if (tryClose()) { + close(); + } } - -bool ModInfoDialog::hideFile(const QString &oldName) +bool ModInfoDialog::tryClose() { - QString newName = oldName + ModInfo::s_HiddenExt; + // cancel the close if any tab returns false; for example. this can happen if + // a tab has unsaved content, pops a confirmation dialog, and the user clicks + // cancel - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { + for (auto& tabInfo : m_tabs) { + if (!tabInfo.tab->canClose()) { return false; } } - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); - return false; - } + return true; } - -bool ModInfoDialog::unhideFile(const QString &oldName) +void ModInfoDialog::onTabSelectionChanged() { - QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); - return false; + if (m_arrangingTabs) { + // this can be fired while re-arranging tabs, which happens before mods + // are given to tabs, and might trigger first activation, which breaks all + // sorts of things + return; } -} - -void ModInfoDialog::hideConflictFile() -{ - if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - emit originModified(m_Origin->getID()); - refreshLists(); + // this will call firstActivation() on the tab if needed + if (auto* tabInfo=currentTab()) { + tabInfo->tab->activated(); } } - -void ModInfoDialog::unhideConflictFile() +void ModInfoDialog::onTabMoved() { - if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - emit originModified(m_Origin->getID()); - refreshLists(); + // reset + for (auto& tabInfo : m_tabs) { + tabInfo.realPos = -1; } -} - -int ModInfoDialog::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) -{ - QString extension = targetInfo.suffix(); - if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || - (extension.compare("com", Qt::CaseInsensitive) == 0) || - (extension.compare("bat", Qt::CaseInsensitive) == 0)) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - return 1; - } - else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { - binaryInfo = targetInfo; - return 1; - } - else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { - // types that need to be injected into - std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); - QString binaryPath; - - { // try to find java automatically - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(buffer, &binaryType)) { - qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); - } - else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty() && (extension == "jar")) { - // second attempt: look to the registry - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - } - if (binaryPath.isEmpty()) { - binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); - } - if (binaryPath.isEmpty()) { - return 0; - } - binaryInfo = QFileInfo(binaryPath); - if (extension == "jar") { - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - else { - arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - return 1; - } - else { - return 2; - } -} - -void ModInfoDialog::openDataFile() -{ - if (m_ConflictsContextItem != nullptr) { - QFileInfo targetInfo(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - m_OrganizerCore->spawnBinaryDirect( - binaryInfo, arguments, m_OrganizerCore->currentProfile()->name(), - targetInfo.absolutePath(), "", ""); - } break; - case 2: { - ::ShellExecuteW(nullptr, L"open", - ToWString(targetInfo.absoluteFilePath()).c_str(), - nullptr, nullptr, SW_SHOWNORMAL); - } break; - default: { - // nop - } break; - } - } -} - -void ModInfoDialog::previewDataFile() -{ - QString fileName = QDir::fromNativeSeparators(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); - // what we have is an absolute path to the file in its actual location (for the primary origin) - // what we want is the path relative to the virtual data directory + // for each tab in the widget + for (int i=0; i<ui->tabWidget->count(); ++i) { + const auto* w = ui->tabWidget->widget(i); - // we need to look in the virtual directory for the file to make sure the info is up to date. + bool found = false; - // check if the file comes from the actual data folder instead of a mod - QDir gameDirectory = m_OrganizerCore->managedGame()->dataDirectory().absolutePath(); - QString relativePath = gameDirectory.relativeFilePath(fileName); - QDir direRelativePath = gameDirectory.relativeFilePath(fileName); - // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case - if (!direRelativePath.isAbsolute() && !relativePath.startsWith("..")) { - fileName = relativePath; - } - else { - // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory - int offset = m_OrganizerCore->settings().getModDirectory().size() + 1; - offset = fileName.indexOf("/", offset); - fileName = fileName.mid(offset + 1); - } - - - - const FileEntry::Ptr file = m_OrganizerCore->directoryStructure()->searchFile(ToWString(fileName), nullptr); - - if (file.get() == nullptr) { - reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); - return; - } - - // set up preview dialog - PreviewDialog preview(fileName); - auto addFunc = [&](int originId) { - FilesOrigin &origin = m_OrganizerCore->directoryStructure()->getOriginByID(originId); - QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; - if (QFile::exists(filePath)) { - // it's very possible the file doesn't exist, because it's inside an archive. we don't support that - QWidget *wid = m_PluginContainer->previewGenerator().genPreview(filePath); - if (wid == nullptr) { - reportError(tr("failed to generate preview for %1").arg(filePath)); - } - else { - preview.addVariant(ToQString(origin.getName()), wid); - } - } - }; - - addFunc(file->getOrigin()); - for (auto alt : file->getAlternatives()) { - addFunc(alt.first); - } - if (preview.numVariants() > 0) { - preview.exec(); - } - else { - QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); - } -} - - -void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); - - if (m_ConflictsContextItem != nullptr) { - // offer to hide/unhide file, but not for files from archives - if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { - QMenu menu; - if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); - } else { - menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); - } - - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - - QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); - if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); + // find the corresponding tab info + for (auto& tabInfo : m_tabs) { + if (tabInfo.widget == w) { + tabInfo.realPos = i; + found = true; + break; } + } - menu.exec(ui->overwriteTree->mapToGlobal(pos)); + if (!found) { + qCritical() << "unknown tab at index " << i; } } } -void ModInfoDialog::on_overwrittenTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwrittenTree->itemAt(pos.x(), pos.y()); - - if (m_ConflictsContextItem != nullptr) { - if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { - QMenu menu; - - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - - QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); - if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); - } - - menu.exec(ui->overwrittenTree->mapToGlobal(pos)); - } - } -} - - -void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); - this->accept(); -} - -void ModInfoDialog::on_refreshButton_clicked() -{ - if (m_ModInfo->getNexusID() > 0) { - QLineEdit *modIDEdit = findChild<QLineEdit*>("modIDEdit"); - int modID = modIDEdit->text().toInt(); - refreshNexusData(modID); - } else - qInfo("Mod has no valid Nexus ID, info can't be updated."); -} - -void ModInfoDialog::on_endorseBtn_clicked() -{ - emit endorseMod(m_ModInfo); -} - -void ModInfoDialog::on_nextButton_clicked() -{ - int currentTab = ui->tabWidget->currentIndex(); - int tab = m_RealTabPos[currentTab]; - - emit modOpenNext(tab); - this->accept(); -} - -void ModInfoDialog::on_prevButton_clicked() +void ModInfoDialog::onNextMod() { - int currentTab = ui->tabWidget->currentIndex(); - int tab = m_RealTabPos[currentTab]; + auto mod = m_mainWindow->nextModInList(); + if (!mod || mod == m_mod) { + return; + } - emit modOpenPrev(tab); - this->accept(); + setMod(mod); + update(); } - -void ModInfoDialog::createTweak() +void ModInfoDialog::onPreviousMod() { - QString name = QInputDialog::getText(this, tr("Name"), tr("Please enter a name")); - if (name.isNull()) { - return; - } else if (!fixDirectoryName(name)) { - QMessageBox::critical(this, tr("Error"), tr("Invalid name. Must be a valid file name")); - return; - } else if (ui->iniTweaksList->findItems(name, Qt::MatchFixedString).count() != 0) { - QMessageBox::critical(this, tr("Error"), tr("A tweak by that name exists")); + auto mod = m_mainWindow->previousModInList(); + if (!mod || mod == m_mod) { return; } - QListWidgetItem *newTweak = new QListWidgetItem(name + ".ini"); - newTweak->setData(Qt::UserRole, "INI Tweaks/" + name + ".ini"); - newTweak->setFlags(newTweak->flags() | Qt::ItemIsUserCheckable); - newTweak->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newTweak); -} - -void ModInfoDialog::on_iniTweaksList_customContextMenuRequested(const QPoint &pos) -{ - QMenu menu; - menu.addAction(tr("Create Tweak"), this, SLOT(createTweak())); - menu.exec(ui->iniTweaksList->mapToGlobal(pos)); + setMod(mod); + update(); } diff --git a/src/modinfodialog.h b/src/modinfodialog.h index 4c731c00..34555b0c 100644 --- a/src/modinfodialog.h +++ b/src/modinfodialog.h @@ -23,231 +23,236 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "modinfo.h"
#include "tutorabledialog.h"
-#include "plugincontainer.h"
-#include "organizercore.h"
+#include "filerenamer.h"
+#include "modinfodialogfwd.h"
-#include <QDialog>
-#include <QSignalMapper>
-#include <QSettings>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QModelIndex>
-#include <QAction>
-#include <QListWidgetItem>
-#include <QTreeWidgetItem>
-#include <QTextCodec>
-#include <set>
-#include <directoryentry.h>
+namespace Ui { class ModInfoDialog; }
+namespace MOShared { class FilesOrigin; }
-
-namespace Ui {
- class ModInfoDialog;
-}
-
-class QFileSystemModel;
-class QTreeView;
-class CategoryFactory;
+class PluginContainer;
+class OrganizerCore;
+class Settings;
+class ModInfoDialogTab;
+class MainWindow;
/**
- * this is a larger dialog used to visualise information abount the mod.
+ * this is a larger dialog used to visualise information about the mod.
* @todo this would probably a good place for a plugin-system
**/
class ModInfoDialog : public MOBase::TutorableDialog
{
- Q_OBJECT
+ Q_OBJECT;
+
+ // creates a tab, it's a friend because it uses a bunch of member variables
+ // to create ModInfoDialogTabContext
+ //
+ template <class T>
+ friend std::unique_ptr<ModInfoDialogTab> createTab(
+ ModInfoDialog& d, ModInfoTabIDs index);
public:
+ ModInfoDialog(
+ MainWindow* mw, OrganizerCore* core, PluginContainer* plugin,
+ ModInfo::Ptr mod);
- enum ETabs {
- TAB_TEXTFILES,
- TAB_INIFILES,
- TAB_IMAGES,
- TAB_ESPS,
- TAB_CONFLICTS,
- TAB_CATEGORIES,
- TAB_NEXUS,
- TAB_NOTES,
- TAB_FILETREE
- };
+ ~ModInfoDialog();
-public:
+ // switches to the tab with the given id
+ //
+ void selectTab(ModInfoTabIDs id);
- /**
- * @brief constructor
- *
- * @param modInfo info structure about the mod to display
- * @param parent parend widget
- **/
- explicit ModInfoDialog(ModInfo::Ptr modInfo, const MOShared::DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent = 0);
+ // updates all tabs, selects the initial tab and opens the dialog
+ //
+ int exec() override;
- ~ModInfoDialog();
+ // saves the dialog state and calls saveState() on all tabs
+ //
+ void saveState(Settings& s) const;
+
+ // restores the dialog state and calls restoreState() on all tabs
+ //
+ void restoreState(const Settings& s);
+
+signals:
+ // emitted when a tab changes the origin
+ //
+ void originModified(int originID);
- /**
- * @brief retrieve the (user-modified) version of the mod
- *
- * @return the (user-modified) version of the mod
- **/
- QString getModVersion() const;
+protected:
+ // forwards to tryClose()
+ //
+ void closeEvent(QCloseEvent* e);
- /**
- * @brief retrieve the (user-modified) mod id
- *
- * @return the (user-modified) id of the mod
- **/
- const int getModID() const;
+private:
+ // represents a single tab
+ //
+ struct TabInfo
+ {
+ // tab implementation
+ std::unique_ptr<ModInfoDialogTab> tab;
- /**
- * @brief open the specified tab in the dialog if it's enabled
- *
- * @param tab the tab to activate
- **/
- void openTab(int tab);
+ // actual position in the tab bar, updated every time a tab is moved
+ int realPos;
- void restoreTabState(const QByteArray &state);
+ // widget used by the QTabWidget for this tab
+ //
+ // because QTabWidget doesn't support simply hiding tabs, they have to be
+ // completely removed from the widget when they don't support the current
+ // mod
+ //
+ // therefore, `widget, `caption` and `icon` are remembered so tabs can be
+ // removed and re-added when navigating between mods
+ //
+ // `widget` is also used figure out which tab is where when they're
+ // re-ordered
+ QWidget* widget;
- QByteArray saveTabState() const;
+ // caption for this tab, see `widget`
+ QString caption;
-signals:
+ // icon for this tab, see `widget`
+ QIcon icon;
- void thumbnailClickedSignal(const QString &filename);
- void linkActivated(const QString &link);
- void downloadRequest(const QString &link);
- void modOpen(const QString &modName, int tab);
- void modOpenNext(int tab=-1);
- void modOpenPrev(int tab=-1);
- void originModified(int originID);
- void endorseMod(ModInfo::Ptr nexusID);
-public slots:
+ TabInfo(std::unique_ptr<ModInfoDialogTab> tab);
- void modDetailsUpdated(bool success);
+ // returns whether this tab is part of the tab widget
+ //
+ bool isVisible() const;
+ };
-private:
+ std::unique_ptr<Ui::ModInfoDialog> ui;
+ MainWindow* m_mainWindow;
+ ModInfo::Ptr m_mod;
+ OrganizerCore* m_core;
+ PluginContainer* m_plugin;
+ std::vector<TabInfo> m_tabs;
- void initFiletree(ModInfo::Ptr modInfo);
- void initINITweaks();
+ // initial tab requested by the main window when the dialog is opened; whether
+ // the request can be honoured depends on what tabs are present
+ ModInfoTabIDs m_initialTab;
- void refreshLists();
+ // set to true when tabs are being removed and re-added while navigating
+ // between mods; since the current index changes while this is happening,
+ // onTabSelectionChanged() will be called repeatedly
+ //
+ // however, it will check this flag and ignore the event so first activations
+ // are not fired incorrectly
+ bool m_arrangingTabs;
- void addCategories(const CategoryFactory &factory, const std::set<int> &enabledCategories, QTreeWidgetItem *root, int rootLevel);
- void updateVersionColor();
+ // creates all the tabs and connects events
+ //
+ void createTabs();
- void refreshNexusData(int modID);
- void activateNexusTab();
- QString getFileCategory(int categoryID);
- bool recursiveDelete(const QModelIndex &index);
- void deleteFile(const QModelIndex &index);
- void openFile(const QModelIndex &index);
- void saveIniTweaks();
- void saveCategories(QTreeWidgetItem *currentNode);
- void saveCurrentTextFile();
- void saveCurrentIniFile();
- void openTextFile(const QString &fileName);
- void openIniFile(const QString &fileName);
- bool allowNavigateFromTXT();
- bool allowNavigateFromINI();
- bool hideFile(const QString &oldName);
- bool unhideFile(const QString &oldName);
- void addCheckedCategories(QTreeWidgetItem *tree);
- void refreshPrimaryCategoriesBox();
- int tabIndex(const QString &tabId);
+ // sets the currently selected mod; resets first activation, but doesn't
+ // update anything
+ //
+ void setMod(ModInfo::Ptr mod);
-private slots:
+ // sets the currently selected mod, if found; forwards to setMod() above
+ //
+ void setMod(const QString& name);
- void hideConflictFile();
- void unhideConflictFile();
- int getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments);
- void previewDataFile();
- void openDataFile();
+ // returns the origin of the current mod, may be null
+ //
+ MOShared::FilesOrigin* getOrigin();
- void thumbnailClicked(const QString &fileName);
- void linkClicked(const QUrl &url);
- void linkClicked(QString url);
+ // returns the currently selected tab, taking re-ordering in to account;
+ // shouldn't be null, but could be
+ //
+ TabInfo* currentTab();
- void delete_activated();
- void deleteTriggered();
- void renameTriggered();
- void openTriggered();
- void createDirectoryTriggered();
- void hideTriggered();
- void unhideTriggered();
+ // fully updates the dialog; sets the title, the tab visibility and updates
+ // all the tabs; used when the current mod changes
+ //
+ // see setTabsVisibility() for firstTime
+ //
+ void update(bool firstTime=false);
- void on_openInExplorerButton_clicked();
- void on_closeButton_clicked();
- void on_saveButton_clicked();
- void on_activateESP_clicked();
- void on_deactivateESP_clicked();
- void on_saveTXTButton_clicked();
- void on_visitNexusLabel_linkActivated(const QString &link);
- void on_modIDEdit_editingFinished();
- void on_sourceGameEdit_currentIndexChanged(int);
- void on_versionEdit_editingFinished();
- void on_customUrlLineEdit_editingFinished();
- void on_iniFileView_textChanged();
- void on_textFileView_textChanged();
- void on_tabWidget_currentChanged(int index);
- void on_primaryCategoryBox_currentIndexChanged(int index);
- void on_categoriesTree_itemChanged(QTreeWidgetItem *item, int column);
- void on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int column);
- void on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int column);
- void on_overwriteTree_customContextMenuRequested(const QPoint &pos);
- void on_overwrittenTree_customContextMenuRequested(const QPoint &pos);
- void on_fileTree_customContextMenuRequested(const QPoint &pos);
+ // builds the list of visible tabs; if the list is different from what's
+ // currently displayed, or firstTime is true, forwards to reAddTabs()
+ void setTabsVisibility(bool firstTime);
- void on_refreshButton_clicked();
+ // clears the tab widgets and re-adds the tabs having the visible flag in
+ // the given vector, following the tab order from the settings
+ //
+ void reAddTabs(const std::vector<bool>& visibility, ModInfoTabIDs sel);
- void on_endorseBtn_clicked();
+ // called by update(); clears tabs, feeds files and calls update() on all
+ // tabs, then setTabsColors()
+ //
+ void updateTabs(bool becauseOriginChanged=false);
- void on_nextButton_clicked();
+ // goes through all files on the filesystem for the current mod and calls
+ // feedFile() on every tab until one accepts it
+ //
+ void feedFiles(std::vector<TabInfo*>& interestedTabs);
- void on_prevButton_clicked();
+ // goes through all tabs and sets the tab text colour depending on whether
+ // they have data or not
+ //
+ void setTabsColors();
- void on_iniTweaksList_customContextMenuRequested(const QPoint &pos);
- void createTweak();
-private:
+ // called when the delete key is pressed anywhere in the dialog; forwards to
+ // ModInfoDialogTab::deleteRequest() for the currently selected tab
+ //
+ void onDeleteShortcut();
+
- Ui::ModInfoDialog *ui;
+ // finds the tab with the given id and selects it
+ //
+ void switchToTab(ModInfoTabIDs id);
- ModInfo::Ptr m_ModInfo;
- int m_OriginID;
- QSignalMapper m_ThumbnailMapper;
- QString m_RootPath;
+ // saves the current tab order; used by saveState(), but also by
+ // setTabsVisibility() to make sure any changes to order are saved before
+ // re-adding tabs
+ //
+ void saveTabOrder(Settings& s) const;
- OrganizerCore *m_OrganizerCore;
- PluginContainer *m_PluginContainer;
+ // returns a list of tab names in the order they should appear on the widget
+ //
+ std::vector<QString> getOrderedTabNames() const;
- QFileSystemModel *m_FileSystemModel;
- QTreeView *m_FileTree;
- QModelIndexList m_FileSelection;
+ // asks all the tabs if they accept closing the dialog, returns false if one
+ // objected
+ //
+ bool tryClose();
- QSettings *m_Settings;
- std::set<int> m_RequestIDs;
- bool m_RequestStarted;
+ // called when the user clicks the close button; closing the dialog by other
+ // means ends up in closeEvent(); forwards to tryClose()
+ //
+ void onCloseButton();
- QAction *m_DeleteAction;
- QAction *m_RenameAction;
- QAction *m_OpenAction;
- QAction *m_NewFolderAction;
- QAction *m_HideAction;
- QAction *m_UnhideAction;
+ // called when the user clicks the previous button; asks the main window for
+ // the previous mod in the list and loads it
+ //
+ void onPreviousMod();
- QTreeWidgetItem *m_ConflictsContextItem;
+ // called when the user clicks the next button; asks the main window for the
+ // next mod in the list and loads it
+ //
+ void onNextMod();
- const MOShared::DirectoryEntry *m_Directory;
- MOShared::FilesOrigin *m_Origin;
+ // called when the selects a tab; handles first activation
+ //
+ void onTabSelectionChanged();
- std::map<int, int> m_RealTabPos;
+ // called when the user re-orders tabs; sets the correct TabInfo::realPos for
+ // all tabs
+ //
+ void onTabMoved();
+ // called when a tab has modified the origin; emits originModified() and
+ // updates all the tabs that use origin files
+ //
+ void onOriginModified(int originID);
};
#endif // MODINFODIALOG_H
diff --git a/src/modinfodialog.ui b/src/modinfodialog.ui index 0211e704..5b559992 100644 --- a/src/modinfodialog.ui +++ b/src/modinfodialog.ui @@ -6,7 +6,7 @@ <rect>
<x>0</x>
<y>0</y>
- <width>790</width>
+ <width>735</width>
<height>534</height>
</rect>
</property>
@@ -27,162 +27,134 @@ </property>
<widget class="QWidget" name="tabText">
<attribute name="title">
- <string>Textfiles</string>
+ <string>Text Files</string>
</attribute>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <layout class="QVBoxLayout" name="verticalLayout_15">
<item>
- <widget class="QListWidget" name="textFileList">
- <property name="maximumSize">
- <size>
- <width>192</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="toolTip">
- <string>A list of text-files in the mod directory.</string>
+ <widget class="QSplitter" name="tabTextSplitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
</property>
- <property name="whatsThis">
- <string>A list of text-files in the mod directory like readmes. </string>
+ <property name="childrenCollapsible">
+ <bool>false</bool>
</property>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_9">
- <item>
- <widget class="QTextEdit" name="textFileView">
- <property name="toolTip">
- <string/>
+ <widget class="QWidget" name="widget_6" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_16">
+ <property name="leftMargin">
+ <number>0</number>
</property>
- <property name="whatsThis">
- <string/>
+ <property name="topMargin">
+ <number>0</number>
</property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="saveTXTButton">
- <property name="enabled">
- <bool>false</bool>
+ <property name="rightMargin">
+ <number>0</number>
</property>
- <property name="text">
- <string>Save</string>
+ <property name="bottomMargin">
+ <number>0</number>
</property>
- </widget>
- </item>
- </layout>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Text Files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="textFileList">
+ <property name="toolTip">
+ <string>A list of text-files in the mod directory.</string>
+ </property>
+ <property name="whatsThis">
+ <string>A list of text-files in the mod directory like readmes. </string>
+ </property>
+ <property name="uniformItemSizes">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="textFileFilter"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="widget_7" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_17">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="TextEditor" name="textFileEditor"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabIni">
<attribute name="title">
- <string>INI-Files</string>
+ <string>INI Files</string>
</attribute>
- <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <layout class="QVBoxLayout" name="verticalLayout_9">
<item>
- <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0,0">
- <property name="spacing">
- <number>6</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
+ <widget class="QSplitter" name="tabIniSplitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
</property>
- <item>
- <widget class="QLabel" name="label_6">
- <property name="text">
- <string>Ini Files</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QListWidget" name="iniFileList">
- <property name="maximumSize">
- <size>
- <width>228</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="toolTip">
- <string>This is a list of .ini files in the mod.</string>
- </property>
- <property name="whatsThis">
- <string>This is a list of .ini files in the mod. These are usually used to configure the behaviour of mods if there are configurable parameters.</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_5">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="text">
- <string>Ini Tweaks *This feature is non-functional*</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QListWidget" name="iniTweaksList">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="maximumSize">
- <size>
- <width>228</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>This is a list of ini tweaks (ini modifications that can be toggled).</string>
- </property>
- <property name="whatsThis">
- <string>This is a list of ini tweaks. Ini Tweaks are (usually small) fragments of ini files that are applied over existing settings in skyrim.ini/skyrimprefs.ini. Each tweak can be toggled individually. You should check the description of the mod wether the tweaks are really optional.</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,0">
- <item>
- <widget class="QTextBrowser" name="iniFileView">
- <property name="toolTip">
- <string/>
- </property>
- <property name="whatsThis">
- <string/>
- </property>
- <property name="undoRedoEnabled">
- <bool>true</bool>
- </property>
- <property name="readOnly">
- <bool>false</bool>
- </property>
- <property name="acceptRichText">
- <bool>false</bool>
- </property>
- <property name="openLinks">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="saveButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Save changes to the file.</string>
- </property>
- <property name="whatsThis">
- <string>Save changes to the file. This overwrites the original. There is no automatic backup!</string>
- </property>
- <property name="text">
- <string>Save</string>
+ <widget class="QWidget" name="layoutWidget">
+ <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0">
+ <property name="spacing">
+ <number>6</number>
</property>
- </widget>
- </item>
- </layout>
+ <item>
+ <widget class="QLabel" name="label_6">
+ <property name="text">
+ <string>Ini Files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="iniFileList">
+ <property name="toolTip">
+ <string>This is a list of .ini files in the mod.</string>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of .ini files in the mod. These are usually used to configure the behaviour of mods if there are configurable parameters.</string>
+ </property>
+ <property name="uniformItemSizes">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="iniFileFilter"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="layoutWidget1">
+ <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0">
+ <item>
+ <widget class="TextEditor" name="iniFileEditor">
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
</item>
</layout>
</widget>
@@ -190,67 +162,19 @@ <attribute name="title">
<string>Images</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QLabel" name="imageLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="layoutDirection">
- <enum>Qt::LeftToRight</enum>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
+ <layout class="QVBoxLayout" name="verticalLayout_19">
<item>
- <widget class="QScrollArea" name="scrollArea">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>128</height>
- </size>
+ <widget class="QSplitter" name="tabImagesSplitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
</property>
- <property name="widgetResizable">
- <bool>true</bool>
+ <property name="childrenCollapsible">
+ <bool>false</bool>
</property>
- <widget class="QWidget" name="scrollAreaWidgetContents">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>746</width>
- <height>126</height>
- </rect>
- </property>
- <property name="toolTip">
- <string>Images located in the mod.</string>
- </property>
- <property name="whatsThis">
- <string>This lists all the images (.jpg and .png) in the mod directory, like screenshots and such. Click one to get a larger view.</string>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <widget class="QWidget" name="widget_8" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,0,0">
<property name="spacing">
- <number>0</number>
+ <number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
@@ -265,11 +189,110 @@ <number>0</number>
</property>
<item>
- <layout class="QHBoxLayout" name="thumbnailArea">
- <property name="spacing">
- <number>0</number>
+ <widget class="QWidget" name="imagesScroller" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="ImagesTabHelpers::ThumbnailsWidget" name="imagesThumbnails" native="true">
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ImagesTabHelpers::Scrollbar" name="imagesScrollerVBar">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="imagesShowDDS">
+ <property name="text">
+ <string>Show .dds files</string>
</property>
- </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="imagesFilter"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="imagesContainer" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_18" stretch="0,1">
+ <property name="spacing">
+ <number>3</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="widget_12" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QToolButton" name="imagesExplore">
+ <property name="text">
+ <string>Open in Explorer</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="imagesPath">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="imagesSize">
+ <property name="text">
+ <string>0x0</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="imagesImage" native="true"/>
</item>
</layout>
</widget>
@@ -281,117 +304,181 @@ <attribute name="title">
<string>Optional ESPs</string>
</attribute>
- <layout class="QFormLayout" name="formLayout">
- <item row="1" column="1">
- <widget class="QListWidget" name="inactiveESPList">
- <property name="toolTip">
- <string>List of esps, esms, and esls that can not be loaded by the game.</string>
- </property>
- <property name="whatsThis">
- <string>List of esps, esms, and esls contained in this plugin that currently can not be loaded by the game. They will not even appear in the esp-list in the main MO-window.
-They usually contain optional functionality, see the readme.
-
-Most mods do not have optional esps, so chances are good you are looking at an empty list.</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>Optional ESPs</string>
+ <layout class="QVBoxLayout" name="verticalLayout_23">
+ <item>
+ <widget class="QSplitter" name="ESPsSplitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
</property>
- </widget>
- </item>
- <item row="2" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_5">
- <item>
- <widget class="QToolButton" name="deactivateESP">
- <property name="minimumSize">
- <size>
- <width>96</width>
- <height>0</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Make the selected mod in the lower list unavailable.</string>
- </property>
- <property name="whatsThis">
- <string>The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become "invisible" to the game. It can then no longer be activated.</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/go-up.png</normaloff>:/MO/gui/resources/go-up.png</iconset>
+ <widget class="QWidget" name="widget_9" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_20">
+ <property name="leftMargin">
+ <number>0</number>
</property>
- <property name="iconSize">
- <size>
- <width>22</width>
- <height>22</height>
- </size>
+ <property name="topMargin">
+ <number>0</number>
</property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="activateESP">
- <property name="minimumSize">
- <size>
- <width>96</width>
- <height>0</height>
- </size>
+ <property name="rightMargin">
+ <number>0</number>
</property>
- <property name="toolTip">
- <string>Move a file to the data directory.</string>
+ <property name="bottomMargin">
+ <number>0</number>
</property>
- <property name="whatsThis">
- <string>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes "available", it will not necessarily be loaded! That is configured in the main window of omo.</string>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Optional ESPs</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="inactiveESPList">
+ <property name="toolTip">
+ <string>List of esps, esms, and esls that can not be loaded by the game.</string>
+ </property>
+ <property name="whatsThis">
+ <string>List of esps, esms, and esls contained in this plugin that currently can not be loaded by the game. They will not even appear in the esp-list in the main MO-window.
+They usually contain optional functionality, see the readme.
+
+Most mods do not have optional esps, so chances are good you are looking at an empty list.</string>
+ </property>
+ <property name="uniformItemSizes">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="widget_11" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
</property>
- <property name="text">
- <string/>
+ <property name="topMargin">
+ <number>0</number>
</property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/go-down.png</normaloff>:/MO/gui/resources/go-down.png</iconset>
+ <property name="rightMargin">
+ <number>0</number>
</property>
- <property name="iconSize">
- <size>
- <width>22</width>
- <height>22</height>
- </size>
+ <property name="bottomMargin">
+ <number>0</number>
</property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="2" column="0">
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="3" column="1">
- <widget class="QListWidget" name="activeESPList">
- <property name="toolTip">
- <string>ESPs in the data directory and thus visible to the game.</string>
- </property>
- <property name="whatsThis">
- <string>These are the mod files that are in the (virtual) data directory of your game and will thus be selecteable in the esp list in the main window.</string>
- </property>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Available ESPs</string>
- </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_22">
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QToolButton" name="activateESP">
+ <property name="toolTip">
+ <string>Move a file to the data directory.</string>
+ </property>
+ <property name="whatsThis">
+ <string>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes "available", it will not necessarily be loaded! That is configured in the main window of omo.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/next</normaloff>:/MO/gui/next</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>22</width>
+ <height>22</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="deactivateESP">
+ <property name="toolTip">
+ <string>Make the selected mod in the lower list unavailable.</string>
+ </property>
+ <property name="whatsThis">
+ <string>The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become "invisible" to the game. It can then no longer be activated.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/previous</normaloff>:/MO/gui/previous</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>22</width>
+ <height>22</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_10" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_21">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Available ESPs</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="activeESPList">
+ <property name="toolTip">
+ <string>ESPs in the data directory and thus visible to the game.</string>
+ </property>
+ <property name="whatsThis">
+ <string><html><head/><body><p>These are the mod files that are in the (virtual) data directory of your game and will thus be selectable in the esp list in the main window.</p></body></html></string>
+ </property>
+ <property name="uniformItemSizes">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
</layout>
@@ -400,149 +487,361 @@ Most mods do not have optional esps, so chances are good you are looking at an e <attribute name="title">
<string>Conflicts</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_8">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_10" stretch="1,0">
- <item>
- <widget class="QLabel" name="label_7">
- <property name="text">
- <string>The following conflicted files are provided by this mod</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLCDNumber" name="overwriteCount">
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <property name="lineWidth">
- <number>1</number>
- </property>
- <property name="segmentStyle">
- <enum>QLCDNumber::Flat</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QTreeWidget" name="overwriteTree">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="textElideMode">
- <enum>Qt::ElideLeft</enum>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- <property name="animated">
- <bool>true</bool>
- </property>
- <property name="headerHidden">
- <bool>false</bool>
- </property>
- <property name="columnCount">
- <number>2</number>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>365</number>
- </attribute>
- <attribute name="headerHighlightSections">
- <bool>false</bool>
- </attribute>
- <attribute name="headerMinimumSectionSize">
- <number>200</number>
- </attribute>
- <column>
- <property name="text">
- <string>File</string>
- </property>
- </column>
- <column>
- <property name="text">
- <string>Overwritten Mods</string>
- </property>
- </column>
- </widget>
- </item>
+ <layout class="QVBoxLayout" name="verticalLayout_8" stretch="0">
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="1,0">
- <item>
- <widget class="QLabel" name="label_8">
- <property name="text">
- <string>The following conflicted files are provided by other mods</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLCDNumber" name="overwrittenCount">
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <property name="segmentStyle">
- <enum>QLCDNumber::Flat</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QTreeWidget" name="overwrittenTree">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="textElideMode">
- <enum>Qt::ElideLeft</enum>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
+ <widget class="QTabWidget" name="tabConflictsTabs">
+ <property name="currentIndex">
+ <number>0</number>
</property>
- <property name="animated">
- <bool>true</bool>
- </property>
- <property name="columnCount">
- <number>2</number>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>365</number>
- </attribute>
- <attribute name="headerMinimumSectionSize">
- <number>200</number>
- </attribute>
- <column>
- <property name="text">
- <string>File</string>
- </property>
- </column>
- <column>
- <property name="text">
- <string>Providing Mod</string>
- </property>
- </column>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>General</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_11" stretch="0,1,0,1,0,1,0">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="overwriteRoot">
+ <item>
+ <layout class="QHBoxLayout" name="overwriteHeader" stretch="1,0">
+ <item>
+ <widget class="QToolButton" name="overwriteExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>The following conflicted files are provided by this mod</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLCDNumber" name="overwriteCount">
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="overwriteTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideLeft</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="overwrittenRoot">
+ <item>
+ <layout class="QHBoxLayout" name="overwrittenHeader" stretch="1,0">
+ <item>
+ <widget class="QToolButton" name="overwrittenExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>The following conflicted files are provided by other mods</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLCDNumber" name="overwrittenCount">
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="overwrittenTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideLeft</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="noConflictRoot">
+ <item>
+ <layout class="QHBoxLayout" name="noConflictHeader" stretch="1,0">
+ <item>
+ <widget class="QToolButton" name="noConflictExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>The following files have no conflicts</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLCDNumber" name="noConflictCount">
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="noConflictTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideLeft</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>Advanced</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_13">
+ <item>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_14">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTreeView" name="conflictsAdvancedList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_4" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="conflictsAdvancedShowNoConflict">
+ <property name="toolTip">
+ <string>Whether files that have no conflicts should be visible in the list</string>
+ </property>
+ <property name="text">
+ <string>Show files that have no conflicts</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="conflictsAdvancedShowAll">
+ <property name="toolTip">
+ <string>Shows all mods overwriting or being overwritten by this mod</string>
+ </property>
+ <property name="text">
+ <string>Show all conflicting mods</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="conflictsAdvancedShowNearest">
+ <property name="toolTip">
+ <string>Shows only the nearest conflicting mods, in order of priority</string>
+ </property>
+ <property name="text">
+ <string>Show nearest conflicting mod</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_5" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <property name="leftMargin">
+ <number>20</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLineEdit" name="conflictsAdvancedFilter">
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_8" stretch="1,0">
- <item>
- <widget class="QLabel" name="label_9">
- <property name="text">
- <string>Non-Conflicted files</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLCDNumber" name="noConflictCount">
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <property name="segmentStyle">
- <enum>QLCDNumber::Flat</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
</layout>
</widget>
<widget class="QWidget" name="tabCategories">
@@ -551,10 +850,7 @@ Most mods do not have optional esps, so chances are good you are looking at an e </attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
- <widget class="QTreeWidget" name="categoriesTree">
- <property name="animated">
- <bool>true</bool>
- </property>
+ <widget class="QTreeWidget" name="categories">
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
@@ -575,7 +871,7 @@ Most mods do not have optional esps, so chances are good you are looking at an e </widget>
</item>
<item>
- <widget class="QComboBox" name="primaryCategoryBox"/>
+ <widget class="QComboBox" name="primaryCategories"/>
</item>
</layout>
</item>
@@ -591,202 +887,268 @@ Most mods do not have optional esps, so chances are good you are looking at an e </attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string>Mod ID</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="ModIDLineEdit" name="modIDEdit">
- <property name="toolTip">
- <string>Mod ID for this mod on Nexus.</string>
- </property>
- <property name="whatsThis">
- <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+ <widget class="QWidget" name="widget_14" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_25">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0,0,0,0,0,1">
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Mod ID</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ModIDLineEdit" name="modID">
+ <property name="toolTip">
+ <string>Mod ID for this mod on Nexus.</string>
+ </property>
+ <property name="whatsThis">
+ <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Mod ID for this mod on Nexus. This is filled in automatically if you downloaded and installed the mod from inside MO. Otherwise you can enter it manually. To find the correct id, find the mod on nexus. The URL will look like this: <a href=" https://www.nexusmods.com/skyrimspecialedition/mods/6194"><span style=" text-decoration: underline; color:#0000ff;">https://www.nexusmods.com/skyrimspecialedition/mods/6194</span></a>. In this example, 6194 is the id you're looking for. Besides: The above is the link to Mod Organizer2 on Nexus. Why not go there now and endorse us?</p></body></html></string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_4">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>10</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="label_10">
- <property name="text">
- <string>Source Game</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="sourceGameEdit">
- <property name="toolTip">
- <string>Source game for this mod.</string>
- </property>
- <property name="whatsThis">
- <string><html><head/><body><p>Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.</p></body></html></string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_3">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="versionLabel">
- <property name="whatsThis">
- <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Mod ID for this mod on Nexus. This is filled in automatically if you downloaded and installed the mod from inside MO. Otherwise you can enter it manually. To find the correct id, find the mod on nexus. The URL will look like this: <a href=" https://www.nexusmods.com/skyrimspecialedition/mods/6194"><span style=" text-decoration: underline; color:#0000ff;">https://www.nexusmods.com/skyrimspecialedition/mods/6194</span></a>. In this example, 6194 is the id you're looking for. Besides: The above is the link to Mod Organizer 2 on Nexus. Why not go there now and endorse us?</p></body></html></string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_10">
+ <property name="text">
+ <string>Source Game</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="sourceGame">
+ <property name="toolTip">
+ <string>Source game for this mod.</string>
+ </property>
+ <property name="whatsThis">
+ <string><html><head/><body><p>Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.</p></body></html></string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="versionLabel">
+ <property name="whatsThis">
+ <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Installed Version of the Mod. The tooltip will contain the current version available on nexus. The installed version is only set if you installed the mod through MO.</span></p></body></html></string>
- </property>
- <property name="text">
- <string>Version</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="versionEdit">
- <property name="maximumSize">
- <size>
- <width>150</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="maxLength">
- <number>32</number>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_11" stretch="0,1">
- <item>
- <widget class="QPushButton" name="refreshButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="toolTip">
- <string>Refresh</string>
- </property>
- <property name="whatsThis">
- <string>Refresh all information from Nexus.</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/refresh</normaloff>:/MO/gui/refresh</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_4">
- <property name="text">
- <string>Description</string>
- </property>
- </widget>
- </item>
- </layout>
+ </property>
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="version">
+ <property name="maxLength">
+ <number>32</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_15" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="refresh">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Refresh</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh all information from Nexus.</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/refresh</normaloff>:/MO/gui/refresh</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="visitNexus">
+ <property name="text">
+ <string>Open in Browser</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/internet-web-browser.png</normaloff>:/MO/gui/resources/internet-web-browser.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="endorse">
+ <property name="text">
+ <string>Endorse</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/icon_favorite</normaloff>:/MO/gui/icon_favorite</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="track">
+ <property name="text">
+ <string>Track</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/tracked</normaloff>:/MO/gui/tracked</iconset>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <widget class="QWebEngineView" name="descriptionView" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <widget class="QFrame" name="frame">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
</property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>200</height>
- </size>
- </property>
- <property name="url" stdset="0">
- <url>
- <string>about:blank</string>
- </url>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
</property>
+ <layout class="QVBoxLayout" name="verticalLayout_24">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWebEngineView" name="browser">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>200</height>
+ </size>
+ </property>
+ <property name="url">
+ <url>
+ <string>about:blank</string>
+ </url>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_7">
- <item>
- <widget class="QLabel" name="visitNexusLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>1</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="openExternalLinks">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="endorseBtn">
- <property name="text">
- <string>Endorse</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/icon_favorite</normaloff>:/MO/gui/icon_favorite</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_14">
- <item>
- <widget class="QLabel" name="label_11">
- <property name="text">
- <string>Web page URL (only used if invalid NexusID) :</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="customUrlLineEdit"/>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_13" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="hasCustomURL">
+ <property name="text">
+ <string>Use Custom URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="customURL"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="visitCustomURL">
+ <property name="text">
+ <string>Open in Browser</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
</layout>
</widget>
@@ -796,7 +1158,7 @@ p, li { white-space: pre-wrap; } </attribute>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
- <widget class="QLineEdit" name="commentsEdit">
+ <widget class="QLineEdit" name="comments">
<property name="toolTip">
<string>Enter comments about the mod here. These are displayed in the notes column of the mod list.</string>
</property>
@@ -809,7 +1171,7 @@ p, li { white-space: pre-wrap; } </widget>
</item>
<item>
- <widget class="QTextEdit" name="notesEdit">
+ <widget class="HTMLEditor" name="notes">
<property name="toolTip">
<string>Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column.</string>
</property>
@@ -834,7 +1196,7 @@ p, li { white-space: pre-wrap; } <item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
- <widget class="QPushButton" name="openInExplorerButton">
+ <widget class="QToolButton" name="openInExplorer">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
@@ -862,7 +1224,7 @@ p, li { white-space: pre-wrap; } </layout>
</item>
<item>
- <widget class="QTreeView" name="fileTree">
+ <widget class="QTreeView" name="filetree">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
@@ -886,6 +1248,9 @@ p, li { white-space: pre-wrap; } <property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
</widget>
</item>
</layout>
@@ -895,17 +1260,23 @@ p, li { white-space: pre-wrap; } <item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
- <widget class="QPushButton" name="prevButton">
+ <widget class="QPushButton" name="previousMod">
<property name="text">
<string>Previous</string>
</property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
</widget>
</item>
<item>
- <widget class="QPushButton" name="nextButton">
+ <widget class="QPushButton" name="nextMod">
<property name="text">
<string>Next</string>
</property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
</widget>
</item>
<item>
@@ -922,10 +1293,13 @@ p, li { white-space: pre-wrap; } </spacer>
</item>
<item>
- <widget class="QPushButton" name="closeButton">
+ <widget class="QPushButton" name="close">
<property name="text">
<string>Close</string>
</property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
</widget>
</item>
</layout>
@@ -943,6 +1317,27 @@ p, li { white-space: pre-wrap; } <extends>QLineEdit</extends>
<header>modidlineedit.h</header>
</customwidget>
+ <customwidget>
+ <class>TextEditor</class>
+ <extends>QPlainTextEdit</extends>
+ <header>texteditor.h</header>
+ </customwidget>
+ <customwidget>
+ <class>HTMLEditor</class>
+ <extends>QTextEdit</extends>
+ <header>texteditor.h</header>
+ </customwidget>
+ <customwidget>
+ <class>ImagesTabHelpers::ThumbnailsWidget</class>
+ <extends>QWidget</extends>
+ <header>modinfodialogimages.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>ImagesTabHelpers::Scrollbar</class>
+ <extends>QScrollBar</extends>
+ <header>modinfodialogimages.h</header>
+ </customwidget>
</customwidgets>
<resources>
<include location="resources.qrc"/>
diff --git a/src/modinfodialogcategories.cpp b/src/modinfodialogcategories.cpp new file mode 100644 index 00000000..c61e248e --- /dev/null +++ b/src/modinfodialogcategories.cpp @@ -0,0 +1,137 @@ +#include "modinfodialogcategories.h" +#include "ui_modinfodialog.h" +#include "categories.h" +#include "modinfo.h" + +CategoriesTab::CategoriesTab(ModInfoDialogTabContext cx) + : ModInfoDialogTab(std::move(cx)) +{ + connect( + ui->categories, &QTreeWidget::itemChanged, + [&](auto* item, int col){ onCategoryChanged(item, col); }); + + connect( + ui->primaryCategories, + static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), + [&](int index){ onPrimaryChanged(index); }); +} + +void CategoriesTab::clear() +{ + ui->categories->clear(); + ui->primaryCategories->clear(); + setHasData(false); +} + +void CategoriesTab::update() +{ + clear(); + + add( + CategoryFactory::instance(), mod().getCategories(), + ui->categories->invisibleRootItem(), 0); + + updatePrimary(); +} + +bool CategoriesTab::canHandleSeparators() const +{ + return true; +} + +bool CategoriesTab::usesOriginFiles() const +{ + return false; +} + +void CategoriesTab::add( + const CategoryFactory &factory, const std::set<int>& enabledCategories, + QTreeWidgetItem* root, int rootLevel) +{ + for (int i=0; i<static_cast<int>(factory.numCategories()); ++i) { + if (factory.getParentID(i) != rootLevel) { + continue; + } + + int categoryID = factory.getCategoryID(i); + + QTreeWidgetItem *newItem + = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); + + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + + newItem->setCheckState(0, enabledCategories.find(categoryID) + != enabledCategories.end() + ? Qt::Checked + : Qt::Unchecked); + + newItem->setData(0, Qt::UserRole, categoryID); + + if (factory.hasChildren(i)) { + add(factory, enabledCategories, newItem, categoryID); + } + + root->addChild(newItem); + } +} + +void CategoriesTab::updatePrimary() +{ + ui->primaryCategories->clear(); + + int primaryCategory = mod().getPrimaryCategory(); + + addChecked(ui->categories->invisibleRootItem()); + + for (int i = 0; i < ui->primaryCategories->count(); ++i) { + if (ui->primaryCategories->itemData(i).toInt() == primaryCategory) { + ui->primaryCategories->setCurrentIndex(i); + break; + } + } + + setHasData(ui->primaryCategories->count() > 0); +} + +void CategoriesTab::addChecked(QTreeWidgetItem* tree) +{ + for (int i = 0; i < tree->childCount(); ++i) { + QTreeWidgetItem *child = tree->child(i); + if (child->checkState(0) == Qt::Checked) { + ui->primaryCategories->addItem(child->text(0), child->data(0, Qt::UserRole)); + addChecked(child); + } + } +} + +void CategoriesTab::save(QTreeWidgetItem* currentNode) +{ + for (int i = 0; i < currentNode->childCount(); ++i) { + QTreeWidgetItem *childNode = currentNode->child(i); + + mod().setCategory( + childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); + + save(childNode); + } +} + +void CategoriesTab::onCategoryChanged(QTreeWidgetItem* item, int) +{ + QTreeWidgetItem *parent = item->parent(); + + while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { + parent->setCheckState(0, Qt::Checked); + parent = parent->parent(); + } + + updatePrimary(); + save(ui->categories->invisibleRootItem()); +} + +void CategoriesTab::onPrimaryChanged(int index) +{ + if (index != -1) { + mod().setPrimaryCategory(ui->primaryCategories->itemData(index).toInt()); + } +} diff --git a/src/modinfodialogcategories.h b/src/modinfodialogcategories.h new file mode 100644 index 00000000..c8b52fec --- /dev/null +++ b/src/modinfodialogcategories.h @@ -0,0 +1,27 @@ +#include "modinfodialogtab.h" + +class CategoryFactory; + +class CategoriesTab : public ModInfoDialogTab +{ +public: + CategoriesTab(ModInfoDialogTabContext cx); + + void clear() override; + void update() override; + bool canHandleSeparators() const override; + bool usesOriginFiles() const override; + +private: + void add( + const CategoryFactory& factory, const std::set<int>& enabledCategories, + QTreeWidgetItem* root, int rootLevel); + + void updatePrimary(); + void addChecked(QTreeWidgetItem* tree); + + void save(QTreeWidgetItem* currentNode); + + void onCategoryChanged(QTreeWidgetItem* item, int col); + void onPrimaryChanged(int index); +}; diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp new file mode 100644 index 00000000..511d48ad --- /dev/null +++ b/src/modinfodialogconflicts.cpp @@ -0,0 +1,1229 @@ +#include "modinfodialogconflicts.h" +#include "ui_modinfodialog.h" +#include "modinfodialog.h" +#include "utility.h" +#include "settings.h" +#include "organizercore.h" + +using namespace MOShared; +using namespace MOBase; +namespace shell = MOBase::shell; + +// if there are more than 50 selected items in the conflict tree, don't bother +// checking whether menu items apply to them, just show all of them +const std::size_t max_small_selection = 50; + + +class ConflictItem +{ +public: + ConflictItem( + QString before, QString relativeName, QString after, + FileEntry::Index index, QString fileName, + bool hasAltOrigins, QString altOrigin, bool archive) : + m_before(std::move(before)), + m_relativeName(std::move(relativeName)), + m_after(std::move(after)), + m_index(index), + m_fileName(std::move(fileName)), + m_hasAltOrigins(hasAltOrigins), + m_altOrigin(std::move(altOrigin)), + m_isArchive(archive) + { + } + + const QString& before() const + { + return m_before; + } + + const QString& relativeName() const + { + return m_relativeName; + } + + const QString& after() const + { + return m_after; + } + + const QString& fileName() const + { + return m_fileName; + } + + const QString& altOrigin() const + { + return m_altOrigin; + } + + bool hasAlts() const + { + return m_hasAltOrigins; + } + + bool isArchive() const + { + return m_isArchive; + } + + FileEntry::Index fileIndex() const + { + return m_index; + } + + bool canHide() const + { + return canHideFile(isArchive(), fileName()); + } + + bool canUnhide() const + { + return canUnhideFile(isArchive(), fileName()); + } + + bool canOpen() const + { + return canOpenFile(isArchive(), fileName()); + } + + bool canPreview(PluginContainer& pluginContainer) const + { + return canPreviewFile(pluginContainer, isArchive(), fileName()); + } + + bool canExplore() const + { + return canExploreFile(isArchive(), fileName()); + } + +private: + QString m_before; + QString m_relativeName; + QString m_after; + FileEntry::Index m_index; + QString m_fileName; + bool m_hasAltOrigins; + QString m_altOrigin; + bool m_isArchive; +}; + + +class ConflictListModel : public QAbstractItemModel +{ +public: + struct Column + { + QString caption; + const QString& (ConflictItem::*getText)() const; + }; + + ConflictListModel(QTreeView* tree, std::vector<Column> columns) : + m_tree(tree), m_columns(std::move(columns)), + m_sortColumn(-1), m_sortOrder(Qt::AscendingOrder) + { + m_tree->setModel(this); + } + + void clear() + { + m_items.clear(); + endResetModel(); + } + + void reserve(std::size_t s) + { + m_items.reserve(s); + } + + QModelIndex index(int row, int col, const QModelIndex& ={}) const override + { + return createIndex(row, col); + } + + QModelIndex parent(const QModelIndex&) const override + { + return {}; + } + + int rowCount(const QModelIndex& parent={}) const override + { + if (parent.isValid()) { + return 0; + } + + return static_cast<int>(m_items.size()); + } + + int columnCount(const QModelIndex& ={}) const override + { + return static_cast<int>(m_columns.size()); + } + + QVariant data(const QModelIndex& index, int role) const override + { + if (role == Qt::DisplayRole || role == Qt::FontRole) { + const auto row = index.row(); + if (row < 0) { + return {}; + } + + const auto i = static_cast<std::size_t>(row); + if (i >= m_items.size()) { + return {}; + } + + const auto col = index.column(); + if (col < 0) { + return {}; + } + + const auto c = static_cast<std::size_t>(col); + if (c >= m_columns.size()) { + return {}; + } + + const auto& item = m_items[i]; + + if (role == Qt::DisplayRole) { + return (item.*m_columns[c].getText)(); + } else if (role == Qt::FontRole) { + if (item.isArchive()) { + QFont f = m_tree->font(); + f.setItalic(true); + return f; + } + } + } + + return {}; + } + + QVariant headerData(int col, Qt::Orientation, int role) const + { + if (role == Qt::DisplayRole) { + if (col < 0) { + return {}; + } + + const auto i = static_cast<std::size_t>(col); + if (i >= m_columns.size()) { + return {}; + } + + return m_columns[i].caption; + } + + return {}; + } + + void sort(int colIndex, Qt::SortOrder order=Qt::AscendingOrder) + { + m_sortColumn = colIndex; + m_sortOrder = order; + + doSort(); + emit layoutChanged({}, QAbstractItemModel::VerticalSortHint); + } + + void add(ConflictItem item) + { + m_items.emplace_back(std::move(item)); + } + + void finished() + { + endResetModel(); + doSort(); + } + + const ConflictItem* getItem(std::size_t row) const + { + if (row >= m_items.size()) { + return nullptr; + } + + return &m_items[row]; + } + +private: + QTreeView* m_tree; + std::vector<Column> m_columns; + std::vector<ConflictItem> m_items; + int m_sortColumn; + Qt::SortOrder m_sortOrder; + + void doSort() + { + if (m_items.empty()) { + return; + } + + if (m_sortColumn < 0) { + return; + } + + const auto c = static_cast<std::size_t>(m_sortColumn); + if (c >= m_columns.size()) { + return; + } + + const auto& col = m_columns[c]; + + // avoids branching on sort order while sorting + auto sortAsc = [&](const auto& a, const auto& b) { + return (naturalCompare((a.*col.getText)(), (b.*col.getText)()) < 0); + }; + + auto sortDesc = [&](const auto& a, const auto& b) { + return (naturalCompare((a.*col.getText)(), (b.*col.getText)()) > 0); + }; + + if (m_sortOrder == Qt::AscendingOrder) { + std::sort(m_items.begin(), m_items.end(), sortAsc); + } else { + std::sort(m_items.begin(), m_items.end(), sortDesc); + } + } +}; + + +class OverwriteConflictListModel : public ConflictListModel +{ +public: + OverwriteConflictListModel(QTreeView* tree) + : ConflictListModel(tree, { + {tr("File"), &ConflictItem::relativeName}, + {tr("Overwritten Mods"), &ConflictItem::before} + }) + { + } +}; + + +class OverwrittenConflictListModel : public ConflictListModel +{ +public: + OverwrittenConflictListModel(QTreeView* tree) + : ConflictListModel(tree, { + {tr("File"), &ConflictItem::relativeName}, + {tr("Providing Mod"), &ConflictItem::after} + }) + { + } +}; + + +class NoConflictListModel : public ConflictListModel +{ +public: + NoConflictListModel(QTreeView* tree) + : ConflictListModel(tree, { + {tr("File"), &ConflictItem::relativeName} + }) + { + } +}; + + +class AdvancedConflictListModel : public ConflictListModel +{ +public: + AdvancedConflictListModel(QTreeView* tree) + : ConflictListModel(tree, { + {tr("Overwrites"), &ConflictItem::before}, + {tr("File"), &ConflictItem::relativeName}, + {tr("Overwritten By"), &ConflictItem::after} + }) + { + } +}; + + +std::size_t smallSelectionSize(const QTreeView* tree) +{ + const std::size_t too_many = std::numeric_limits<std::size_t>::max(); + + std::size_t n = 0; + const auto* sel = tree->selectionModel(); + + for (const auto& range : sel->selection()) { + n += range.height(); + + if (n >= max_small_selection) { + return too_many; + } + } + + return n; +} + +template <class F> +void for_each_in_selection(QTreeView* tree, F&& f) +{ + const auto* sel = tree->selectionModel(); + const auto* model = dynamic_cast<ConflictListModel*>(tree->model()); + + if (!model) { + qCritical() << "tree doesn't have a ConflictListModel"; + return; + } + + for (const auto& range : sel->selection()) { + // ranges are inclusive + for (int row=range.top(); row<=range.bottom(); ++row) { + if (auto* item=model->getItem(static_cast<std::size_t>(row))) { + if (!f(item)) { + return; + } + } + } + } +} + + +ConflictsTab::ConflictsTab(ModInfoDialogTabContext cx) : + ModInfoDialogTab(cx), // don't move, cx is used again + m_general(this, cx.ui, cx.core), m_advanced(this, cx.ui, cx.core) +{ + connect( + &m_general, &GeneralConflictsTab::modOpen, + [&](const QString& name){ emitModOpen(name); }); + + connect( + &m_advanced, &AdvancedConflictsTab::modOpen, + [&](const QString& name){ emitModOpen(name); }); +} + +void ConflictsTab::update() +{ + setHasData(m_general.update()); + m_advanced.update(); +} + +void ConflictsTab::clear() +{ + m_general.clear(); + m_advanced.clear(); + setHasData(false); +} + +void ConflictsTab::saveState(Settings& s) +{ + s.directInterface().setValue( + "mod_info_conflicts_tab", ui->tabConflictsTabs->currentIndex()); + + m_general.saveState(s); + m_advanced.saveState(s); +} + +void ConflictsTab::restoreState(const Settings& s) +{ + ui->tabConflictsTabs->setCurrentIndex( + s.directInterface().value("mod_info_conflicts_tab", 0).toInt()); + + m_general.restoreState(s); + m_advanced.restoreState(s); +} + +bool ConflictsTab::canHandleUnmanaged() const +{ + return true; +} + +void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible) +{ + bool changed = false; + bool stop = false; + + const auto n = smallSelectionSize(tree); + + qDebug().nospace().noquote() + << (visible ? "unhiding" : "hiding") << " " + << (n > max_small_selection ? "a lot of" : QString("%1").arg(n)) + << " conflict files"; + + QFlags<FileRenamer::RenameFlags> flags = + (visible ? FileRenamer::UNHIDE : FileRenamer::HIDE); + + if (n > 1) { + flags |= FileRenamer::MULTIPLE; + } + + FileRenamer renamer(parentWidget(), flags); + + auto* model = dynamic_cast<ConflictListModel*>(tree->model()); + if (!model) { + qCritical() << "list doesn't have a ConflictListModel"; + return; + } + + for_each_in_selection(tree, [&](const ConflictItem* item) { + if (stop) { + return false; + } + + auto result = FileRenamer::RESULT_CANCEL; + + if (visible) { + if (!item->canUnhide()) { + qDebug().nospace() << "cannot unhide " << item->relativeName() << ", skipping"; + return true; + } + + result = unhideFile(renamer, item->fileName()); + + } else { + if (!item->canHide()) { + qDebug().nospace() << "cannot hide " << item->relativeName() << ", skipping"; + return true; + } + + result = hideFile(renamer, item->fileName()); + } + + switch (result) { + case FileRenamer::RESULT_OK: { + // will trigger a refresh at the end + changed = true; + break; + } + + case FileRenamer::RESULT_SKIP: { + // nop + break; + } + + case FileRenamer::RESULT_CANCEL: { + // stop right now, but make sure to refresh if needed + stop = true; + break; + } + } + + return true; + }); + + qDebug().nospace() << (visible ? "unhiding" : "hiding") << " conflict files done"; + + if (changed) { + qDebug().nospace() << "triggering refresh"; + + if (origin()) { + emitOriginModified(); + } + + update(); + } +} + +void ConflictsTab::openItems(QTreeView* tree) +{ + // the menu item is only shown for a single selection, but handle all of them + // in case this changes + for_each_in_selection(tree, [&](const ConflictItem* item) { + core().executeFileVirtualized(parentWidget(), item->fileName()); + return true; + }); +} + +void ConflictsTab::previewItems(QTreeView* tree) +{ + // the menu item is only shown for a single selection, but handle all of them + // in case this changes + for_each_in_selection(tree, [&](const ConflictItem* item) { + core().previewFileWithAlternatives(parentWidget(), item->fileName()); + return true; + }); +} + +void ConflictsTab::exploreItems(QTreeView* tree) +{ + // the menu item is only shown for a single selection, but handle all of them + // in case this changes + for_each_in_selection(tree, [&](const ConflictItem* item) { + shell::ExploreFile(item->fileName()); + return true; + }); +} + +void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree) +{ + auto actions = createMenuActions(tree); + + QMenu menu; + + // open + if (actions.open) { + connect(actions.open, &QAction::triggered, [&]{ + openItems(tree); + }); + + menu.addAction(actions.open); + } + + // preview + if (actions.preview) { + connect(actions.preview, &QAction::triggered, [&]{ + previewItems(tree); + }); + + menu.addAction(actions.preview); + } + + // explore + if (actions.explore) { + connect(actions.explore, &QAction::triggered, [&]{ + exploreItems(tree); + }); + + menu.addAction(actions.explore); + } + + // hide + if (actions.hide) { + connect(actions.hide, &QAction::triggered, [&]{ + changeItemsVisibility(tree, false); + }); + + menu.addAction(actions.hide); + } + + // unhide + if (actions.unhide) { + connect(actions.unhide, &QAction::triggered, [&]{ + changeItemsVisibility(tree, true); + }); + + menu.addAction(actions.unhide); + } + + // goto + if (actions.gotoMenu) { + menu.addMenu(actions.gotoMenu); + + for (auto* a : actions.gotoActions) { + connect(a, &QAction::triggered, [&, name=a->text()]{ + emitModOpen(name); + }); + + actions.gotoMenu->addAction(a); + } + } + + if (!menu.isEmpty()) { + menu.exec(tree->viewport()->mapToGlobal(pos)); + } +} + +ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) +{ + if (tree->selectionModel()->selection().isEmpty()) { + return {}; + } + + bool enableHide = true; + bool enableUnhide = true; + bool enableOpen = true; + bool enablePreview = true; + bool enableExplore = true; + bool enableGoto = true; + + const auto n = smallSelectionSize(tree); + + const auto* model = dynamic_cast<ConflictListModel*>(tree->model()); + if (!model) { + qCritical() << "tree doesn't have a ConflictListModel"; + return {}; + } + + if (n == 1) { + // this is a single selection + const auto* item = model->getItem(static_cast<std::size_t>( + tree->selectionModel()->selectedRows()[0].row())); + + if (!item) { + return {}; + } + + enableHide = item->canHide(); + enableUnhide = item->canUnhide(); + enableOpen = item->canOpen(); + enablePreview = item->canPreview(plugin()); + enableExplore = item->canExplore(); + enableGoto = item->hasAlts(); + } + else { + // this is a multiple selection, don't show open/preview so users don't open + // a thousand files + enableOpen = false; + enablePreview = false; + + // can't explore multiple files + enableExplore = false; + + // don't bother with this on multiple selection, at least for now + enableGoto = false; + + if (n <= max_small_selection) { + // if the number of selected items is low, checking them to accurately + // show the menu items is worth it + enableHide = false; + enableUnhide = false; + + for_each_in_selection(tree, [&](const ConflictItem* item) { + if (item->canHide()) { + enableHide = true; + } + + if (item->canUnhide()) { + enableUnhide = true; + } + + if (enableHide && enableUnhide && enableGoto) { + // found all, no need to check more + return false; + } + + return true; + }); + } + } + + Actions actions; + + actions.hide = new QAction(tr("&Hide"), parentWidget()); + actions.hide->setEnabled(enableHide); + + // note that it is possible for hidden files to appear if they override other + // hidden files from another mod + actions.unhide = new QAction(tr("&Unhide"), parentWidget()); + actions.unhide->setEnabled(enableUnhide); + + actions.open = new QAction(tr("&Open/Execute"), parentWidget()); + actions.open->setEnabled(enableOpen); + + actions.preview = new QAction(tr("&Preview"), parentWidget()); + actions.preview->setEnabled(enablePreview); + + actions.explore = new QAction(tr("Open in &Explorer"), parentWidget()); + actions.explore->setEnabled(enableExplore); + + actions.gotoMenu = new QMenu(tr("&Go to..."), parentWidget()); + actions.gotoMenu->setEnabled(enableGoto); + + if (enableGoto && n == 1) { + const auto* item = model->getItem(static_cast<std::size_t>( + tree->selectionModel()->selectedRows()[0].row())); + + actions.gotoActions = createGotoActions(item); + } + + return actions; +} + +std::vector<QAction*> ConflictsTab::createGotoActions(const ConflictItem* item) +{ + if (!origin()) { + return {}; + } + + auto file = origin()->findFile(item->fileIndex()); + if (!file) { + return {}; + } + + + std::vector<QString> mods; + const auto& ds = *core().directoryStructure(); + + // add all alternatives + for (const auto& alt : file->getAlternatives()) { + const auto& o = ds.getOriginByID(alt.first); + if (o.getID() != origin()->getID()) { + mods.push_back(ToQString(o.getName())); + } + } + + // add the real origin if different from this mod + const FilesOrigin& realOrigin = ds.getOriginByID(file->getOrigin()); + if (realOrigin.getID() != origin()->getID()) { + mods.push_back(ToQString(realOrigin.getName())); + } + + std::sort(mods.begin(), mods.end(), [](const auto& a, const auto& b) { + return (QString::localeAwareCompare(a, b) < 0); + }); + + std::vector<QAction*> actions; + + for (const auto& name : mods) { + actions.push_back(new QAction(name, parentWidget())); + } + + return actions; +} + + +GeneralConflictsTab::GeneralConflictsTab( + ConflictsTab* tab, Ui::ModInfoDialog* pui, OrganizerCore& oc) : + m_tab(tab), ui(pui), m_core(oc), + m_overwriteModel(new OverwriteConflictListModel(ui->overwriteTree)), + m_overwrittenModel(new OverwrittenConflictListModel(ui->overwrittenTree)), + m_noConflictModel(new NoConflictListModel(ui->noConflictTree)) +{ + m_expanders.overwrite.set(ui->overwriteExpander, ui->overwriteTree, true); + m_expanders.overwritten.set(ui->overwrittenExpander, ui->overwrittenTree, true); + m_expanders.nonconflict.set(ui->noConflictExpander, ui->noConflictTree); + + QObject::connect( + ui->overwriteTree, &QTreeView::doubleClicked, + [&](auto&& item){ onOverwriteActivated(item); }); + + QObject::connect( + ui->overwrittenTree, &QTreeView::doubleClicked, + [&](auto&& item){ onOverwrittenActivated(item); }); + + QObject::connect( + ui->overwriteTree, &QTreeView::customContextMenuRequested, + [&](const QPoint& p){ m_tab->showContextMenu(p, ui->overwriteTree); }); + + QObject::connect( + ui->overwrittenTree, &QTreeView::customContextMenuRequested, + [&](const QPoint& p){ m_tab->showContextMenu(p, ui->overwrittenTree); }); + + QObject::connect( + ui->noConflictTree, &QTreeView::customContextMenuRequested, + [&](const QPoint& p){ m_tab->showContextMenu(p, ui->noConflictTree); }); +} + +void GeneralConflictsTab::clear() +{ + m_overwriteModel->clear(); + m_overwrittenModel->clear(); + m_noConflictModel->clear(); + + ui->overwriteCount->display(0); + ui->overwrittenCount->display(0); + ui->noConflictCount->display(0); +} + +void GeneralConflictsTab::saveState(Settings& s) +{ + QByteArray result; + QDataStream stream(&result, QIODevice::WriteOnly); + + stream + << m_expanders.overwrite.opened() + << m_expanders.overwritten.opened() + << m_expanders.nonconflict.opened(); + + s.directInterface().setValue( + "mod_info_conflicts_general_expanders", result); + + s.directInterface().setValue( + "mod_info_conflicts_general_overwrite", + ui->overwriteTree->header()->saveState()); + + s.directInterface().setValue( + "mod_info_conflicts_general_noconflict", + ui->noConflictTree->header()->saveState()); + + s.directInterface().setValue( + "mod_info_conflicts_general_overwritten", + ui->overwrittenTree->header()->saveState()); +} + +void GeneralConflictsTab::restoreState(const Settings& s) +{ + QDataStream stream(s.directInterface() + .value("mod_info_conflicts_general_expanders").toByteArray()); + + bool overwriteExpanded = false; + bool overwrittenExpanded = false; + bool noConflictExpanded = false; + + stream >> overwriteExpanded >> overwrittenExpanded >> noConflictExpanded; + + if (stream.status() == QDataStream::Ok) { + m_expanders.overwrite.toggle(overwriteExpanded); + m_expanders.overwritten.toggle(overwrittenExpanded); + m_expanders.nonconflict.toggle(noConflictExpanded); + } + + ui->overwriteTree->header()->restoreState(s.directInterface() + .value("mod_info_conflicts_general_overwrite").toByteArray()); + + ui->noConflictTree->header()->restoreState(s.directInterface() + .value("mod_info_conflicts_general_noconflict").toByteArray()); + + ui->overwrittenTree->header()->restoreState(s.directInterface() + .value("mod_info_conflicts_general_overwritten").toByteArray()); +} + +bool GeneralConflictsTab::update() +{ + clear(); + + int numNonConflicting = 0; + int numOverwrite = 0; + int numOverwritten = 0; + + if (m_tab->origin() != nullptr) { + const auto rootPath = m_tab->mod().absolutePath(); + + for (const auto& file : m_tab->origin()->getFiles()) { + // careful: these two strings are moved into createXItem() below + QString relativeName = QDir::fromNativeSeparators(ToQString(file->getRelativePath())); + QString fileName = rootPath + relativeName; + + bool archive = false; + const int fileOrigin = file->getOrigin(archive); + const auto& alternatives = file->getAlternatives(); + + if (fileOrigin == m_tab->origin()->getID()) { + if (!alternatives.empty()) { + m_overwriteModel->add(createOverwriteItem( + file->getIndex(), archive, + std::move(fileName), std::move(relativeName), alternatives)); + + ++numOverwrite; + } else { + // otherwise, put the file in the noconflict tree + m_noConflictModel->add(createNoConflictItem( + file->getIndex(), archive, + std::move(fileName), std::move(relativeName))); + + ++numNonConflicting; + } + } else { + m_overwrittenModel->add(createOverwrittenItem( + file->getIndex(), fileOrigin, archive, + std::move(fileName), std::move(relativeName))); + + ++numOverwritten; + } + } + + m_overwriteModel->finished(); + m_overwrittenModel->finished(); + m_noConflictModel->finished(); + } + + ui->overwriteCount->display(numOverwrite); + ui->overwrittenCount->display(numOverwritten); + ui->noConflictCount->display(numNonConflicting); + + return (numOverwrite > 0 || numOverwritten > 0); +} + +ConflictItem GeneralConflictsTab::createOverwriteItem( + FileEntry::Index index, bool archive, QString fileName, QString relativeName, + const FileEntry::AlternativesVector& alternatives) +{ + const auto& ds = *m_core.directoryStructure(); + std::wstring altString; + + for (const auto& alt : alternatives) { + if (!altString.empty()) { + altString += L", "; + } + + altString += ds.getOriginByID(alt.first).getName(); + } + + auto origin = ToQString(ds.getOriginByID(alternatives.back().first).getName()); + + return ConflictItem( + ToQString(altString), std::move(relativeName), QString::null, index, + std::move(fileName), true, std::move(origin), archive); +} + +ConflictItem GeneralConflictsTab::createNoConflictItem( + FileEntry::Index index, bool archive, QString fileName, QString relativeName) +{ + return ConflictItem( + QString::null, std::move(relativeName), QString::null, index, + std::move(fileName), false, QString::null, archive); +} + +ConflictItem GeneralConflictsTab::createOverwrittenItem( + FileEntry::Index index, int fileOrigin, bool archive, + QString fileName, QString relativeName) +{ + const auto& ds = *m_core.directoryStructure(); + const FilesOrigin& realOrigin = ds.getOriginByID(fileOrigin); + + QString after = ToQString(realOrigin.getName()); + QString altOrigin = after; + + return ConflictItem( + QString::null, std::move(relativeName), std::move(after), + index, std::move(fileName), true, std::move(altOrigin), archive); +} + +void GeneralConflictsTab::onOverwriteActivated(const QModelIndex& index) +{ + auto* model = dynamic_cast<ConflictListModel*>(ui->overwriteTree->model()); + if (!model) { + return; + } + + auto* item = model->getItem(static_cast<std::size_t>(index.row())); + if (!item) { + return; + } + + const auto origin = item->altOrigin(); + if (!origin.isEmpty()) { + emit modOpen(origin); + } +} + +void GeneralConflictsTab::onOverwrittenActivated(const QModelIndex& index) +{ + auto* model = dynamic_cast<ConflictListModel*>(ui->overwrittenTree->model()); + if (!model) { + return; + } + + auto* item = model->getItem(static_cast<std::size_t>(index.row())); + if (!item) { + return; + } + + const auto origin = item->altOrigin(); + if (!origin.isEmpty()) { + emit modOpen(origin); + } +} + + +AdvancedConflictsTab::AdvancedConflictsTab( + ConflictsTab* tab, Ui::ModInfoDialog* pui, OrganizerCore& oc) : + m_tab(tab), ui(pui), m_core(oc), + m_model(new AdvancedConflictListModel(ui->conflictsAdvancedList)) +{ + // left-elide the overwrites column so that the nearest are visible + ui->conflictsAdvancedList->setItemDelegateForColumn( + 0, new ElideLeftDelegate(ui->conflictsAdvancedList)); + + // left-elide the file column to see filenames + ui->conflictsAdvancedList->setItemDelegateForColumn( + 1, new ElideLeftDelegate(ui->conflictsAdvancedList)); + + // don't elide the overwritten by column so that the nearest are visible + + QObject::connect( + ui->conflictsAdvancedShowNoConflict, &QCheckBox::clicked, + [&]{ update(); }); + + QObject::connect( + ui->conflictsAdvancedShowAll, &QRadioButton::clicked, + [&]{ update(); }); + + QObject::connect( + ui->conflictsAdvancedShowNearest, &QRadioButton::clicked, + [&]{ update(); }); + + QObject::connect( + ui->conflictsAdvancedList, &QTreeView::customContextMenuRequested, + [&](const QPoint& p){ m_tab->showContextMenu(p, ui->conflictsAdvancedList); }); + + m_filter.setEdit(ui->conflictsAdvancedFilter); + QObject::connect(&m_filter, &FilterWidget::changed, [&]{ update(); }); +} + +void AdvancedConflictsTab::clear() +{ + m_model->clear(); +} + +void AdvancedConflictsTab::saveState(Settings& s) +{ + s.directInterface().setValue( + "mod_info_conflicts_advanced_list", + ui->conflictsAdvancedList->header()->saveState()); + + QByteArray result; + QDataStream stream(&result, QIODevice::WriteOnly); + + stream + << ui->conflictsAdvancedShowNoConflict->isChecked() + << ui->conflictsAdvancedShowAll->isChecked() + << ui->conflictsAdvancedShowNearest->isChecked(); + + s.directInterface().setValue( + "mod_info_conflicts_advanced_options", result); +} + +void AdvancedConflictsTab::restoreState(const Settings& s) +{ + ui->conflictsAdvancedList->header()->restoreState( + s.directInterface().value("mod_info_conflicts_advanced_list").toByteArray()); + + QDataStream stream(s.directInterface() + .value("mod_info_conflicts_advanced_options").toByteArray()); + + bool noConflictChecked = false; + bool showAllChecked = false; + bool showNearestChecked = false; + + stream >> noConflictChecked >> showAllChecked >> showNearestChecked; + + if (stream.status() == QDataStream::Ok) { + ui->conflictsAdvancedShowNoConflict->setChecked(noConflictChecked); + ui->conflictsAdvancedShowAll->setChecked(showAllChecked); + ui->conflictsAdvancedShowNearest->setChecked(showNearestChecked); + } +} + +void AdvancedConflictsTab::update() +{ + clear(); + + if (m_tab->origin() != nullptr) { + const auto rootPath = m_tab->mod().absolutePath(); + + const auto& files = m_tab->origin()->getFiles(); + m_model->reserve(files.size()); + + for (const auto& file : files) { + // careful: these two strings are moved into createItem() below + QString relativeName = QDir::fromNativeSeparators(ToQString(file->getRelativePath())); + QString fileName = rootPath + relativeName; + + bool archive = false; + const int fileOrigin = file->getOrigin(archive); + const auto& alternatives = file->getAlternatives(); + + auto item = createItem( + file->getIndex(), fileOrigin, archive, + std::move(fileName), std::move(relativeName), alternatives); + + if (item) { + m_model->add(std::move(*item)); + } + } + + m_model->finished(); + } +} + +std::optional<ConflictItem> AdvancedConflictsTab::createItem( + FileEntry::Index index, int fileOrigin, bool archive, + QString fileName, QString relativeName, + const MOShared::FileEntry::AlternativesVector& alternatives) +{ + const auto& ds = *m_core.directoryStructure(); + + std::wstring before, after; + + if (!alternatives.empty()) { + const bool showAllAlts = ui->conflictsAdvancedShowAll->isChecked(); + + int beforePrio = 0; + int afterPrio = std::numeric_limits<int>::max(); + + for (const auto& alt : alternatives) + { + const auto& altOrigin = ds.getOriginByID(alt.first); + + if (showAllAlts) { + // fills 'before' and 'after' with all the alternatives that come + // before and after this mod in terms of priority + + if (altOrigin.getPriority() < m_tab->origin()->getPriority()) { + // add all the mods having a lower priority than this one + if (!before.empty()) { + before += L", "; + } + + before += altOrigin.getName(); + } else if (altOrigin.getPriority() > m_tab->origin()->getPriority()) { + // add all the mods having a higher priority than this one + if (!after.empty()) { + after += L", "; + } + + after += altOrigin.getName(); + } + } else { + // keep track of the nearest mods that come before and after this one + // in terms of priority + + if (altOrigin.getPriority() < m_tab->origin()->getPriority()) { + // the alternative has a lower priority than this mod + + if (altOrigin.getPriority() > beforePrio) { + // the alternative has a higher priority and therefore is closer + // to this mod, use it + before = altOrigin.getName(); + beforePrio = altOrigin.getPriority(); + } + } + + if (altOrigin.getPriority() > m_tab->origin()->getPriority()) { + // the alternative has a higher priority than this mod + + if (altOrigin.getPriority() < afterPrio) { + // the alternative has a lower priority and there is closer + // to this mod, use it + after = altOrigin.getName(); + afterPrio = altOrigin.getPriority(); + } + } + } + } + + // the primary origin is never in the list of alternatives, so it has to + // be handled separately + // + // if 'after' is not empty, it means at least one alternative with a higher + // priority than this mod was found; if the user only wants to see the + // nearest mods, it's not worth checking for the primary origin because it + // will always have a higher priority than the alternatives (or it wouldn't + // be the primary) + if (after.empty() || showAllAlts) { + const FilesOrigin& realOrigin = ds.getOriginByID(fileOrigin); + + // if no mods overwrite this file, the primary origin is the same as this + // mod, so ignore that + if (realOrigin.getID() != m_tab->origin()->getID()) { + if (!after.empty()) { + after += L", "; + } + + after += realOrigin.getName(); + } + } + } + + const bool hasAlts = !before.empty() || !after.empty(); + + if (!hasAlts) { + // if both before and after are empty, it means this file has no conflicts + // at all, only display it if the user wants it + if (!ui->conflictsAdvancedShowNoConflict->isChecked()) { + return {}; + } + } + + auto beforeQS = QString::fromStdWString(before); + auto afterQS = QString::fromStdWString(after); + + const bool matched = m_filter.matches([&](auto&& what) { + return + beforeQS.contains(what, Qt::CaseInsensitive) || + relativeName.contains(what, Qt::CaseInsensitive) || + afterQS.contains(what, Qt::CaseInsensitive); + }); + + if (!matched) { + return {}; + } + + return ConflictItem( + std::move(beforeQS), std::move(relativeName), std::move(afterQS), + index, std::move(fileName), hasAlts, QString::null, archive); +} diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h new file mode 100644 index 00000000..a77c2ac9 --- /dev/null +++ b/src/modinfodialogconflicts.h @@ -0,0 +1,142 @@ +#ifndef MODINFODIALOGCONFLICTS_H +#define MODINFODIALOGCONFLICTS_H + +#include "modinfodialogtab.h" +#include "expanderwidget.h" +#include "filterwidget.h" +#include "directoryentry.h" +#include <QTreeWidget> +#include <optional> + +class ConflictsTab; +class OrganizerCore; +class ConflictItem; +class ConflictListModel; + +class GeneralConflictsTab : public QObject +{ + Q_OBJECT; + +public: + GeneralConflictsTab( + ConflictsTab* tab, Ui::ModInfoDialog* ui, OrganizerCore& oc); + + void clear(); + void saveState(Settings& s); + void restoreState(const Settings& s); + + bool update(); + +signals: + void modOpen(QString name); + +private: + struct Expanders + { + ExpanderWidget overwrite, overwritten, nonconflict; + }; + + ConflictsTab* m_tab; + Ui::ModInfoDialog* ui; + OrganizerCore& m_core; + Expanders m_expanders; + + ConflictListModel* m_overwriteModel; + ConflictListModel* m_overwrittenModel; + ConflictListModel* m_noConflictModel; + + ConflictItem createOverwriteItem( + MOShared::FileEntry::Index index, bool archive, + QString fileName, QString relativeName, + const MOShared::FileEntry::AlternativesVector& alternatives); + + ConflictItem createNoConflictItem( + MOShared::FileEntry::Index index, bool archive, + QString fileName, QString relativeName); + + ConflictItem createOverwrittenItem( + MOShared::FileEntry::Index index, int fileOrigin, bool archive, + QString fileName, QString relativeName); + + void onOverwriteActivated(const QModelIndex& index); + void onOverwrittenActivated(const QModelIndex& index); + + void onOverwriteTreeContext(const QPoint &pos); + void onOverwrittenTreeContext(const QPoint &pos); + void onNoConflictTreeContext(const QPoint &pos); +}; + + +class AdvancedConflictsTab : public QObject +{ + Q_OBJECT; + +public: + AdvancedConflictsTab( + ConflictsTab* tab, Ui::ModInfoDialog* ui, OrganizerCore& oc); + + void clear(); + void saveState(Settings& s); + void restoreState(const Settings& s); + + void update(); + +signals: + void modOpen(QString name); + +private: + ConflictsTab* m_tab; + Ui::ModInfoDialog* ui; + OrganizerCore& m_core; + FilterWidget m_filter; + ConflictListModel* m_model; + + std::optional<ConflictItem> createItem( + MOShared::FileEntry::Index index, int fileOrigin, bool archive, + QString fileName, QString relativeName, + const MOShared::FileEntry::AlternativesVector& alternatives); +}; + + +class ConflictsTab : public ModInfoDialogTab +{ + Q_OBJECT; + +public: + ConflictsTab(ModInfoDialogTabContext cx); + + void update() override; + void clear() override; + void saveState(Settings& s) override; + void restoreState(const Settings& s) override; + bool canHandleUnmanaged() const override; + + void openItems(QTreeView* tree); + void previewItems(QTreeView* tree); + void exploreItems(QTreeView* tree); + + void changeItemsVisibility(QTreeView* tree, bool visible); + + void showContextMenu(const QPoint &pos, QTreeView* tree); + +private: + struct Actions + { + QAction* hide = nullptr; + QAction* unhide = nullptr; + QAction* open = nullptr; + QAction* preview = nullptr; + QAction* explore = nullptr; + QMenu* gotoMenu = nullptr; + + std::vector<QAction*> gotoActions; + }; + + GeneralConflictsTab m_general; + AdvancedConflictsTab m_advanced; + + Actions createMenuActions(QTreeView* tree); + std::vector<QAction*> createGotoActions(const ConflictItem* item); +}; + +#endif // MODINFODIALOGCONFLICTS_H diff --git a/src/modinfodialogesps.cpp b/src/modinfodialogesps.cpp new file mode 100644 index 00000000..fba5d39a --- /dev/null +++ b/src/modinfodialogesps.cpp @@ -0,0 +1,399 @@ +#include "modinfodialogesps.h" +#include "ui_modinfodialog.h" +#include "modinfodialog.h" +#include "settings.h" +#include <report.h> + +using MOBase::reportError; + +class ESPItem +{ +public: + ESPItem(QString rootPath, QString relativePath) + : m_rootPath(std::move(rootPath)), m_active(false) + { + if (relativePath.contains('/') || relativePath.contains('\\')) { + m_inactivePath = relativePath; + } else { + m_activePath = relativePath; + m_active = true; + } + + pathChanged(); + } + + const QString& rootPath() const + { + return m_rootPath; + } + + const QString& relativePath() const + { + if (m_active) { + return m_activePath; + } else { + return m_inactivePath; + } + } + + const QString& filename() const + { + return m_filename; + } + + const QString& activePath() const + { + return m_activePath; + } + + const QString& inactivePath() const + { + return m_inactivePath; + } + + const QFileInfo& fileInfo() const + { + return m_fileInfo; + } + + bool isActive() const + { + return m_active; + } + + bool activate(const QString& newName) + { + QDir root(m_rootPath); + + if (root.rename(m_inactivePath, newName)) { + m_active = true; + m_activePath = newName; + + if (QFileInfo(m_inactivePath).fileName() != newName) { + // file was renamed + m_inactivePath = QFileInfo(m_inactivePath).path() + QDir::separator() + newName; + } + + pathChanged(); + + return true; + } + + return false; + } + + bool deactivate(const QString& newName) + { + QDir root(m_rootPath); + + if (root.rename(m_activePath, newName)) { + m_active = false; + m_inactivePath = newName; + pathChanged(); + return true; + } + + return false; + } + +private: + QString m_rootPath; + QString m_activePath; + QString m_inactivePath; + QString m_filename; + QFileInfo m_fileInfo; + bool m_active; + + void pathChanged() + { + m_fileInfo.setFile(m_rootPath + QDir::separator() + relativePath()); + m_filename = m_fileInfo.fileName(); + } +}; + + +class ESPListModel : public QAbstractItemModel +{ +public: + void clear() + { + m_esps.clear(); + endResetModel(); + } + + QModelIndex index(int row, int col, const QModelIndex& ={}) const override + { + return createIndex(row, col); + } + + QModelIndex parent(const QModelIndex&) const override + { + return {}; + } + + int rowCount(const QModelIndex& ={}) const override + { + return static_cast<int>(m_esps.size()); + } + + int columnCount(const QModelIndex& ={}) const override + { + return 1; + } + + QVariant data(const QModelIndex& index, int role) const override + { + if (role == Qt::DisplayRole) { + if (auto* esp=getESP(index)) { + return esp->filename(); + } + } + + return {}; + } + + + void add(ESPItem esp) + { + m_esps.emplace_back(std::move(esp)); + } + + void addOne(ESPItem esp) + { + const auto i = m_esps.size(); + + beginInsertRows({}, static_cast<int>(i), static_cast<int>(i)); + add(std::move(esp)); + endInsertRows(); + } + + bool removeRows(int row, int count, const QModelIndex& = {}) override + { + if (row < 0) { + return false; + } + + const auto start = static_cast<std::size_t>(row); + if (start >= m_esps.size()) { + return false; + } + + const auto end = std::min( + start + static_cast<std::size_t>(count), + m_esps.size()); + + beginRemoveRows({}, static_cast<int>(start), static_cast<int>(end)); + m_esps.erase(m_esps.begin() + start, m_esps.begin() + end); + endRemoveRows(); + + return true; + } + + void finished() + { + std::sort(m_esps.begin(), m_esps.end(), [](const auto& a, const auto& b) { + return (naturalCompare(a.filename(), b.filename()) < 0); + }); + + endResetModel(); + } + + const ESPItem* getESP(const QModelIndex& index) const + { + const auto row = index.row(); + if (row < 0) { + return nullptr; + } + + const auto i = static_cast<std::size_t>(row); + if (i >= m_esps.size()) { + return nullptr; + } + + return &m_esps[i]; + } + + ESPItem* getESP(const QModelIndex& index) + { + return const_cast<ESPItem*>(std::as_const(*this).getESP(index)); + } + +private: + std::deque<ESPItem> m_esps; +}; + + + +ESPsTab::ESPsTab(ModInfoDialogTabContext cx) : + ModInfoDialogTab(std::move(cx)), + m_inactiveModel(new ESPListModel), m_activeModel(new ESPListModel) +{ + ui->inactiveESPList->setModel(m_inactiveModel); + ui->activeESPList->setModel(m_activeModel); + + QObject::connect( + ui->activateESP, &QToolButton::clicked, [&]{ onActivate(); }); + + QObject::connect( + ui->deactivateESP, &QToolButton::clicked, [&]{ onDeactivate(); }); +} + +void ESPsTab::clear() +{ + m_inactiveModel->clear(); + m_activeModel->clear(); + setHasData(false); +} + +bool ESPsTab::feedFile(const QString& rootPath, const QString& fullPath) +{ + static const QString extensions[] = {".esp", ".esm", ".esl"}; + + for (const auto& e : extensions) { + if (fullPath.endsWith(e, Qt::CaseInsensitive)) { + ESPItem esp(rootPath, fullPath.mid(rootPath.length() + 1)); + + if (esp.isActive()) { + m_activeModel->add(std::move(esp)); + } else { + m_inactiveModel->add(std::move(esp)); + } + + return true; + } + } + + return false; +} + +void ESPsTab::update() +{ + m_inactiveModel->finished(); + m_activeModel->finished(); + + setHasData(m_inactiveModel->rowCount() > 0 || m_activeModel->rowCount() > 0); +} + +void ESPsTab::saveState(Settings& s) +{ + saveWidgetState(s.directInterface(), ui->ESPsSplitter); +} + +void ESPsTab::restoreState(const Settings& s) +{ + restoreWidgetState(s.directInterface(), ui->ESPsSplitter); +} + +void ESPsTab::onActivate() +{ + const auto index = ui->inactiveESPList->currentIndex(); + if (!index.isValid()) { + return; + } + + auto* esp = m_inactiveModel->getESP(index); + if (!esp) { + return; + } + + if (esp->isActive()) { + qWarning("ESPsTab::onActive(): item is already active"); + return; + } + + QDir root(esp->rootPath()); + const QFileInfo file(esp->fileInfo()); + + QString newName = file.fileName(); + + while (root.exists(newName)) { + bool okClicked = false; + + newName = QInputDialog::getText( + parentWidget(), + QObject::tr("File Exists"), + QObject::tr("A file with that name exists, please enter a new one"), + QLineEdit::Normal, file.fileName(), &okClicked); + + if (!okClicked) { + return; + } + + if (newName.isEmpty()) { + newName = file.fileName(); + } + } + + if (esp->activate(newName)) { + // copy esp, original will be destroyed + auto copy = *esp; + m_inactiveModel->removeRow(index.row()); + m_activeModel->addOne(std::move(copy)); + selectRow(ui->inactiveESPList, index.row()); + } else { + reportError(QObject::tr("Failed to move file")); + } +} + +void ESPsTab::onDeactivate() +{ + const auto index = ui->activeESPList->currentIndex(); + if (!index.isValid()) { + return; + } + + auto* esp = m_activeModel->getESP(index); + if (!esp) { + return; + } + + if (!esp->isActive()) { + qWarning("ESPsTab::onDeactivate(): item is already inactive"); + return; + } + + QDir root(esp->rootPath()); + + // if we moved the file from optional to active in this session, we move the + // file back to where it came from. Otherwise, it is moved to the new folder + // "optional" + + QString newName = esp->inactivePath(); + + if (newName.isEmpty()) { + if (!root.exists("optional")) { + if (!root.mkdir("optional")) { + reportError(QObject::tr("Failed to create directory \"optional\"")); + return; + } + } + + newName = QString("optional") + QDir::separator() + esp->fileInfo().fileName(); + } + + if (esp->deactivate(newName)) { + // copy esp, original will be destroyed + auto copy = *esp; + + m_activeModel->removeRow(index.row()); + m_inactiveModel->addOne(std::move(copy)); + selectRow(ui->activeESPList, index.row()); + } else { + reportError(QObject::tr("Failed to move file")); + } +} + +void ESPsTab::selectRow(QListView* list, int row) +{ + const auto* model = list->model(); + const auto count = model->rowCount(); + if (count == 0) { + return; + } + + if (row >= count) { + list->setCurrentIndex(model->index(count - 1, 0)); + } else { + list->setCurrentIndex(model->index(row, 0)); + } +} diff --git a/src/modinfodialogesps.h b/src/modinfodialogesps.h new file mode 100644 index 00000000..b128f279 --- /dev/null +++ b/src/modinfodialogesps.h @@ -0,0 +1,31 @@ +#ifndef MODINFODIALOGESPS_H +#define MODINFODIALOGESPS_H + +#include "modinfodialogtab.h" + +class ESPItem; +class ESPListModel; + +class ESPsTab : public ModInfoDialogTab +{ + Q_OBJECT; + +public: + ESPsTab(ModInfoDialogTabContext cx); + + void clear() override; + bool feedFile(const QString& rootPath, const QString& fullPath) override; + void update(); + void saveState(Settings& s) override; + void restoreState(const Settings& s) override; + +private: + ESPListModel* m_inactiveModel; + ESPListModel* m_activeModel; + + void onActivate(); + void onDeactivate(); + void selectRow(QListView* list, int row); +}; + +#endif // MODINFODIALOGESPS_H diff --git a/src/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp new file mode 100644 index 00000000..0b519932 --- /dev/null +++ b/src/modinfodialogfiletree.cpp @@ -0,0 +1,443 @@ +#include "modinfodialogfiletree.h" +#include "ui_modinfodialog.h" +#include "modinfodialog.h" +#include "organizercore.h" +#include "filerenamer.h" +#include <utility.h> +#include <report.h> + +using MOBase::reportError; +namespace shell = MOBase::shell; + +// if there are more than 50 selected items in the filetree, don't bother +// checking whether menu items apply to them, just show all of them +const int max_scan_for_context_menu = 50; + +FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx) + : ModInfoDialogTab(std::move(cx)), m_fs(nullptr) +{ + m_fs = new QFileSystemModel(this); + m_fs->setReadOnly(false); + ui->filetree->setModel(m_fs); + ui->filetree->setColumnWidth(0, 300); + + m_actions.newFolder = new QAction(tr("&New Folder"), ui->filetree); + m_actions.open = new QAction(tr("&Open/Execute"), ui->filetree); + m_actions.preview = new QAction(tr("&Preview"), ui->filetree); + m_actions.explore = new QAction(tr("Open in &Explorer"), ui->filetree); + m_actions.rename = new QAction(tr("&Rename"), ui->filetree); + m_actions.del = new QAction(tr("&Delete"), ui->filetree); + m_actions.hide = new QAction(tr("&Hide"), ui->filetree); + m_actions.unhide = new QAction(tr("&Unhide"), ui->filetree); + + connect(m_actions.newFolder, &QAction::triggered, [&]{ onCreateDirectory(); }); + connect(m_actions.open, &QAction::triggered, [&]{ onOpen(); }); + connect(m_actions.preview, &QAction::triggered, [&]{ onPreview(); }); + connect(m_actions.explore, &QAction::triggered, [&]{ onExplore(); }); + connect(m_actions.rename, &QAction::triggered, [&]{ onRename(); }); + connect(m_actions.del, &QAction::triggered, [&]{ onDelete(); }); + connect(m_actions.hide, &QAction::triggered, [&]{ onHide(); }); + connect(m_actions.unhide, &QAction::triggered, [&]{ onUnhide(); }); + + connect(ui->openInExplorer, &QToolButton::clicked, [&]{ onOpenInExplorer(); }); + + connect( + ui->filetree, &QTreeView::customContextMenuRequested, + [&](const QPoint& pos){ onContextMenu(pos); }); +} + +void FileTreeTab::clear() +{ + m_fs->setRootPath({}); + + // always has data; even if the mod is empty, it still has a meta.ini + setHasData(true); +} + +void FileTreeTab::update() +{ + const auto rootPath = mod().absolutePath(); + + m_fs->setRootPath(rootPath); + ui->filetree->setRootIndex(m_fs->index(rootPath)); +} + +bool FileTreeTab::deleteRequested() +{ + if (!ui->filetree->hasFocus()) { + return false; + } + + onDelete(); + return true; +} + +QModelIndex FileTreeTab::singleSelection() const +{ + const auto rows = ui->filetree->selectionModel()->selectedRows(); + if (rows.size() != 1) { + return {}; + } + + return rows[0]; +} + +void FileTreeTab::onCreateDirectory() +{ + const auto selectedRows = ui->filetree->selectionModel()->selectedRows(); + if (selectedRows.size() > 1) { + return; + } + + QModelIndex selection; + + if (selectedRows.size() == 0) { + selection = m_fs->index(m_fs->rootPath(), 0); + } else { + selection = selectedRows[0]; + } + + QModelIndex index = m_fs->isDir(selection) ? selection : selection.parent(); + index = index.sibling(index.row(), 0); + + QString name = tr("New Folder"); + QString path = m_fs->filePath(index).append("/"); + + QModelIndex existingIndex = m_fs->index(path + name); + int suffix = 1; + while (existingIndex.isValid()) { + name = tr("New Folder") + QString::number(suffix++); + existingIndex = m_fs->index(path + name); + } + + QModelIndex newIndex = m_fs->mkdir(index, name); + if (!newIndex.isValid()) { + reportError(tr("Failed to create \"%1\"").arg(name)); + return; + } + + ui->filetree->setCurrentIndex(newIndex); + ui->filetree->edit(newIndex); +} + +void FileTreeTab::onOpen() +{ + auto selection = singleSelection(); + if (!selection.isValid()) { + return; + } + + shell::OpenFile(m_fs->filePath(selection)); +} + +void FileTreeTab::onPreview() +{ + auto selection = singleSelection(); + if (!selection.isValid()) { + return; + } + + core().previewFile(parentWidget(), mod().name(), m_fs->filePath(selection)); +} + +void FileTreeTab::onExplore() +{ + auto selection = singleSelection(); + + if (selection.isValid()) { + shell::ExploreFile(m_fs->filePath(selection)); + } else { + shell::ExploreFile(mod().absolutePath()); + } +} + +void FileTreeTab::onRename() +{ + auto selection = singleSelection(); + if (!selection.isValid()) { + return; + } + + QModelIndex index = selection.sibling(selection.row(), 0); + if (!index.isValid() || m_fs->isReadOnly()) { + return; + } + + ui->filetree->edit(index); +} + +void FileTreeTab::onDelete() +{ + const auto rows = ui->filetree->selectionModel()->selectedRows(); + if (rows.count() == 0) { + return; + } + + QString message; + + if (rows.count() == 1) { + QString fileName = m_fs->fileName(rows[0]); + message = tr("Are you sure you want to delete \"%1\"?").arg(fileName); + } else { + message = tr("Are you sure you want to delete the selected files?"); + } + + if (QMessageBox::question(parentWidget(), tr("Confirm"), message) != QMessageBox::Yes) { + return; + } + + for (const auto& index : rows) { + deleteFile(index); + } +} + +void FileTreeTab::onHide() +{ + changeVisibility(false); +} + +void FileTreeTab::onUnhide() +{ + changeVisibility(true); +} + +void FileTreeTab::onOpenInExplorer() +{ + shell::ExploreFile(mod().absolutePath()); +} + +bool FileTreeTab::deleteFile(const QModelIndex& index) +{ + bool res = false; + + if (m_fs->isDir(index)) { + res = deleteFileRecursive(index); + } else { + res = m_fs->remove(index); + } + + if (!res) { + reportError(tr("Failed to delete %1").arg(m_fs->fileName(index))); + } + + return res; +} + +bool FileTreeTab::deleteFileRecursive(const QModelIndex& parent) +{ + for (int row = 0; row<m_fs->rowCount(parent); ++row) { + QModelIndex index = m_fs->index(row, 0, parent); + + if (m_fs->isDir(index)) { + if (!deleteFileRecursive(index)) { + qCritical() << "failed to delete" << m_fs->fileName(index); + return false; + } + } else { + if (!m_fs->remove(index)) { + qCritical() << "failed to delete", m_fs->fileName(index); + return false; + } + } + } + + if (!m_fs->remove(parent)) { + qCritical() << "failed to delete" << m_fs->fileName(parent); + return false; + } + + return true; +} + +void FileTreeTab::changeVisibility(bool visible) +{ + const auto selection = ui->filetree->selectionModel()->selectedRows(); + + bool changed = false; + bool stop = false; + + qDebug().nospace() + << (visible ? "unhiding" : "hiding") << " " + << selection.size() << " filetree files"; + + QFlags<FileRenamer::RenameFlags> flags = + (visible ? FileRenamer::UNHIDE : FileRenamer::HIDE); + + if (selection.size() > 1) { + flags |= FileRenamer::MULTIPLE; + } + + FileRenamer renamer(parentWidget(), flags); + + for (const auto& index : selection) { + if (stop) { + break; + } + + const QString path = m_fs->filePath(index); + auto result = FileRenamer::RESULT_CANCEL; + + if (visible) { + if (!canUnhideFile(false, path)) { + qDebug().nospace() << "cannot unhide " << path << ", skipping"; + continue; + } + result = unhideFile(renamer, path); + } else { + if (!canHideFile(false, path)) { + qDebug().nospace() << "cannot hide " << path << ", skipping"; + continue; + } + result = hideFile(renamer, path); + } + + switch (result) { + case FileRenamer::RESULT_OK: { + // will trigger a refresh at the end + changed = true; + break; + } + + case FileRenamer::RESULT_SKIP: { + // nop + break; + } + + case FileRenamer::RESULT_CANCEL: { + // stop right now, but make sure to refresh if needed + stop = true; + break; + } + } + } + + qDebug().nospace() << (visible ? "unhiding" : "hiding") << " filetree files done"; + + if (changed) { + if (origin()) { + emitOriginModified(); + } + } +} + +void FileTreeTab::onContextMenu(const QPoint &pos) +{ + const auto selection = ui->filetree->selectionModel()->selectedRows(); + + QMenu menu(ui->filetree); + + bool enableNewFolder = true; + bool enableOpen = true; + bool enablePreview = true; + bool enableExplore = true; + bool enableRename = true; + bool enableDelete = true; + bool enableHide = true; + bool enableUnhide = true; + + if (selection.size() == 0) { + // no selection, only new folder and explore + enableOpen = false; + enablePreview = false; + enableRename = false; + enableDelete = false; + enableHide = false; + enableUnhide = false; + } else if (selection.size() == 1) { + // single selection + + // only enable open action if a file is selected + bool hasFiles = false; + + for (auto index : selection) { + if (m_fs->fileInfo(index).isFile()) { + hasFiles = true; + break; + } + } + + if (!hasFiles) { + enableOpen = false; + enablePreview = false; + } + + const QString fileName = m_fs->fileName(selection[0]); + + if (!canPreviewFile(plugin(), false, fileName)) { + enablePreview = false; + } + + if (!canExploreFile(false, fileName)) { + enableExplore = false; + } + + if (!canHideFile(false, fileName)) { + enableHide = false; + } + + if (!canUnhideFile(false, fileName)) { + enableUnhide = false; + } + } else { + // this is a multiple selection, don't show open action so users don't open + // a thousand files + enableOpen = false; + enablePreview = false; + + // can't explore multiple files + enableExplore = false; + + // can't rename multiple files + enableRename = false; + + if (selection.size() < max_scan_for_context_menu) { + // if the number of selected items is low, checking them to accurately + // show the menu items is worth it + enableHide = false; + enableUnhide = false; + + for (const auto& index : selection) { + const QString fileName = m_fs->fileName(index); + + if (canHideFile(false, fileName)) { + enableHide = true; + } + + if (canUnhideFile(false, fileName)) { + enableUnhide = true; + } + + if (enableHide && enableUnhide) { + // found both, no need to check more + break; + } + } + } + } + + menu.addAction(m_actions.newFolder); + m_actions.newFolder->setEnabled(enableNewFolder); + + menu.addAction(m_actions.open); + m_actions.open->setEnabled(enableOpen); + + menu.addAction(m_actions.preview); + m_actions.preview->setEnabled(enablePreview); + + menu.addAction(m_actions.explore); + m_actions.explore->setEnabled(enableExplore); + + menu.addAction(m_actions.rename); + m_actions.rename->setEnabled(enableRename); + + menu.addAction(m_actions.del); + m_actions.del->setEnabled(enableDelete); + + menu.addAction(m_actions.hide); + m_actions.hide->setEnabled(enableHide); + + menu.addAction(m_actions.unhide); + m_actions.unhide->setEnabled(enableUnhide); + + menu.exec(ui->filetree->viewport()->mapToGlobal(pos)); +} diff --git a/src/modinfodialogfiletree.h b/src/modinfodialogfiletree.h new file mode 100644 index 00000000..f9fa62d4 --- /dev/null +++ b/src/modinfodialogfiletree.h @@ -0,0 +1,48 @@ +#ifndef MODINFODIALOGFILETREE_H +#define MODINFODIALOGFILETREE_H + +#include "modinfodialogtab.h" + +class FileTreeTab : public ModInfoDialogTab +{ +public: + FileTreeTab(ModInfoDialogTabContext cx); + + void clear() override; + void update() override; + bool deleteRequested() override; + +private: + struct Actions + { + QAction *newFolder = nullptr; + QAction *open = nullptr; + QAction *preview = nullptr; + QAction *explore = nullptr; + QAction *rename = nullptr; + QAction *del = nullptr; + QAction *hide = nullptr; + QAction *unhide = nullptr; + }; + + QFileSystemModel* m_fs; + Actions m_actions; + + void onCreateDirectory(); + void onOpen(); + void onPreview(); + void onExplore(); + void onRename(); + void onDelete(); + void onHide(); + void onUnhide(); + void onOpenInExplorer(); + void onContextMenu(const QPoint &pos); + + QModelIndex singleSelection() const; + bool deleteFile(const QModelIndex& index); + bool deleteFileRecursive(const QModelIndex& index); + void changeVisibility(bool visible); +}; + +#endif // MODINFODIALOGFILETREE_H diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h new file mode 100644 index 00000000..9ede766f --- /dev/null +++ b/src/modinfodialogfwd.h @@ -0,0 +1,50 @@ +#ifndef MODINFODIALOGFWD_H +#define MODINFODIALOGFWD_H + +#include "filerenamer.h" + +class ModInfo; +using ModInfoPtr = QSharedPointer<ModInfo>; + +enum class ModInfoTabIDs +{ + None = -1, + TextFiles = 0, + IniFiles, + Images, + Esps, + Conflicts, + Categories, + Nexus, + Notes, + Filetree +}; + +class PluginContainer; + +bool canPreviewFile(PluginContainer& pluginContainer, bool isArchive, const QString& filename); +bool canOpenFile(bool isArchive, const QString& filename); +bool canExploreFile(bool isArchive, const QString& filename); +bool canHideFile(bool isArchive, const QString& filename); +bool canUnhideFile(bool isArchive, const QString& filename); + +FileRenamer::RenameResults hideFile(FileRenamer& renamer, const QString &oldName); +FileRenamer::RenameResults unhideFile(FileRenamer& renamer, const QString &oldName); + +int naturalCompare(const QString& a, const QString& b); + + +class ElideLeftDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + +protected: + void initStyleOption(QStyleOptionViewItem* o, const QModelIndex& i) const + { + QStyledItemDelegate::initStyleOption(o, i); + o->textElideMode = Qt::ElideLeft; + } +}; + +#endif // MODINFODIALOGFWD_H diff --git a/src/modinfodialogimages.cpp b/src/modinfodialogimages.cpp new file mode 100644 index 00000000..69866902 --- /dev/null +++ b/src/modinfodialogimages.cpp @@ -0,0 +1,1102 @@ +#include "modinfodialogimages.h" +#include "ui_modinfodialog.h" +#include "settings.h" +#include "utility.h" + +using namespace ImagesTabHelpers; + +QSize resizeWithAspectRatio(const QSize& original, const QSize& available) +{ + const auto ratio = std::min({ + 1.0, + static_cast<double>(available.width()) / original.width(), + static_cast<double>(available.height()) / original.height()}); + + const QSize scaledSize( + static_cast<int>(std::round(original.width() * ratio)), + static_cast<int>(std::round(original.height() * ratio))); + + return scaledSize; +} + +QRect centeredRect(const QRect& rect, const QSize& size) +{ + return QRect( + (rect.left()+rect.width()/2) - size.width()/2, + (rect.top()+rect.height()/2) - size.height()/2, + size.width(), + size.height()); +} + +QString dimensionString(const QSize& s) +{ + return QString::fromUtf8("%1 \xc3\x97 %2") + .arg(s.width()).arg(s.height()); +} + + +ImagesTab::ImagesTab(ModInfoDialogTabContext cx) : + ModInfoDialogTab(std::move(cx)), m_image(new ScalableImage), + m_ddsAvailable(false), m_ddsEnabled(false) +{ + getSupportedFormats(); + + auto* ly = new QVBoxLayout(ui->imagesImage); + ly->setContentsMargins({0, 0, 0, 0}); + ly->addWidget(m_image); + + delete ui->imagesThumbnails->layout(); + + ui->tabImagesSplitter->setSizes({128, 1}); + ui->tabImagesSplitter->setStretchFactor(0, 0); + ui->tabImagesSplitter->setStretchFactor(1, 1); + + ui->imagesThumbnails->setTab(this); + + ui->imagesScrollerVBar->setTab(this); + connect(ui->imagesScrollerVBar, &QScrollBar::valueChanged, [&]{ onScrolled(); }); + + ui->imagesShowDDS->setEnabled(m_ddsAvailable); + + m_filter.setEdit(ui->imagesFilter); + connect(&m_filter, &FilterWidget::changed, [&]{ onFilterChanged(); }); + + connect(ui->imagesExplore, &QAbstractButton::clicked, [&]{ onExplore(); }); + connect(ui->imagesShowDDS, &QCheckBox::toggled, [&]{ onShowDDS(); }); + + ui->imagesShowDDS->setEnabled(m_ddsAvailable); + + ui->imagesThumbnails->setAutoFillBackground(false); + ui->imagesThumbnails->setAttribute(Qt::WA_OpaquePaintEvent, true); + + { + auto list = std::make_unique<QListWidget>(); + parentWidget()->style()->polish(list.get()); + + m_theme.borderColor = QColor(Qt::black); + m_theme.backgroundColor = QColor(Qt::black); + m_theme.textColor = list->palette().color(QPalette::WindowText); + + m_theme.highlightBackgroundColor = list->palette().color(QPalette::Highlight); + m_theme.highlightTextColor = list->palette().color(QPalette::HighlightedText); + + m_theme.font = list->font(); + + const QFontMetrics fm(m_theme.font); + m_metrics.textHeight = fm.height(); + + m_image->setColors(m_theme.borderColor, m_theme.backgroundColor); + } +} + +void ImagesTab::clear() +{ + m_files.clear(); + ui->imagesScrollerVBar->setValue(0); + select(BadIndex); + setHasData(false); +} + +bool ImagesTab::feedFile(const QString& rootPath, const QString& fullPath) +{ + for (const auto& ext : m_supportedFormats) { + if (fullPath.endsWith(ext, Qt::CaseInsensitive)) { + m_files.add({fullPath}); + return true; + } + } + + return false; +} + +void ImagesTab::update() +{ + checkFiltering(); + updateScrollbar(); + + // visibility needs to be rechecked here because the scrollbar configuration + // may have changed in updateScrollbar(), in which case any ensureVisible() + // calls in checkFiltering() might have been incorrect + if (m_files.selectedIndex() != BadIndex) { + ensureVisible(m_files.selectedIndex(), Visibility::Partial); + } + + ui->imagesThumbnails->update(); + + setHasData(m_files.size() > 0); +} + +void ImagesTab::saveState(Settings& s) +{ + s.directInterface().setValue( + "mod_info_dialog_images_show_dds", m_ddsEnabled); + + saveWidgetState(s.directInterface(), ui->tabImagesSplitter); +} + +void ImagesTab::restoreState(const Settings& s) +{ + ui->imagesShowDDS->setChecked(s.directInterface() + .value("mod_info_dialog_images_show_dds", false).toBool()); + + restoreWidgetState(s.directInterface(), ui->tabImagesSplitter); +} + +void ImagesTab::checkFiltering() +{ + if (m_filter.empty() && m_ddsEnabled) { + // no filtering needed + + if (m_files.isFiltered()) { + // was filtered, needs switch + switchToAll(); + } + } else { + // filtering is needed + switchToFiltered(); + } +} + +void ImagesTab::switchToAll() +{ + // remember selection + const auto* oldSelection = m_files.selectedFile(); + + // switch + m_files.switchToAll(); + + // reselect old + if (oldSelection) { + select(m_files.indexOf(oldSelection)); + } else { + select(BadIndex); + } +} + +void ImagesTab::switchToFiltered() +{ + // remember old selection, will be checked when building the filtered list + // below + const auto* oldSelection = m_files.selectedFile(); + std::size_t newSelection = BadIndex; + + // switch, also clears list + m_files.switchToFiltered(); + + const bool hasTextFilter = !m_filter.empty(); + + for (auto& f : m_files.allFiles()) { + if (hasTextFilter) { + // check filter widget + const auto m = m_filter.matches([&](auto&& what) { + return f.path().contains(what, Qt::CaseInsensitive); + }); + + if (!m) { + // no match, skip + continue; + } + } + + if (!m_ddsEnabled) { + // skip .dds files + if (f.path().endsWith(".dds", Qt::CaseInsensitive)) { + continue; + } + } + + if (&f == oldSelection) { + // found the old selection, remember its index + newSelection = m_files.size(); + } + + m_files.addFiltered(&f); + } + + // reselect old, or clear if it wasn't found + select(newSelection); +} + +void ImagesTab::getSupportedFormats() +{ + m_ddsAvailable = false; + + for (const auto& entry : QImageReader::supportedImageFormats()) { + QString s(entry); + if (s.isNull() || s.isEmpty()) { + continue; + } + + // used to enable the checkbox + if (s.compare("dds", Qt::CaseInsensitive) == 0) { + m_ddsAvailable = true; + } + + // make sure it starts with a dot + if (s[0] != ".") { + s = "." + s; + } + + m_supportedFormats.emplace_back(std::move(s)); + } +} + +void ImagesTab::select(std::size_t i, Visibility v) +{ + m_files.select(i); + + if (auto* f=m_files.selectedFile()) { + // when jumping elsewhere in the list, such as with page down/up, the file + // might not be visible yet, which means it hasn't been loaded and would + // pass a null image in setImage() below + f->ensureOriginalLoaded(); + + ui->imagesPath->setText(QDir::toNativeSeparators(f->path())); + ui->imagesExplore->setEnabled(true); + ui->imagesSize->setText(dimensionString(f->original().size())); + + m_image->setImage(f->original()); + ensureVisible(i, v); + } else { + ui->imagesPath->clear(); + ui->imagesExplore->setEnabled(false); + ui->imagesSize->clear(); + m_image->clear(); + } + + ui->imagesThumbnails->update(); +} + +void ImagesTab::moveSelection(int by) +{ + if (m_files.empty()) { + return; + } + + auto i = m_files.selectedIndex(); + if (i == BadIndex) { + i = 0; + } + + if (by > 0) { + // moving down + i += static_cast<std::size_t>(by); + + if (i >= m_files.size()) { + i = (m_files.size() - 1); + } + } else if (by < 0) { + // moving up + const auto abs_by = static_cast<std::size_t>(std::abs(by)); + + if (abs_by > i) { + i = 0; + } else { + i -= abs_by; + } + } + + select(i); +} + +void ImagesTab::ensureVisible(std::size_t i, Visibility v) +{ + if (v == Visibility::Ignore) { + return; + } + + const auto geo = makeGeometry(); + + const auto fullyVisible = geo.fullyVisibleCount(); + const auto partiallyVisible = fullyVisible + 1; + + const auto first = ui->imagesScrollerVBar->value(); + const auto last = (v == Visibility::Full ? + first + fullyVisible : first + partiallyVisible); + + if (i < first) { + // go up + ui->imagesScrollerVBar->setValue(static_cast<int>(i)); + } else if (i >= last) { + // go down + + if (i >= fullyVisible) { + ui->imagesScrollerVBar->setValue(static_cast<int>(i - fullyVisible + 1)); + } + } +} + +std::size_t ImagesTab::fileIndexAtPos(const QPoint& p) const +{ + const auto geo = makeGeometry(); + + // this is the index relative to the top + const auto offset = geo.indexAt(p); + if (offset == BadIndex) { + return BadIndex; + } + + const auto first = ui->imagesScrollerVBar->value(); + if (first < 0) { + return BadIndex; + } + + const auto i = static_cast<std::size_t>(first) + offset; + if (i >= m_files.size()) { + return BadIndex; + } + + return i; +} + +const File* ImagesTab::fileAtPos(const QPoint& p) const +{ + const auto i = fileIndexAtPos(p); + if (i >= m_files.size()) { + return nullptr; + } + + return m_files.get(i); +} + +Geometry ImagesTab::makeGeometry() const +{ + return Geometry(ui->imagesThumbnails->size(), m_metrics); +} + +void ImagesTab::paintThumbnailsArea(QPaintEvent* e) +{ + PaintContext cx(ui->imagesThumbnails, makeGeometry()); + + cx.painter.fillRect( + ui->imagesThumbnails->rect(), + ui->imagesThumbnails->palette().color(QPalette::Window)); + + const auto visible = cx.geo.fullyVisibleCount() + 1; + const auto first = ui->imagesScrollerVBar->value(); + + for (std::size_t i=0; i<visible; ++i) { + const auto fileIndex = first + i; + auto* file = m_files.get(fileIndex); + if (!file) { + break; + } + + cx.file = file; + cx.thumbIndex = i; + cx.fileIndex = fileIndex; + + paintThumbnail(cx); + } +} + +void ImagesTab::paintThumbnail(const PaintContext& cx) +{ + paintThumbnailBackground(cx); + paintThumbnailBorder(cx); + paintThumbnailImage(cx); + paintThumbnailText(cx); +} + +void ImagesTab::paintThumbnailBackground(const PaintContext& cx) +{ + if (m_files.selectedIndex() == cx.fileIndex) { + const auto rect = cx.geo.thumbRect(cx.thumbIndex); + cx.painter.fillRect(rect, m_theme.highlightBackgroundColor); + } +} + +void ImagesTab::paintThumbnailBorder(const PaintContext& cx) +{ + auto borderRect = cx.geo.borderRect(cx.thumbIndex); + + // rects don't include the bottom right corner, but drawRect() does, so + // resize it + borderRect.setRight(borderRect.right() - 1); + borderRect.setBottom(borderRect.bottom() - 1); + + cx.painter.setPen(m_theme.borderColor); + cx.painter.drawRect(borderRect); +} + +void ImagesTab::paintThumbnailImage(const PaintContext& cx) +{ + if (cx.file->failed()) { + return; + } + + cx.file->loadIfNeeded(cx.geo); + + const auto imageRect = cx.geo.imageRect(cx.thumbIndex); + const auto scaledThumbRect = centeredRect( + imageRect, cx.file->thumbnail().size()); + + cx.painter.fillRect(scaledThumbRect, m_theme.backgroundColor); + cx.painter.drawImage(scaledThumbRect, cx.file->thumbnail()); +} + +void ImagesTab::paintThumbnailText(const PaintContext& cx) +{ + const auto tr = cx.geo.textRect(cx.thumbIndex); + + if (cx.fileIndex == m_files.selectedIndex()) { + cx.painter.setPen(m_theme.highlightTextColor); + } else { + cx.painter.setPen(m_theme.textColor); + } + + cx.painter.setFont(m_theme.font); + + QFontMetrics fm(m_theme.font); + + const auto text = fm.elidedText( + cx.file->filename(), Qt::ElideRight, tr.width()); + + const auto flags = Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextSingleLine; + + cx.painter.drawText(tr, flags, text); +} + +void ImagesTab::scrollAreaResized(const QSize&) +{ + updateScrollbar(); +} + +void ImagesTab::thumbnailAreaMouseEvent(QMouseEvent* e) +{ + if (e->button() != Qt::LeftButton) { + return; + } + + const auto i = fileIndexAtPos(e->pos()); + if (i != BadIndex) { + // the only way to click on a thumbnail is if it's already visible, so the + // only thing that can happen is a click on a partially visible thumbnail, + // which would scroll so it is fully visible, and that's just annoying + select(i, Visibility::Ignore); + } +} + +void ImagesTab::thumbnailAreaWheelEvent(QWheelEvent* e) +{ + const auto d = (e->angleDelta() / 8).y(); + + ui->imagesScrollerVBar->setValue( + ui->imagesScrollerVBar->value() + (d > 0 ? -1 : 1)); +} + +bool ImagesTab::thumbnailAreaKeyPressEvent(QKeyEvent* e) +{ + switch (e->key()) { + case Qt::Key_Down: { + moveSelection(ui->imagesScrollerVBar->singleStep()); + return true; + } + + case Qt::Key_Up: { + moveSelection(-ui->imagesScrollerVBar->singleStep()); + return true; + } + + case Qt::Key_PageDown: { + moveSelection(ui->imagesScrollerVBar->pageStep()); + return true; + } + + case Qt::Key_PageUp: { + moveSelection(-ui->imagesScrollerVBar->pageStep()); + return true; + } + + case Qt::Key_Home: { + select(0); + return true; + } + + case Qt::Key_End: { + if (!m_files.empty()) { + select(m_files.size() - 1); + } + + return true; + } + } + + return false; +} + +void ImagesTab::onScrolled() +{ + ui->imagesThumbnails->update(); +} + +void ImagesTab::showTooltip(QHelpEvent* e) +{ + const auto* f = fileAtPos(e->pos()); + if (!f) { + QToolTip::hideText(); + e->ignore(); + return; + } + + const auto s = QString("%1 (%2)") + .arg(QDir::toNativeSeparators(f->path())) + .arg(dimensionString(f->original().size())); + + QToolTip::showText(e->globalPos(), s, ui->imagesThumbnails); +} + +void ImagesTab::onExplore() +{ + if (auto* f=m_files.selectedFile()) { + MOBase::shell::ExploreFile(f->path()); + } +} + +void ImagesTab::onShowDDS() +{ + const auto b = ui->imagesShowDDS->isChecked(); + if (b != m_ddsEnabled) { + m_ddsEnabled = b; + update(); + } +} + +void ImagesTab::onFilterChanged() +{ + update(); +} + +void ImagesTab::updateScrollbar() +{ + if (m_files.size() == 0) { + ui->imagesScrollerVBar->setRange(0, 0); + ui->imagesScrollerVBar->setEnabled(false); + return; + } + + const auto geo = makeGeometry(); + const auto availableSize = ui->imagesThumbnails->size(); + const auto fullyVisible = geo.fullyVisibleCount(); + + if (fullyVisible >= m_files.size()) { + ui->imagesScrollerVBar->setRange(0, 0); + ui->imagesScrollerVBar->setEnabled(false); + } else { + const auto d = m_files.size() - fullyVisible; + ui->imagesScrollerVBar->setRange(0, static_cast<int>(d)); + ui->imagesScrollerVBar->setSingleStep(1); + ui->imagesScrollerVBar->setPageStep(static_cast<int>(fullyVisible - 1)); + ui->imagesScrollerVBar->setEnabled(true); + } +} + + +namespace ImagesTabHelpers +{ + +void Scrollbar::setTab(ImagesTab* tab) +{ + m_tab = tab; +} + +void Scrollbar::wheelEvent(QWheelEvent* e) +{ + if (m_tab) { + m_tab->thumbnailAreaWheelEvent(e); + } +} + + +void ThumbnailsWidget::setTab(ImagesTab* tab) +{ + m_tab = tab; +} + +void ThumbnailsWidget::paintEvent(QPaintEvent* e) +{ + if (m_tab) { + m_tab->paintThumbnailsArea(e); + } +} + +void ThumbnailsWidget::mousePressEvent(QMouseEvent* e) +{ + if (m_tab) { + m_tab->thumbnailAreaMouseEvent(e); + } +} + +void ThumbnailsWidget::wheelEvent(QWheelEvent* e) +{ + if (m_tab) { + m_tab->thumbnailAreaWheelEvent(e); + } +} + +void ThumbnailsWidget::resizeEvent(QResizeEvent* e) +{ + if (m_tab) { + m_tab->scrollAreaResized(e->size()); + } +} + +void ThumbnailsWidget::keyPressEvent(QKeyEvent* e) +{ + if (m_tab) { + if (m_tab->thumbnailAreaKeyPressEvent(e)) { + return; + } + } + + QWidget::keyPressEvent(e); +} + +bool ThumbnailsWidget::event(QEvent* e) +{ + if (e->type() == QEvent::ToolTip) { + m_tab->showTooltip(static_cast<QHelpEvent*>(e)); + return true; + } + + return QWidget::event(e); +} + + +ScalableImage::ScalableImage(QString path) + : m_path(std::move(path)), m_border(1) +{ + auto sp = sizePolicy(); + sp.setHeightForWidth(true); + setSizePolicy(sp); +} + +void ScalableImage::setImage(const QString& path) +{ + m_path = path; + m_original = {}; + m_scaled = {}; + + update(); +} + +void ScalableImage::setImage(QImage image) +{ + m_path.clear(); + m_original = std::move(image); + m_scaled = {}; + + update(); +} + +void ScalableImage::clear() +{ + setImage(QImage()); +} + +bool ScalableImage::hasHeightForWidth() const +{ + return true; +} + +int ScalableImage::heightForWidth(int w) const +{ + return w; +} + +void ScalableImage::setColors(const QColor& border, const QColor& background) +{ + m_borderColor = border; + m_backgroundColor = background; +} + +void ScalableImage::paintEvent(QPaintEvent* e) +{ + if (m_original.isNull()) { + if (m_path.isNull()) { + return; + } + + m_original.load(m_path); + + if (m_original.isNull()) { + return; + } + } + + const QRect widgetRect = rect(); + const QRect imageRect = widgetRect.adjusted( + m_border, m_border, -m_border, -m_border); + + const QSize scaledSize = resizeWithAspectRatio( + m_original.size(), imageRect.size()); + + if (m_scaled.isNull() || m_scaled.size() != scaledSize) { + m_scaled = m_original.scaled( + scaledSize.width(), scaledSize.height(), + Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + const QRect drawBorderRect = widgetRect.adjusted(0, 0, -1, -1); + const QRect drawImageRect = centeredRect(imageRect, m_scaled.size()); + + QPainter painter(this); + + // border + painter.setPen(m_borderColor); + painter.drawRect(drawBorderRect); + + // background + painter.fillRect(drawImageRect, m_backgroundColor); + + // image + painter.drawImage(drawImageRect, m_scaled); +} + + +Metrics::Metrics() : + margins(3), + border(1), + padding(0), + spacing(5), + textSpacing(2), + textHeight(0) +{ +} + + +Geometry::Geometry(QSize widgetSize, Metrics metrics) + : m_widgetSize(widgetSize), m_metrics(metrics), m_topRect(calcTopRect()) +{ +} + +QRect Geometry::calcTopRect() const +{ + const auto thumbWidth = m_widgetSize.width(); + const auto& m = m_metrics; + + const auto imageSize = + thumbWidth - + (m.margins * 2) - + (m.border * 2) - + (m.padding * 2); + + const auto thumbHeight = + m.margins + + m.border + m.padding + + imageSize + + m.padding + m.border + + m.textSpacing + m.textHeight + + m.margins; + + return {0, 0, thumbWidth, thumbHeight}; +} + +std::size_t Geometry::fullyVisibleCount() const +{ + const auto r = thumbRect(0); + + const auto thumbWithSpacing = r.height() + m_metrics.spacing; + const auto visible = (m_widgetSize.height() / thumbWithSpacing); + + return static_cast<std::size_t>(visible); +} + +QRect Geometry::thumbRect(std::size_t i) const +{ + // rect for the top thumbnail + QRect r = m_topRect; + + // move down + const auto thumbWithSpacing = m_metrics.spacing + r.height(); + r.translate(0, static_cast<int>(i * thumbWithSpacing)); + + return r; +} + +QRect Geometry::borderRect(std::size_t i) const +{ + auto r = thumbRect(i); + const auto& m = m_metrics; + + // remove margins and text + r.adjust(m.margins, m.margins, -m.margins, -m.margins); + + // remove text + r.adjust(0, 0, 0, -(m.textSpacing + m.textHeight)); + + return r; +} + +QRect Geometry::imageRect(std::size_t i) const +{ + auto r = borderRect(i); + + // remove border and padding + const auto m = m_metrics.border + m_metrics.padding; + r.adjust(m, m, -m, -m); + + return r; +} + +QRect Geometry::textRect(std::size_t i) const +{ + const auto r = borderRect(i); + + return QRect( + r.left(), + r.bottom() + m_metrics.textSpacing, + r.width(), + m_metrics.textHeight); +} + +QRect Geometry::selectionRect(std::size_t i) const +{ + const auto br = borderRect(i); + const auto tr = textRect(i); + + return QRect( + br.left(), + br.top(), + br.width(), + tr.bottom() - br.top()); +} + +std::size_t Geometry::indexAt(const QPoint& p) const +{ + // calculate index purely based on y position + const std::size_t offset = p.y() / (m_topRect.height() + m_metrics.spacing); + + if (!selectionRect(offset).contains(p)) { + return BadIndex; + } + + return offset; +} + +QSize Geometry::scaledImageSize(const QSize& originalSize) const +{ + const auto availableSize = imageRect(0).size(); + return resizeWithAspectRatio(originalSize, availableSize); +} + + +File::File(QString path) + : m_path(std::move(path)), m_failed(false) +{ +} + +void File::ensureOriginalLoaded() +{ + if (!m_original.isNull()) { + // already loaded + return; + } + + QImageReader reader(m_path); + + if (!reader.read(&m_original)) { + qCritical().noquote().nospace() + << "failed to load '" << m_path << "'\n" + << reader.errorString() << " " + << "(error " << static_cast<int>(reader.error()) << ")"; + + m_failed = true; + } +} + +const QString& File::path() const +{ + return m_path; +} + +const QString& File::filename() const +{ + if (m_filename.isEmpty()) { + m_filename = QFileInfo(m_path).fileName(); + } + + return m_filename; +} + +const QImage& File::original() const +{ + return m_original; +} + +const QImage& File::thumbnail() const +{ + return m_thumbnail; +} + +bool File::failed() const +{ + return m_failed; +} + +void File::loadIfNeeded(const Geometry& geo) +{ + if (needsLoad(geo)) { + load(geo); + } +} + +bool File::needsLoad(const Geometry& geo) const +{ + if (m_failed) { + return false; + } + + if (m_original.isNull() || m_thumbnail.isNull()) { + return true; + } + + const auto scaledSize = geo.scaledImageSize(m_original.size()); + return (m_thumbnail.size() != scaledSize); +} + +void File::load(const Geometry& geo) +{ + m_failed = false; + ensureOriginalLoaded(); + + if (m_failed) { + return; + } + + const auto scaledSize = geo.scaledImageSize(m_original.size()); + + m_thumbnail = m_original.scaled( + scaledSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +} + + +Files::Files() + : m_selection(BadIndex), m_filtered(false) +{ +} + +void Files::clear() +{ + m_allFiles.clear(); + m_filteredFiles.clear(); + m_selection = BadIndex; + m_filtered = false; +} + +void Files::add(File f) +{ + m_allFiles.emplace_back(std::move(f)); +} + +void Files::addFiltered(File* f) +{ + m_filteredFiles.push_back(f); +} + +bool Files::empty() const +{ + if (m_filtered) { + return m_filteredFiles.empty(); + } else { + return m_allFiles.empty(); + } +} + +std::size_t Files::size() const +{ + if (m_filtered) { + return m_filteredFiles.size(); + } else { + return m_allFiles.size(); + } +} + +void Files::switchToAll() +{ + m_filtered = false; + m_filteredFiles.clear(); +} + +void Files::switchToFiltered() +{ + m_filtered = true; + m_filteredFiles.clear(); +} + +const File* Files::get(std::size_t i) const +{ + if (m_filtered) { + if (i < m_filteredFiles.size()) { + return m_filteredFiles[i]; + } + } else { + if (i < m_allFiles.size()) { + return &m_allFiles[i]; + } + } + + return nullptr; +} + +File* Files::get(std::size_t i) +{ + return const_cast<File*>(std::as_const(*this).get(i)); +} + +std::size_t Files::indexOf(const File* f) const +{ + if (m_filtered) { + for (std::size_t i=0; i<m_filteredFiles.size(); ++i) { + if (m_filteredFiles[i] == f) { + return i; + } + } + } else { + for (std::size_t i=0; i<m_allFiles.size(); ++i) { + if (&m_allFiles[i] == f) { + return i; + } + } + } + + return BadIndex; +} + +const File* Files::selectedFile() const +{ + return get(m_selection); +} + +File* Files::selectedFile() +{ + return get(m_selection); +} + +std::size_t Files::selectedIndex() const +{ + return m_selection; +} + +void Files::select(std::size_t i) +{ + m_selection = i; +} + +std::vector<File>& Files::allFiles() +{ + return m_allFiles; +} + +bool Files::isFiltered() const +{ + return m_filtered; +} + + +PaintContext::PaintContext(QWidget* w, Geometry geo) + : painter(w), geo(geo), file(nullptr), thumbIndex(0), fileIndex(0) +{ +} + +} // namespace diff --git a/src/modinfodialogimages.h b/src/modinfodialogimages.h new file mode 100644 index 00000000..8d9b965b --- /dev/null +++ b/src/modinfodialogimages.h @@ -0,0 +1,385 @@ +#ifndef MODINFODIALOGIMAGES_H +#define MODINFODIALOGIMAGES_H + +#include "modinfodialogtab.h" +#include "filterwidget.h" +#include <QScrollBar> + +class ImagesTab; + +namespace ImagesTabHelpers +{ + +static constexpr std::size_t BadIndex = std::numeric_limits<std::size_t>::max(); + +// vertical scrollbar, this is only to handle wheel events to scroll by one +// instead of the system's scroll setting +// +class Scrollbar : public QScrollBar +{ +public: + using QScrollBar::QScrollBar; + void setTab(ImagesTab* tab); + +protected: + // forwards to ImagesTab::thumbnailAreaWheelEvent() + // + void wheelEvent(QWheelEvent* event) override; + +private: + ImagesTab* m_tab = nullptr; +}; + + +// widget inside the scroller, calls ImagesTab::paintThumbnailArea() when +// needed and also forwards mouse clicks and tooltip events +// +class ThumbnailsWidget : public QWidget +{ + Q_OBJECT; + +public: + using QWidget::QWidget; + void setTab(ImagesTab* tab); + +protected: + // forwards to ImagesTab::paintThumbnailArea() + // + void paintEvent(QPaintEvent* e) override; + + // forwards to ImagesTab::thumbnailAreaMouseEvent() + // + void mousePressEvent(QMouseEvent* e) override; + + // forwards to ImagesTab::thumbnailAreaWheelEvent() + // + void wheelEvent(QWheelEvent* e); + + // forwards to ImagesTab::scrollAreaResized() + // + void resizeEvent(QResizeEvent* e) override; + + // forwards to ImagesTab::thumbnailAreaKeyPressEvent() + // + void keyPressEvent(QKeyEvent* e) override; + + // forwards to ImagesTab::showTooltip for tooltip events + // + bool event(QEvent* e) override; + +private: + ImagesTab* m_tab = nullptr; +}; + + +// a widget that draws an image scaled to fit while keeping the aspect ratio +// +class ScalableImage : public QWidget +{ + Q_OBJECT; + +public: + ScalableImage(QString path={}); + + // sets the image to draw + void setImage(const QString& path); + void setImage(QImage image); + + // removes the image, won't draw the border nor the image + void clear(); + + // tells the QWidget's layout manager this widget is always square + bool hasHeightForWidth() const override; + int heightForWidth(int w) const override; + + // sets the colors + void setColors(const QColor& border, const QColor& background); + +protected: + void paintEvent(QPaintEvent* e) override; + +private: + QString m_path; + QImage m_original, m_scaled; + int m_border; + QColor m_borderColor, m_backgroundColor; +}; + + +struct Theme +{ + QColor borderColor, backgroundColor, textColor; + QColor highlightBackgroundColor, highlightTextColor; + QFont font; +}; + + +struct Metrics +{ + // space outside the thumbnail border + int margins; + + // size of the border + int border; + + // space between the border and the image + int padding; + + // spacing between the thumbnail and the text + int textSpacing; + + // height of the text + int textHeight; + + // spacing between thumbnails + int spacing; + + Metrics(); +}; + + +// handles all the geometry calculations by ImagesTab for painting or handling +// mouse clicks +// +// a thumbnail looks like this: +// +// +-----------------------+ <--- thumb rect +// | margins | +// | | +// | +-border--------+ <------- border rect +// | | padding | | +// | | | | +// | | +-------+ <----------- image rect +// | | | | | | +// | | | image | | | +// | | | | | | +// | | +-------+ | | +// | | | | +// | +---------------+ | +// | text spacing | +// | +---------------+ <------- text rect +// | | text | | +// | +---------------+ | +// | | +// +-----------------------+ +// +// spacing +// +// +-----------------------+ <-- thumb rect +// | margins | +// | | +// .... +// +// +class Geometry +{ +public: + Geometry(QSize widgetSize, Metrics metrics); + + // returns the number of images fully visible in the widget + // + std::size_t fullyVisibleCount() const; + + // rectangle around the whole thumbnail + // + QRect thumbRect(std::size_t i) const; + + // rectangle of the border for the given thumbnail + // + QRect borderRect(std::size_t i) const; + + // rectangle of the image for the given thumbnail + // + QRect imageRect(std::size_t i) const; + + // rectangle of the text for the given thumbnail + // + QRect textRect(std::size_t i) const; + + // rectangle that responds to selection: includes the border and extends down + // to the text + // + QRect selectionRect(std::size_t i) const; + + // returns the index of the image at the given point; this does not take into + // account any scrolling, the image at the top of widget is always 0 + // + // returns BadIndex if there's no thumbnail at this point + // + std::size_t indexAt(const QPoint& p) const; + + // returns the size of the image that fits in imageRect() while keeping the + // same aspect ratio as the given one + // + QSize scaledImageSize(const QSize& originalSize) const; + +private: + // size of the widget containing all the thumbnails + const QSize m_widgetSize; + + // metrics + const Metrics m_metrics; + + // rectangle of the first thumbnail on top + const QRect m_topRect; + + + // calculates the top rectangle + // + QRect calcTopRect() const; +}; + + +class File +{ +public: + File(QString path); + + void ensureOriginalLoaded(); + + const QString& path() const; + const QString& filename() const; + const QImage& original() const; + const QImage& thumbnail() const; + bool failed() const; + + void loadIfNeeded(const Geometry& geo); + +private: + QString m_path; + mutable QString m_filename; + QImage m_original, m_thumbnail; + bool m_failed; + + bool needsLoad(const Geometry& geo) const; + void load(const Geometry& geo); +}; + + +class Files +{ +public: + Files(); + + void clear(); + + void add(File f); + void addFiltered(File* f); + + bool empty() const; + std::size_t size() const; + + void switchToAll(); + void switchToFiltered(); + + const File* get(std::size_t i) const; + File* get(std::size_t i); + std::size_t indexOf(const File* f) const; + + const File* selectedFile() const; + File* selectedFile(); + std::size_t selectedIndex() const; + void select(std::size_t i); + + std::vector<File>& allFiles(); + + bool isFiltered() const; + +private: + std::vector<File> m_allFiles; + std::vector<File*> m_filteredFiles; + std::size_t m_selection; + bool m_filtered; +}; + + +struct PaintContext +{ + mutable QPainter painter; + Geometry geo; + File* file; + std::size_t thumbIndex; + std::size_t fileIndex; + + PaintContext(QWidget* w, Geometry geo); +}; + +} // namespace + + +class ImagesTab : public ModInfoDialogTab +{ + Q_OBJECT; + friend class ImagesTabHelpers::Scrollbar; + friend class ImagesTabHelpers::ThumbnailsWidget; + +public: + ImagesTab(ModInfoDialogTabContext cx); + + void clear() override; + bool feedFile(const QString& rootPath, const QString& fullPath) override; + void update() override; + void saveState(Settings& s) override; + void restoreState(const Settings& s) override; + +private: + enum class Visibility + { + Ignore = 0, + Full, + Partial + }; + + using ScalableImage = ImagesTabHelpers::ScalableImage; + using Files = ImagesTabHelpers::Files; + using File = ImagesTabHelpers::File; + using Theme = ImagesTabHelpers::Theme; + using Metrics = ImagesTabHelpers::Metrics; + using PaintContext = ImagesTabHelpers::PaintContext; + using Geometry = ImagesTabHelpers::Geometry; + + ScalableImage* m_image; + std::vector<QString> m_supportedFormats; + Files m_files; + FilterWidget m_filter; + bool m_ddsAvailable, m_ddsEnabled; + Theme m_theme; + Metrics m_metrics; + + void getSupportedFormats(); + void enableDDS(bool b); + + void scrollAreaResized(const QSize& s); + void paintThumbnailsArea(QPaintEvent* e); + void thumbnailAreaMouseEvent(QMouseEvent* e); + void thumbnailAreaWheelEvent(QWheelEvent* e); + bool thumbnailAreaKeyPressEvent(QKeyEvent* e); + void onScrolled(); + + void showTooltip(QHelpEvent* e); + void onExplore(); + void onShowDDS(); + void onFilterChanged(); + + void select(std::size_t i, Visibility v=Visibility::Full); + void moveSelection(int by); + void ensureVisible(std::size_t i, Visibility v); + + std::size_t fileIndexAtPos(const QPoint& p) const; + const File* fileAtPos(const QPoint& p) const; + + Geometry makeGeometry() const; + + void paintThumbnail(const PaintContext& cx); + void paintThumbnailBackground(const PaintContext& cx); + void paintThumbnailBorder(const PaintContext& cx); + void paintThumbnailImage(const PaintContext& cx); + void paintThumbnailText(const PaintContext& cx); + + void checkFiltering(); + void switchToAll(); + void switchToFiltered(); + void updateScrollbar(); +}; + +#endif // MODINFODIALOGIMAGES_H diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp new file mode 100644 index 00000000..04683c89 --- /dev/null +++ b/src/modinfodialognexus.cpp @@ -0,0 +1,360 @@ +#include "modinfodialognexus.h" +#include "ui_modinfodialog.h" +#include "settings.h" +#include "organizercore.h" +#include "iplugingame.h" +#include "bbcode.h" +#include <versioninfo.h> +#include <utility.h> + +namespace shell = MOBase::shell; + +bool isValidModID(int id) +{ + return (id > 0); +} + +NexusTab::NexusTab(ModInfoDialogTabContext cx) : + ModInfoDialogTab(std::move(cx)), m_requestStarted(false), m_loading(false) +{ + ui->modID->setValidator(new QIntValidator(ui->modID)); + ui->endorse->setVisible(core().settings().endorsementIntegration()); + + connect(ui->modID, &QLineEdit::editingFinished, [&]{ onModIDChanged(); }); + connect( + ui->sourceGame, + static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), + [&]{ onSourceGameChanged(); }); + connect(ui->version, &QLineEdit::editingFinished, [&]{ onVersionChanged(); }); + + connect(ui->refresh, &QPushButton::clicked, [&]{ onRefreshBrowser(); }); + connect(ui->visitNexus, &QPushButton::clicked, [&]{ onVisitNexus(); }); + connect(ui->endorse, &QPushButton::clicked, [&]{ onEndorse(); }); + connect(ui->track, &QPushButton::clicked, [&]{ onTrack(); }); + + connect(ui->hasCustomURL, &QCheckBox::toggled, [&]{ onCustomURLToggled(); }); + connect(ui->customURL, &QLineEdit::editingFinished, [&]{ onCustomURLChanged(); }); + connect(ui->visitCustomURL, &QPushButton::clicked, [&]{ onVisitCustomURL(); }); +} + +NexusTab::~NexusTab() +{ + cleanup(); +} + +void NexusTab::cleanup() +{ + if (m_modConnection) { + disconnect(m_modConnection); + m_modConnection = {}; + } +} + +void NexusTab::clear() +{ + ui->modID->clear(); + ui->sourceGame->clear(); + ui->version->clear(); + ui->browser->setPage(new NexusTabWebpage(ui->browser)); + ui->hasCustomURL->setChecked(false); + ui->customURL->clear(); + setHasData(false); +} + +void NexusTab::update() +{ + QScopedValueRollback loading(m_loading, true); + + clear(); + + ui->modID->setText(QString("%1").arg(mod().getNexusID())); + + QString gameName = mod().getGameName(); + ui->sourceGame->addItem( + core().managedGame()->gameName(), + core().managedGame()->gameShortName()); + + if (core().managedGame()->validShortNames().size() == 0) { + ui->sourceGame->setDisabled(true); + } else { + for (auto game : plugin().plugins<MOBase::IPluginGame>()) { + for (QString gameName : core().managedGame()->validShortNames()) { + if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + ui->sourceGame->addItem(game->gameName(), game->gameShortName()); + break; + } + } + } + } + + ui->sourceGame->setCurrentIndex(ui->sourceGame->findData(gameName)); + + auto* page = new NexusTabWebpage(ui->browser); + ui->browser->setPage(page); + + connect( + page, &NexusTabWebpage::linkClicked, + [&](const QUrl& url){ shell::OpenLink(url); }); + + ui->endorse->setEnabled( + (mod().endorsedState() == ModInfo::ENDORSED_FALSE) || + (mod().endorsedState() == ModInfo::ENDORSED_NEVER)); + + setHasData(mod().getNexusID() >= 0); +} + +void NexusTab::firstActivation() +{ + updateWebpage(); +} + +void NexusTab::setMod(ModInfoPtr mod, MOShared::FilesOrigin* origin) +{ + cleanup(); + + ModInfoDialogTab::setMod(mod, origin); + + m_modConnection = connect( + mod.data(), &ModInfo::modDetailsUpdated, [&]{ onModChanged(); }); +} + +bool NexusTab::usesOriginFiles() const +{ + return false; +} + +void NexusTab::updateVersionColor() +{ + if (mod().getVersion() != mod().getNewestVersion()) { + ui->version->setStyleSheet("color: red"); + ui->version->setToolTip(tr("Current Version: %1").arg( + mod().getNewestVersion().canonicalString())); + } else { + ui->version->setStyleSheet("color: green"); + ui->version->setToolTip(tr("No update available")); + } +} + +void NexusTab::updateWebpage() +{ + const int modID = mod().getNexusID(); + + if (isValidModID(modID)) { + const QString nexusLink = NexusInterface::instance(&plugin()) + ->getModURL(modID, mod().getGameName()); + + ui->visitNexus->setToolTip(nexusLink); + refreshData(modID); + } else { + onModChanged(); + } + + ui->version->setText(mod().getVersion().displayString()); + ui->hasCustomURL->setChecked(mod().hasCustomURL()); + ui->customURL->setText(mod().getCustomURL()); + ui->customURL->setEnabled(mod().hasCustomURL()); + ui->visitCustomURL->setEnabled(mod().hasCustomURL()); + ui->visitCustomURL->setToolTip(mod().parseCustomURL().toString()); + + updateTracking(); +} + +void NexusTab::updateTracking() +{ + if (mod().trackedState() == ModInfo::TRACKED_TRUE) { + ui->track->setChecked(true); + ui->track->setText(tr("Tracked")); + } else { + ui->track->setChecked(false); + ui->track->setText(tr("Untracked")); + } +} + +void NexusTab::refreshData(int modID) +{ + if (tryRefreshData(modID)) { + m_requestStarted = true; + } else { + onModChanged(); + } +} + +bool NexusTab::tryRefreshData(int modID) +{ + if (isValidModID(modID) && !m_requestStarted) { + if (mod().updateNXMInfo()) { + ui->browser->setHtml(""); + return true; + } + } + + return false; +} + +void NexusTab::onModChanged() +{ + m_requestStarted = false; + + const QString nexusDescription = mod().getNexusDescription(); + + QString descriptionAsHTML = R"( +<html> + <head> + <style class="nexus-description"> + body + { + font-family: sans-serif; + font-size: 14px; + background: #404040; + color: #f1f1f1; + max-width: 1060px; + margin-left: auto; + margin-right: auto; + } + + a + { + color: #8197ec; + text-decoration: none; + } + </style> + </head> + <body>%1</body> +</html>)"; + + if (nexusDescription.isEmpty()) { + descriptionAsHTML = descriptionAsHTML.arg(tr(R"( + <div style="text-align: center;"> + <p>This mod does not have a valid Nexus ID. You can add a custom web + page for it in the "Custom URL" box below.</p> + </div>)")); + } else { + descriptionAsHTML = descriptionAsHTML.arg( + BBCode::convertToHTML(nexusDescription)); + } + + ui->browser->page()->setHtml(descriptionAsHTML); + updateVersionColor(); + updateTracking(); +} + +void NexusTab::onModIDChanged() +{ + if (m_loading) { + return; + } + + const int oldID = mod().getNexusID(); + const int newID = ui->modID->text().toInt(); + + if (oldID != newID){ + mod().setNexusID(newID); + mod().setLastNexusQuery(QDateTime::fromSecsSinceEpoch(0)); + + ui->browser->page()->setHtml(""); + + if (isValidModID(newID)) { + refreshData(newID); + } + } +} + +void NexusTab::onSourceGameChanged() +{ + if (m_loading) { + return; + } + + for (auto game : plugin().plugins<MOBase::IPluginGame>()) { + if (game->gameName() == ui->sourceGame->currentText()) { + mod().setGameName(game->gameShortName()); + mod().setLastNexusQuery(QDateTime::fromSecsSinceEpoch(0)); + refreshData(mod().getNexusID()); + return; + } + } +} + +void NexusTab::onVersionChanged() +{ + if (m_loading) { + return; + } + + MOBase::VersionInfo version(ui->version->text()); + mod().setVersion(version); + updateVersionColor(); +} + +void NexusTab::onRefreshBrowser() +{ + const auto modID = mod().getNexusID(); + + if (isValidModID(modID)) { + mod().setLastNexusQuery(QDateTime::fromSecsSinceEpoch(0)); + updateWebpage(); + } else { + qInfo("Mod has no valid Nexus ID, info can't be updated."); + } +} + +void NexusTab::onVisitNexus() +{ + const int modID = mod().getNexusID(); + + if (isValidModID(modID)) { + const QString nexusLink = NexusInterface::instance(&plugin()) + ->getModURL(modID, mod().getGameName()); + + shell::OpenLink(QUrl(nexusLink)); + } +} + +void NexusTab::onEndorse() +{ + // use modPtr() instead of mod() or this because the callback may be + // executed after the dialog is closed + core().loggedInAction(parentWidget(), [m=modPtr()]{ m->endorse(true); }); +} + +void NexusTab::onTrack() +{ + // use modPtr() instead of mod() or this because the callback may be + // executed after the dialog is closed + core().loggedInAction(parentWidget(), [m=modPtr()] { + if (m->trackedState() == ModInfo::TRACKED_TRUE) { + m->track(false); + } else { + m->track(true); + } + }); +} + +void NexusTab::onCustomURLToggled() +{ + if (m_loading) { + return; + } + + mod().setHasCustomURL(ui->hasCustomURL->isChecked()); + ui->customURL->setEnabled(mod().hasCustomURL()); + ui->visitCustomURL->setEnabled(mod().hasCustomURL()); +} + +void NexusTab::onCustomURLChanged() +{ + if (m_loading) { + return; + } + + mod().setCustomURL(ui->customURL->text()); + ui->visitCustomURL->setToolTip(mod().parseCustomURL().toString()); +} + +void NexusTab::onVisitCustomURL() +{ + const auto url = mod().parseCustomURL(); + if (url.isValid()) { + shell::OpenLink(url); + } +} diff --git a/src/modinfodialognexus.h b/src/modinfodialognexus.h new file mode 100644 index 00000000..7f894dbf --- /dev/null +++ b/src/modinfodialognexus.h @@ -0,0 +1,74 @@ +#ifndef MODINFODIALOGNEXUS_H +#define MODINFODIALOGNEXUS_H + +#include "modinfodialogtab.h" + +class NexusTabWebpage : public QWebEnginePage +{ + Q_OBJECT + +public: + NexusTabWebpage(QObject* parent = 0) + : QWebEnginePage(parent) + { + } + + bool acceptNavigationRequest( + const QUrl & url, QWebEnginePage::NavigationType type, bool) override + { + if (type == QWebEnginePage::NavigationTypeLinkClicked) + { + emit linkClicked(url); + return false; + } + + return true; + } + +signals: + void linkClicked(const QUrl&); +}; + + +class NexusTab : public ModInfoDialogTab +{ +public: + NexusTab(ModInfoDialogTabContext cx); + + ~NexusTab(); + + void clear() override; + void update() override; + void firstActivation() override; + void setMod(ModInfoPtr mod, MOShared::FilesOrigin* origin) override; + bool usesOriginFiles() const override; + +private: + QMetaObject::Connection m_modConnection; + bool m_requestStarted; + bool m_loading; + + void cleanup(); + void updateVersionColor(); + void updateWebpage(); + void updateTracking(); + + void refreshData(int modID); + bool tryRefreshData(int modID); + void onModChanged(); + + void onModIDChanged(); + void onSourceGameChanged(); + void onVersionChanged(); + + void onRefreshBrowser(); + void onVisitNexus(); + void onEndorse(); + void onTrack(); + + void onCustomURLToggled(); + void onCustomURLChanged(); + void onVisitCustomURL(); +}; + +#endif // MODINFODIALOGNEXUS_H diff --git a/src/modinfodialogtab.cpp b/src/modinfodialogtab.cpp new file mode 100644 index 00000000..9748d059 --- /dev/null +++ b/src/modinfodialogtab.cpp @@ -0,0 +1,209 @@ +#include "modinfodialogtab.h" +#include "ui_modinfodialog.h" +#include "texteditor.h" +#include "directoryentry.h" +#include "modinfo.h" + +ModInfoDialogTab::ModInfoDialogTab(ModInfoDialogTabContext cx) : + ui(cx.ui), m_core(cx.core), m_plugin(cx.plugin), m_parent(cx.parent), + m_origin(cx.origin), m_tabID(cx.id), m_hasData(false), m_firstActivation(true) +{ +} + +void ModInfoDialogTab::activated() +{ + if (m_firstActivation) { + m_firstActivation = false; + firstActivation(); + } +} + +void ModInfoDialogTab::resetFirstActivation() +{ + m_firstActivation = true; +} + +void ModInfoDialogTab::update() +{ + // no-op +} + +bool ModInfoDialogTab::feedFile(const QString&, const QString&) +{ + // no-op + return false; +} + +void ModInfoDialogTab::firstActivation() +{ + // no-op +} + +bool ModInfoDialogTab::canClose() +{ + return true; +} + +void ModInfoDialogTab::saveState(Settings&) +{ + // no-op +} + +void ModInfoDialogTab::restoreState(const Settings& s) +{ + // no-op +} + +bool ModInfoDialogTab::deleteRequested() +{ + // no-op + return false; +} + +bool ModInfoDialogTab::canHandleSeparators() const +{ + return false; +} + +bool ModInfoDialogTab::canHandleUnmanaged() const +{ + return false; +} + +bool ModInfoDialogTab::usesOriginFiles() const +{ + return true; +} + +void ModInfoDialogTab::setMod(ModInfoPtr mod, MOShared::FilesOrigin* origin) +{ + m_mod = mod; + m_origin = origin; +} + +ModInfo& ModInfoDialogTab::mod() const +{ + Q_ASSERT(m_mod); + return *m_mod; +} + +ModInfoPtr ModInfoDialogTab::modPtr() const +{ + Q_ASSERT(m_mod); + return m_mod; +} + +MOShared::FilesOrigin* ModInfoDialogTab::origin() const +{ + return m_origin; +} + +ModInfoTabIDs ModInfoDialogTab::tabID() const +{ + return m_tabID; +} + +bool ModInfoDialogTab::hasData() const +{ + return m_hasData; +} + +OrganizerCore& ModInfoDialogTab::core() +{ + return m_core; +} + +PluginContainer& ModInfoDialogTab::plugin() +{ + return m_plugin; +} + +QWidget* ModInfoDialogTab::parentWidget() +{ + return m_parent; +} + +void ModInfoDialogTab::emitOriginModified() +{ + if (m_origin) { + emit originModified(m_origin->getID()); + } +} + +void ModInfoDialogTab::emitModOpen(QString name) +{ + emit modOpen(name); +} + +void ModInfoDialogTab::setHasData(bool b) +{ + if (m_hasData != b) { + m_hasData = b; + emit hasDataChanged(); + } +} + +void ModInfoDialogTab::setFocus() +{ + emit wantsFocus(); +} + + +NotesTab::NotesTab(ModInfoDialogTabContext cx) + : ModInfoDialogTab(std::move(cx)) +{ + connect(ui->comments, &QLineEdit::editingFinished, [&]{ onComments(); }); + connect(ui->notes, &HTMLEditor::editingFinished, [&]{ onNotes(); }); +} + +void NotesTab::clear() +{ + ui->comments->clear(); + ui->notes->clear(); + setHasData(false); +} + +void NotesTab::update() +{ + const auto comments = mod().comments(); + const auto notes = mod().notes(); + + ui->comments->setText(comments); + ui->notes->setText(notes); + checkHasData(); +} + +bool NotesTab::canHandleSeparators() const +{ + return true; +} + +void NotesTab::onComments() +{ + mod().setComments(ui->comments->text()); + checkHasData(); +} + +void NotesTab::onNotes() +{ + // Avoid saving html stub if notes field is empty. + if (ui->notes->toPlainText().isEmpty()) { + mod().setNotes({}); + } else { + mod().setNotes(ui->notes->toHtml()); + } + + checkHasData(); +} + +bool NotesTab::usesOriginFiles() const +{ + return false; +} + +void NotesTab::checkHasData() +{ + setHasData( + !ui->comments->text().isEmpty() || + !ui->notes->toPlainText().isEmpty()); +} diff --git a/src/modinfodialogtab.h b/src/modinfodialogtab.h new file mode 100644 index 00000000..c43fa076 --- /dev/null +++ b/src/modinfodialogtab.h @@ -0,0 +1,341 @@ +#ifndef MODINFODIALOGTAB_H +#define MODINFODIALOGTAB_H + +#include "modinfodialogfwd.h" +#include <QObject> + +namespace MOShared { class FilesOrigin; } +namespace Ui { class ModInfoDialog; } + +class Settings; +class OrganizerCore; + +// helper struct to avoid passing too much stuff to tab constructors +// +struct ModInfoDialogTabContext +{ + OrganizerCore& core; + PluginContainer& plugin; + QWidget* parent; + Ui::ModInfoDialog* ui; + ModInfoTabIDs id; + ModInfoPtr mod; + MOShared::FilesOrigin* origin; + + ModInfoDialogTabContext( + OrganizerCore& core, + PluginContainer& plugin, + QWidget* parent, + Ui::ModInfoDialog* ui, + ModInfoTabIDs id, + ModInfoPtr mod, + MOShared::FilesOrigin* origin) : + core(core), plugin(plugin), parent(parent), ui(ui), id(id), + mod(mod), origin(origin) + { + } +}; + + +// base class for all tabs in the mod info dialog +// +// when the dialog is opened or when next/previous is clicked, the sequence is: +// setMod(), clear(), feedFile() an update() +// +// when the dialog is closed, canClose() is called on all tabs +// +// when a tab is selected for the first time for the current mod, +// firstActivation() is called; this is used by NexusTab to refresh stuff +// +// when the dialog is first shown, restoreState() is called on all tabs and +// saveState() is called when the dialog is closed +// +// there isn't a good framework for keyboard shortcuts because only the delete +// key is used for now, which calls deletedRequested() on all tabs until one +// returns true +// +// each tab override canHandleSeparators() and canHandleUnmanaged() to return +// true if they can handle separators or unmanaged mods; if these return false +// (which they do by default), the tabs will be removed from the widget entirely +// +// when tabs modify the origin and call emitOriginModified() (such as the +// conflicts tabs), all tabs that return true in usesOriginFiles() will go +// through the full update sequence as above +// +// tabs can call emitModOpen() to request showing a different mod +// +// hasDataChanged() should be called when a tab goes from having data to being +// empty or vice versa; this will update the tab text colour +// +class ModInfoDialogTab : public QObject +{ + Q_OBJECT; + +public: + ModInfoDialogTab(const ModInfoDialogTab&) = delete; + ModInfoDialogTab& operator=(const ModInfoDialogTab&) = delete; + ModInfoDialogTab(ModInfoDialogTab&&) = default; + ModInfoDialogTab& operator=(ModInfoDialogTab&&) = default; + virtual ~ModInfoDialogTab() = default; + + // called by ModInfoDialog every time this tab is selected; this will call + // firstActivation() the first time it's called, until resetFirstActivation() + // is called + // + void activated(); + + // called by ModInfoDialog when the selected mod has changed, to make sure + // activated() will call firstActivation() next time + // + void resetFirstActivation(); + + + // called when the selected mod changed, `mod` can never be empty, but + // `origin` can (if the mod is not active, for example) + // + // derived classes can override this to connect to events on the mod for + // examples (see NexusTab), but must call the base class implementation + // + virtual void setMod(ModInfoPtr mod, MOShared::FilesOrigin* origin); + + // this tab should clear its user interface; clear() will always be called + // before feedFile() and update() + // + virtual void clear() = 0; + + // the dialog will go through each file in the mod and call feedFile() + // with it on all tabs; if a tab handles the file, it should return true to + // prevent other tabs from displaying it + // + // this prevents individual tabs from having to go through the filesystem + // independently, which would kill performance, but it cannot be the only way + // to update tabs because some of them don't actually use files (like + // NotesTab) or they use the internal structures such as `FilesOrigin` (like + // ConflictsTab) + // + // once all the files have been fed to tabs, update() will be called; tabs + // that need to do additional work after being fed files, or tabs that are + // unable to use feedFile() at all can use update() to do work + // + virtual bool feedFile(const QString& rootPath, const QString& filename); + + // called after all the files on the filesystem have been sent through + // feedFile() + // + // this can be used to do a final processing of the files handled by + // feedFile() (ImagesTab will set up the scrollbars, for example) or something + // completely different (CategoriesTab will fill up the lists) + // + virtual void update(); + + // called when a tab is visible for the first time for the current mod, can + // be used to do expensive work that's not worth doing until the tab is + // actually selected by the user + // + virtual void firstActivation(); + + // called when closing the dialog, can return false to stop the dialog from + // closing + // + // this is typically used by tabs that require manual saving, like text files; + // tabs that refuse to close should focus themselves before showing whatever + // confirmation they have + // + virtual bool canClose(); + + + // called after the dialog is closed, tabs should save whatever UI state they + // want + // + virtual void saveState(Settings& s); + + // called before the is shown, tabs should restore whatever UI state they + // saved in saveState() + // + virtual void restoreState(const Settings& s); + + + // called on the selected tab when the Delete key is pressed on the keyboard; + // tabs _must_ check which widget currently has focus to decide whether this + // should be handled or not; do not blindly delete stuff when this is called + // + // if the delete request was handled, this should return true + // + virtual bool deleteRequested(); + + + // return true if this tab can handle a separator mod, defaults to false; + // when this returns false, the tab is removed from the widget entirely + // + // if a tab can show meaningful information about a separator (like + // categories or notes), it should return true + // + virtual bool canHandleSeparators() const; + + // return true if this tab can handle unmanaged mods, defaults to false; + // when this returns false, the tab is removed from the widget entirely + // + virtual bool canHandleUnmanaged() const; + + // return true if this tab uses the files from the mod's origin, defaults to + // false + // + // tabs that do not care about the files inside a mod should return false, + // such as the notes or categories tab + // + // mods that return true will be updated anytime a tab calls + // emitOriginModifed() + // + virtual bool usesOriginFiles() const; + + + // returns the currently selected mod + // + ModInfo& mod() const; + + // returns the currently selected mod, can never be empty + // + ModInfoPtr modPtr() const; + + // returns the origin of the selected mod; this can be null for mods that + // don't have an origin, like deactivated mods + // + MOShared::FilesOrigin* origin() const; + + + // return this tab's ID + // + ModInfoTabIDs tabID() const; + + // returns whether this tab has data; derived classes should call setHasData() + // + bool hasData() const; + +signals: + // emitted when a tab modified the files in a mod + // + void originModified(int originID); + + // emitted when a tab wants to open a mod by name + // + void modOpen(QString name); + + // emitted when a tab used to have data and is now empty, or vice versa + // + void hasDataChanged(); + + // emitted when a tab wants focus + // + void wantsFocus(); + +protected: + Ui::ModInfoDialog* ui; + + ModInfoDialogTab(ModInfoDialogTabContext cx); + + OrganizerCore& core(); + PluginContainer& plugin(); + QWidget* parentWidget(); + + // emits originModified + // + void emitOriginModified(); + + // emits modOpen + // + void emitModOpen(QString name); + + // emits hasDataChanged + // + void setHasData(bool b); + + // emits wantsFocus + // + void setFocus(); + + + // saves the sate of the given widget in geometry/modinfodialog_[objectname] + // + // this needs to be a template because saveState() and restoreState() are + // not in QWidget, but they're in various widgets + // + template <class Widget> + void saveWidgetState(QSettings& s, Widget* w) + { + s.setValue(settingName(w), w->saveState()); + } + + // restores the sate of the given widget from + // geometry/modinfodialog_[objectname] + // + // this needs to be a template because saveState() and restoreState() are + // not in QWidget, but they're in various widgets + // + template <class Widget> + void restoreWidgetState(const QSettings& s, Widget* w) + { + if (s.contains(settingName(w))) { + w->restoreState(s.value(settingName(w)).toByteArray()); + } + } + +private: + // core + OrganizerCore& m_core; + + // plugin + PluginContainer& m_plugin; + + // parent widget, used to display modal dialogs + QWidget* m_parent; + + // current mod, never null + ModInfoPtr m_mod; + + // current mod origin, may be null + MOShared::FilesOrigin* m_origin; + + // tab ID + ModInfoTabIDs m_tabID; + + // whether the tab has data + bool m_hasData; + + // true if the tab has never been selected for the current mod + bool m_firstActivation; + + + // used by saveWidgetState() and restoreWidgetState() + // + QString settingName(QWidget* w) + { + return "geometry/modinfodialog_" + w->objectName(); + } +}; + + +// the Notes tab +// +class NotesTab : public ModInfoDialogTab +{ +public: + NotesTab(ModInfoDialogTabContext cx); + + void clear() override; + void update() override; + + // returns true, separators can have notes + // + bool canHandleSeparators() const override; + + // returns false, notes don't use files + // + bool usesOriginFiles() const override; + +private: + void onComments(); + void onNotes(); + void checkHasData(); +}; + +#endif // MODINFODIALOGTAB_H diff --git a/src/modinfodialogtextfiles.cpp b/src/modinfodialogtextfiles.cpp new file mode 100644 index 00000000..7a09fa4e --- /dev/null +++ b/src/modinfodialogtextfiles.cpp @@ -0,0 +1,252 @@ +#include "modinfodialogtextfiles.h" +#include "ui_modinfodialog.h" +#include "modinfodialog.h" +#include "settings.h" +#include <QMessageBox> + +class FileListModel : public QAbstractItemModel +{ +public: + void clear() + { + m_files.clear(); + endResetModel(); + } + + QModelIndex index(int row, int col, const QModelIndex& ={}) const override + { + return createIndex(row, col); + } + + QModelIndex parent(const QModelIndex&) const override + { + return {}; + } + + int rowCount(const QModelIndex& ={}) const override + { + return static_cast<int>(m_files.size()); + } + + int columnCount(const QModelIndex& ={}) const override + { + return 1; + } + + QVariant data(const QModelIndex& index, int role) const override + { + if (role == Qt::DisplayRole) { + const auto row = index.row(); + if (row < 0) { + return {}; + } + + const auto i = static_cast<std::size_t>(row); + if (i >= m_files.size()) { + return {}; + } + + return m_files[i].text; + } + + return {}; + } + + void add(const QString& rootPath, QString fullPath) + { + QString text = fullPath.mid(rootPath.length() + 1); + m_files.emplace_back(std::move(fullPath), std::move(text)); + } + + void finished() + { + std::sort(m_files.begin(), m_files.end(), [](const auto& a, const auto& b) { + return (naturalCompare(a.text, b.text) < 0); + }); + + endResetModel(); + } + + QString fullPath(const QModelIndex& index) const + { + const auto row = index.row(); + if (row < 0) { + return {}; + } + + const auto i = static_cast<std::size_t>(row); + if (i >= m_files.size()) { + return {}; + } + + return m_files[i].fullPath; + } + +private: + struct File + { + QString fullPath; + QString text; + + File(QString fp, QString t) + : fullPath(std::move(fp)), text(std::move(t)) + { + } + }; + + std::deque<File> m_files; +}; + + +GenericFilesTab::GenericFilesTab( + ModInfoDialogTabContext cx, + QListView* list, QSplitter* sp, + TextEditor* e, QLineEdit* filter) : + ModInfoDialogTab(std::move(cx)), + m_list(list), m_editor(e), m_splitter(sp), m_model(new FileListModel) +{ + m_list->setModel(m_model); + m_editor->setupToolbar(); + + m_splitter->setSizes({200, 1}); + m_splitter->setStretchFactor(0, 0); + m_splitter->setStretchFactor(1, 1); + + m_filter.setEdit(filter); + m_filter.setList(m_list); + + QObject::connect( + m_list->selectionModel(), &QItemSelectionModel::currentRowChanged, + [&](auto current, auto previous){ onSelection(current, previous); }); +} + +void GenericFilesTab::clear() +{ + m_model->clear(); + select({}); + setHasData(false); +} + +bool GenericFilesTab::canClose() +{ + if (!m_editor->dirty()) { + return true; + } + + setFocus(); + + const int res = QMessageBox::question( + parentWidget(), + QObject::tr("Save changes?"), + QObject::tr("Save changes to \"%1\"?").arg(m_editor->filename()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + + if (res == QMessageBox::Cancel) { + return false; + } + + if (res == QMessageBox::Yes) { + m_editor->save(); + } + + return true; +} + +bool GenericFilesTab::feedFile(const QString& rootPath, const QString& fullPath) +{ + static constexpr const char* extensions[] = { + ".txt" + }; + + for (const auto* e : extensions) { + if (wantsFile(rootPath, fullPath)) { + m_model->add(rootPath, fullPath); + return true; + } + } + + return false; +} + +void GenericFilesTab::update() +{ + m_model->finished(); + setHasData(m_model->rowCount() > 0); +} + +void GenericFilesTab::saveState(Settings& s) +{ + saveWidgetState(s.directInterface(), m_splitter); +} + +void GenericFilesTab::restoreState(const Settings& s) +{ + restoreWidgetState(s.directInterface(), m_splitter); +} + +void GenericFilesTab::onSelection( + const QModelIndex& current, const QModelIndex& previous) +{ + if (!canClose()) { + m_list->selectionModel()->select(previous, QItemSelectionModel::Current); + return; + } + + select(current); +} + +void GenericFilesTab::select(const QModelIndex& index) +{ + if (!index.isValid()) { + m_editor->clear(); + m_editor->setEnabled(false); + return; + } + + m_editor->setEnabled(true); + m_editor->load(m_model->fullPath(m_filter.map(index))); +} + + +TextFilesTab::TextFilesTab(ModInfoDialogTabContext cx) + : GenericFilesTab(cx, + cx.ui->textFileList, cx.ui->tabTextSplitter, + cx.ui->textFileEditor, cx.ui->textFileFilter) +{ +} + +bool TextFilesTab::wantsFile(const QString& rootPath, const QString& fullPath) const +{ + static const QString extensions[] = {".txt"}; + + for (const auto& e : extensions) { + if (fullPath.endsWith(e, Qt::CaseInsensitive)) { + return true; + } + } + + return false; +} + +IniFilesTab::IniFilesTab(ModInfoDialogTabContext cx) + : GenericFilesTab(cx, + cx.ui->iniFileList, cx.ui->tabIniSplitter, + cx.ui->iniFileEditor, cx.ui->iniFileFilter) +{ +} + +bool IniFilesTab::wantsFile(const QString& rootPath, const QString& fullPath) const +{ + static const QString extensions[] = {".ini", ".cfg"}; + static const QString meta("meta.ini"); + + for (const auto& e : extensions) { + if (fullPath.endsWith(e, Qt::CaseInsensitive)) { + if (!fullPath.endsWith(meta)) { + return true; + } + } + } + + return false; +} diff --git a/src/modinfodialogtextfiles.h b/src/modinfodialogtextfiles.h new file mode 100644 index 00000000..725ac999 --- /dev/null +++ b/src/modinfodialogtextfiles.h @@ -0,0 +1,64 @@ +#ifndef MODINFODIALOGTEXTFILES_H +#define MODINFODIALOGTEXTFILES_H + +#include "modinfodialogtab.h" +#include "filterwidget.h" +#include <QSplitter> +#include <QListView> + +class FileListItem; +class FileListModel; +class TextEditor; + +class GenericFilesTab : public ModInfoDialogTab +{ + Q_OBJECT; + +public: + void clear() override; + bool canClose() override; + bool feedFile(const QString& rootPath, const QString& fullPath) override; + void update() override; + void saveState(Settings& s) override; + void restoreState(const Settings& s) override; + +protected: + QListView* m_list; + TextEditor* m_editor; + QSplitter* m_splitter; + FileListModel* m_model; + FilterWidget m_filter; + + GenericFilesTab( + ModInfoDialogTabContext cx, + QListView* list, QSplitter* splitter, + TextEditor* editor, QLineEdit* filter); + + virtual bool wantsFile(const QString& rootPath, const QString& fullPath) const = 0; + +private: + void onSelection(const QModelIndex& current, const QModelIndex& previous); + void select(const QModelIndex& index); +}; + + +class TextFilesTab : public GenericFilesTab +{ +public: + TextFilesTab(ModInfoDialogTabContext cx); + +protected: + bool wantsFile(const QString& rootPath, const QString& fullPath) const override; +}; + + +class IniFilesTab : public GenericFilesTab +{ +public: + IniFilesTab(ModInfoDialogTabContext cx); + +protected: + bool wantsFile(const QString& rootPath, const QString& fullPath) const override; +}; + +#endif // MODINFODIALOGTEXTFILES_H diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index a271c4e8..448447e1 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -100,7 +100,70 @@ void ModInfoRegular::readMeta() m_Repository = metaFile.value("repository", "Nexus").toString(); m_Converted = metaFile.value("converted", false).toBool(); m_Validated = metaFile.value("validated", false).toBool(); - m_URL = metaFile.value("url", "").toString(); + + // this handles changes to how the URL works after 2.2.0 + // + // in 2.2.0, "hasCustomUrl" does not exist and "url" is only used when the mod + // id is invalid, although it can be set at any time in the mod info dialog + // + // post 2.2.0, a custom url can be set on any mod, whether the mod id is + // valid or not, so an additional flag "hasCustomURL" is required, with a + // corresponding checkbox in the mod info dialog + // + // there are several cases to handle to make sure no data is lost and to + // determine whether the user has set a custom url before: + // + // 1) some mods have an incorrect url set along with a valid mod id; + // there is apparently a bug with the fomod installer that can set the + // url of a mod to a value used by a _previous_ installation + // + // 2) it is possible to set the url even if the mod id is valid, in which + // case it is saved, but never used in 2.2.0 + // + // 3) opening the mod info dialog on the nexus tab for a mod that has a + // valid id will force the url to be the same as what the plugin gives + // back + // + // the algorithm is as follows: + // always read the url from the meta file and store it so this piece of data + // is never lost; the problem then only becomes about whether to enable + // hasCustomURL + // + // if hasCustomURL is present in the meta file, just read that and be + // done with it + // + // if not, then the flag depends on the mod id and the url + // if the mod id is valid, the custom url is disabled; although the url + // could be _set_ by the user when a mod id was valid, it was never + // _used_, so the behaviour won't change + // + // if the mod id is invalid, the url should normally be empty, unless the + // user specified one, in which case hasCustomURL should be true + // (the only case where this fails is if a mod id was valid before and + // the user visited the nexus tab, in which case the url was set + // automatically, but then the id was manually changed to 0 + // + // in that case, the mod id is invalid and the url is not empty, but it + // was never set by the user; this case is impossible to distinguish + // from a user manually entering a url, and so is handled as such) + + // always read the url + m_CustomURL = metaFile.value("url").toString(); + + if (metaFile.contains("hasCustomURL")) { + m_HasCustomURL = metaFile.value("hasCustomURL").toBool(); + } else { + if (m_NexusID > 0) { + // the mod id is valid, disable the custom url + m_HasCustomURL = false; + } else { + if (!m_CustomURL.isEmpty()) { + // the mod id is invalid and the url is not empty, enable it + m_HasCustomURL = true; + } + } + } + m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); m_LastNexusUpdate = QDateTime::fromString(metaFile.value("lastNexusUpdate", "").toString(), Qt::ISODate); m_NexusLastModified = QDateTime::fromString(metaFile.value("nexusLastModified", QDateTime::currentDateTimeUtc()).toString(), Qt::ISODate); @@ -118,7 +181,7 @@ void ModInfoRegular::readMeta() m_EndorsedState = metaFile.value("endorsed", false).toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; } } - + QString categoriesString = metaFile.value("category", "").toString(); QStringList categories = categoriesString.split(',', QString::SkipEmptyParts); @@ -166,7 +229,8 @@ void ModInfoRegular::saveMeta() metaFile.setValue("comments", m_Comments); metaFile.setValue("notes", m_Notes); metaFile.setValue("nexusDescription", m_NexusDescription); - metaFile.setValue("url", m_URL); + metaFile.setValue("url", m_CustomURL); + metaFile.setValue("hasCustomURL", m_HasCustomURL); metaFile.setValue("nexusFileStatus", m_NexusFileStatus); metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate)); @@ -194,10 +258,14 @@ void ModInfoRegular::saveMeta() if (metaFile.status() == QSettings::NoError) { m_MetaInfoChanged = false; } else { - reportError(tr("failed to write %1/meta.ini: error %2").arg(absolutePath()).arg(metaFile.status())); + qCritical() + << QString("failed to write %1/meta.ini: error %2") + .arg(absolutePath()).arg(metaFile.status()); } } else { - reportError(tr("failed to write %1/meta.ini: error %2").arg(absolutePath()).arg(metaFile.status())); + qCritical() + << QString("failed to write %1/meta.ini: error %2") + .arg(absolutePath()).arg(metaFile.status()); } } } @@ -291,15 +359,27 @@ void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNet bool ModInfoRegular::updateNXMInfo() { - QDateTime time = QDateTime::currentDateTimeUtc(); - QDateTime target = m_LastNexusQuery.addDays(1); - if (m_NexusID > 0 && time >= target) { + if (needsDescriptionUpdate()) { m_NexusBridge.requestDescription(m_GameName, m_NexusID, QVariant()); return true; } + return false; } +bool ModInfoRegular::needsDescriptionUpdate() const +{ + if (m_NexusID > 0) { + QDateTime time = QDateTime::currentDateTimeUtc(); + QDateTime target = m_LastNexusQuery.addDays(1); + + if (time >= target) { + return true; + } + } + + return false; +} void ModInfoRegular::setCategory(int categoryID, bool active) { @@ -753,18 +833,27 @@ void ModInfoRegular::setNexusLastModified(QDateTime time) emit modDetailsUpdated(true); } -void ModInfoRegular::setURL(QString const &url) +void ModInfoRegular::setCustomURL(QString const &url) { - m_URL = url; + m_CustomURL = url; m_MetaInfoChanged = true; } -QString ModInfoRegular::getURL() const +QString ModInfoRegular::getCustomURL() const { - return m_URL; + return m_CustomURL; } +void ModInfoRegular::setHasCustomURL(bool b) +{ + m_HasCustomURL = b; + m_MetaInfoChanged = true; +} +bool ModInfoRegular::hasCustomURL() const +{ + return m_HasCustomURL; +} QStringList ModInfoRegular::archives(bool checkOnDisk) { diff --git a/src/modinforegular.h b/src/modinforegular.h index f70487a2..705e66a8 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -397,15 +397,10 @@ public: void readMeta(); - /** - * @brief set the URL for a mod - */ - virtual void setURL(QString const &); - - /** - * @returns the URL for a mod - */ - virtual QString getURL() const; + virtual void setHasCustomURL(bool b) override; + virtual bool hasCustomURL() const override; + virtual void setCustomURL(QString const &) override; + virtual QString getCustomURL() const override; private: @@ -432,7 +427,8 @@ private: QString m_Notes; QString m_NexusDescription; QString m_Repository; - QString m_URL; + QString m_CustomURL; + bool m_HasCustomURL; QString m_GameName; mutable QStringList m_Archives; @@ -464,6 +460,7 @@ private: mutable std::vector<ModInfo::EContent> m_CachedContent; mutable QTime m_LastContentCheck; + bool needsDescriptionUpdate() const; }; diff --git a/src/modlist.cpp b/src/modlist.cpp index e2398cd6..7b71355c 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -561,6 +561,7 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) m_Modified = true; m_LastCheck.restart(); emit modlistChanged(index, role); + emit tutorialModlistUpdate(); } result = true; emit dataChanged(index, index); @@ -596,6 +597,7 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) if (ok) { info->setNexusID(newID); emit modlistChanged(index, role); + emit tutorialModlistUpdate(); result = true; } else { result = false; @@ -1375,6 +1377,7 @@ bool ModList::toggleSelection(QAbstractItemView *itemView) m_Profile->setModsEnabled(modsToEnable, modsToDisable); emit modlistChanged(dirtyMods, 0); + emit tutorialModlistUpdate(); m_Modified = true; m_LastCheck.restart(); diff --git a/src/modlist.h b/src/modlist.h index 402d662f..5ce32f6e 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -250,6 +250,11 @@ signals: void modlistChanged(const QModelIndexList &indicies, int role);
/**
+ * @brief QML seems to handle overloaded signals poorly - create unique signal for tutorials
+ */
+ void tutorialModlistUpdate();
+
+ /**
* @brief emitted to have all selected mods deleted
*/
void removeSelectedMods();
diff --git a/src/motddialog.cpp b/src/motddialog.cpp index 96d88542..ca1e60ad 100644 --- a/src/motddialog.cpp +++ b/src/motddialog.cpp @@ -21,8 +21,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "bbcode.h"
#include "utility.h"
#include "ui_motddialog.h"
+#include "organizercore.h"
+#include <utility.h>
#include <Shlwapi.h>
+using namespace MOBase;
+
MotDDialog::MotDDialog(const QString &message, QWidget *parent)
: QDialog(parent), ui(new Ui::MotDDialog)
{
@@ -43,5 +47,5 @@ void MotDDialog::on_okButton_clicked() void MotDDialog::linkClicked(const QUrl &url)
{
- ::ShellExecuteW(nullptr, L"open", MOBase::ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ shell::OpenLink(url);
}
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 9db0d54e..ee9acf2c 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -38,6 +38,17 @@ using namespace MOBase; using namespace MOShared; +void throttledWarning(const APIUserAccount& user) +{ + qCritical() << + QString( + "You have fewer than %1 requests remaining (%2). Only downloads and " + "login validation are being allowed.") + .arg(APIUserAccount::ThrottleThreshold) + .arg(user.remainingRequests()); +} + + NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule) : m_Interface(NexusInterface::instance(pluginContainer)) , m_SubModule(subModule) @@ -179,9 +190,39 @@ void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVar QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); +APILimits NexusInterface::defaultAPILimits() +{ + // https://app.swaggerhub.com/apis-docs/NexusMods/nexus-mods_public_api_params_in_form_data/1.0#/ + const int MaxDaily = 2500; + const int MaxHourly = 100; + + APILimits limits; + + limits.maxDailyRequests = MaxDaily; + limits.remainingDailyRequests = MaxDaily; + limits.maxHourlyRequests = MaxHourly; + limits.remainingHourlyRequests = MaxHourly; + + return limits; +} + +APILimits NexusInterface::parseLimits(const QNetworkReply* reply) +{ + APILimits limits; + + limits.maxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); + limits.remainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); + limits.maxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); + limits.remainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); + + return limits; +} + + NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_PluginContainer(pluginContainer), m_RemainingDailyRequests(2500), m_RemainingHourlyRequests(100), m_MaxDailyRequests(2500), m_MaxHourlyRequests(100) + : m_PluginContainer(pluginContainer) { + m_User.limits(defaultAPILimits()); m_MOVersion = createVersionInfo(); m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); @@ -216,13 +257,10 @@ void NexusInterface::loginCompleted() nextRequest(); } -void NexusInterface::setRateMax(const QString&, bool, std::tuple<int, int, int, int> limits) +void NexusInterface::setUserAccount(const APIUserAccount& user) { - m_RemainingDailyRequests = std::get<0>(limits); - m_MaxDailyRequests = std::get<1>(limits); - m_RemainingHourlyRequests = std::get<2>(limits); - m_MaxHourlyRequests = std::get<3>(limits); - emit requestsChanged(m_RequestQueue.size(), limits); + m_User = user; + emit requestsChanged(getAPIStats(), m_User); } void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) @@ -361,70 +399,70 @@ int NexusInterface::requestDescription(QString gameName, int modID, QObject *rec int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + if (m_User.shouldThrottle()) { + throttledWarning(m_User); + return -1; + } - connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; - } - qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") - .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); - return -1; + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; } int NexusInterface::requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, const QString &subModule, const MOBase::IPluginGame *game) { - if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { - NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + if (m_User.shouldThrottle()) { + throttledWarning(m_User); + return -1; + } - connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), - receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection); + NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; - } - qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") - .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); - return -1; + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; } int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule) { - if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { - IPluginGame *game = getGame(gameName); - if (game == nullptr) { - qCritical("requestUpdates can't find plugin for %s", qUtf8Printable(gameName)); - return -1; - } + if (m_User.shouldThrottle()) { + throttledWarning(m_User); + return -1; + } - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + IPluginGame *game = getGame(gameName); + if (game == nullptr) { + qCritical("requestUpdates can't find plugin for %s", qUtf8Printable(gameName)); + return -1; + } - connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; - } - qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") - .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); - return -1; + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; } @@ -519,23 +557,23 @@ int NexusInterface::requestEndorsementInfo(QObject *receiver, QVariant userData, int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { - NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); - requestInfo.m_Endorse = endorse; - m_RequestQueue.enqueue(requestInfo); + if (m_User.shouldThrottle()) { + throttledWarning(m_User); + return -1; + } - connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); + requestInfo.m_Endorse = endorse; + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; - } - qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") - .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); - return -1; + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; } int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule) @@ -556,23 +594,23 @@ int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, co int NexusInterface::requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData, subModule, game); - requestInfo.m_Track = track; - m_RequestQueue.enqueue(requestInfo); + if (m_User.shouldThrottle()) { + throttledWarning(m_User); + return -1; + } - connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)), - receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData, subModule, game); + requestInfo.m_Track = track; + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)), + receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; - } - qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") - .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); - return -1; + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; } int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, @@ -633,16 +671,16 @@ void NexusInterface::nextRequest() } else if (getAccessManager()->validateWaiting()) { return; } else { - qCritical("You must authorize MO2 in Settings -> Nexus to use the Nexus API."); + qCritical() << tr("You must authorize MO2 in Settings -> Nexus to use the Nexus API."); } } - if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) <= 0) { + if (m_User.exhausted()) { m_RequestQueue.clear(); QTime time = QTime::currentTime(); QTime targetTime; targetTime.setHMS((time.hour() + 1) % 23, 5, 0); - QString warning("You've exceeded the Nexus API rate limit and requests are now being throttled. " + QString warning = tr("You've exceeded the Nexus API rate limit and requests are now being throttled. " "Your next batch of requests will be available in approximately %1 minutes and %2 seconds."); qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60); return; @@ -688,11 +726,16 @@ void NexusInterface::nextRequest() } break; case NXMRequestInfo::TYPE_DOWNLOADURL: { ModRepositoryFileInfo *fileInfo = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(info.m_UserData)); - if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires) + if (m_User.type() == APIUserAccountTypes::Premium) { + url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + } else if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires && fileInfo->nexusDownloadUser == m_User.id().toInt()) { url = QString("%1/games/%2/mods/%3/files/%4/download_link?key=%5&expires=%6") .arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID).arg(fileInfo->nexusKey).arg(fileInfo->nexusExpires); - else - url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + } else { + qWarning() << tr("Aborting download: Either you clicked on a premium-only link and your account is not premium, " + "or the download link was generated by a different account than the one stored in Mod Organizer."); + return; + } } break; case NXMRequestInfo::TYPE_ENDORSEMENTS: { url = QString("%1/user/endorsements").arg(info.m_URL); @@ -767,23 +810,16 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter) if (iter->m_AllowedErrors.contains(error) && iter->m_AllowedErrors[error].contains(statusCode)) { // These errors are allows to silently happen. They should be handled in nxmRequestFailed below. } else if (statusCode == 429) { - m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); - m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); - m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); - m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); + m_User.limits(parseLimits(reply)); - if (m_RemainingDailyRequests || m_RemainingHourlyRequests) + if (!m_User.exhausted()) { qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team."); - else + } + else { qWarning("All API requests have been consumed and are now being denied."); + } - emit requestsChanged(m_RequestQueue.size(), std::tuple<int, int, int, int>(std::make_tuple( - m_RemainingDailyRequests, - m_MaxDailyRequests, - m_RemainingHourlyRequests, - m_MaxHourlyRequests - ))); - + emit requestsChanged(getAPIStats(), m_User); qWarning("Error: %s", reply->errorString().toUtf8().constData()); } else { qWarning("request failed: %s", reply->errorString().toUtf8().constData()); @@ -858,17 +894,8 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter) } break; } - m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt(); - m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt(); - m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt(); - m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt(); - - emit requestsChanged(m_RequestQueue.size(), std::tuple<int, int, int, int>(std::make_tuple( - m_RemainingDailyRequests, - m_MaxDailyRequests, - m_RemainingHourlyRequests, - m_MaxHourlyRequests - ))); + m_User.limits(parseLimits(reply)); + emit requestsChanged(getAPIStats(), m_User); } else { emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response")); } @@ -925,6 +952,20 @@ void NexusInterface::requestTimeout() } } +APIUserAccount NexusInterface::getAPIUserAccount() const +{ + return m_User; +} + +APIStats NexusInterface::getAPIStats() const +{ + APIStats stats; + stats.requestsQueued = m_RequestQueue.size(); + + return stats; +} + + namespace { QString get_management_url() { diff --git a/src/nexusinterface.h b/src/nexusinterface.h index ac7f61c5..6e768149 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -20,6 +20,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #ifndef NEXUSINTERFACE_H #define NEXUSINTERFACE_H +#include "apiuseraccount.h" + #include <utility.h> #include <versioninfo.h> #include <imodrepositorybridge.h> @@ -39,6 +41,7 @@ namespace MOBase { class IPluginGame; } class NexusInterface; class NXMAccessManager; + /** * @brief convenience class to make nxm requests easier * usually, all objects that started a nxm request will be signaled if one finished. @@ -53,7 +56,6 @@ class NexusBridge : public MOBase::IModRepositoryBridge Q_OBJECT public: - NexusBridge(PluginContainer *pluginContainer, const QString &subModule = ""); /** @@ -147,6 +149,8 @@ public: }; public: + static APILimits defaultAPILimits(); + static APILimits parseLimits(const QNetworkReply* reply); ~NexusInterface(); @@ -380,7 +384,7 @@ public: /** * */ - int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule, + int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); /** @@ -395,6 +399,9 @@ public: std::vector<std::pair<QString, QString>> getGameChoices(const MOBase::IPluginGame *game); + APIUserAccount getAPIUserAccount() const; + APIStats getAPIStats() const; + public: /** @@ -459,11 +466,11 @@ signals: void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); - void requestsChanged(int queueCount, std::tuple<int,int,int,int> requestsRemaining); + void requestsChanged(const APIStats& stats, const APIUserAccount& user); public slots: - void setRateMax(const QString&, bool, std::tuple<int,int,int,int> limits); + void setUserAccount(const APIUserAccount& user); private slots: @@ -534,23 +541,13 @@ private: QString getOldModsURL(QString gameName) const; private: - QNetworkDiskCache *m_DiskCache; - NXMAccessManager *m_AccessManager; - std::list<NXMRequestInfo> m_ActiveRequest; QQueue<NXMRequestInfo> m_RequestQueue; - MOBase::VersionInfo m_MOVersion; - PluginContainer *m_PluginContainer; - - int m_RemainingDailyRequests; - int m_RemainingHourlyRequests; - int m_MaxDailyRequests; - int m_MaxHourlyRequests; - + APIUserAccount m_User; }; #endif // NEXUSINTERFACE_H diff --git a/src/nexusmanualkey.ui b/src/nexusmanualkey.ui new file mode 100644 index 00000000..847bdd61 --- /dev/null +++ b/src/nexusmanualkey.ui @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NexusManualKeyDialog</class> + <widget class="QDialog" name="NexusManualKeyDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>452</width> + <height>236</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manual Nexus API Key</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,0"> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget_2" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>1. Get your personal API key</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="openBrowser"> + <property name="text"> + <string>Open Browser</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_3" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,1"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget_4" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>2. Enter your personal API key</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>115</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="paste"> + <property name="text"> + <string>Paste</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="clear"> + <property name="text"> + <string>Clear</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="key"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Ignored" vsizetype="Ignored"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>1</width> + <height>1</height> + </size> + </property> + <property name="placeholderText"> + <string>Enter API key here</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_5" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string><b>Sharing this key with anyone could get your Nexus account banned. You can always revoke this key from your account on the Nexus website.</b></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>NexusManualKeyDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>NexusManualKeyDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index b1e1ad89..ee6c03f1 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -18,8 +18,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */ #include "nxmaccessmanager.h" - #include "iplugingame.h" +#include "nexusinterface.h" #include "nxmurl.h" #include "report.h" #include "utility.h" @@ -176,16 +176,21 @@ bool NXMAccessManager::validateWaiting() const } -void NXMAccessManager::apiCheck(const QString &apiKey) +void NXMAccessManager::apiCheck(const QString &apiKey, bool force) { if (m_ValidateReply != nullptr) { return; } + if (force) { + m_ValidateState = VALIDATE_NOT_CHECKED; + } + if (m_ValidateState == VALIDATE_VALID) { emit validateSuccessful(false); return; } + m_ApiKey = apiKey; startValidationCheck(); } @@ -214,6 +219,13 @@ QString NXMAccessManager::apiKey() const return m_ApiKey; } +void NXMAccessManager::clearApiKey() +{ + m_ApiKey = ""; + m_ValidateState = VALIDATE_NOT_VALID; + + emit credentialsReceived(APIUserAccount()); +} void NXMAccessManager::validateTimeout() { @@ -229,7 +241,6 @@ void NXMAccessManager::validateTimeout() if (m_ValidateReply != nullptr) { m_ValidateReply->deleteLater(); m_ValidateReply = nullptr; - m_ValidateAttempted = false; // this usually means we might have success later } emit validateFailed(tr("There was a timeout during the request")); @@ -272,23 +283,25 @@ void NXMAccessManager::validateFinished() if (!jdoc.isNull()) { QJsonObject credentialsData = jdoc.object(); if (credentialsData.contains("user_id")) { + int id = credentialsData.value("user_id").toInt(); QString name = credentialsData.value("name").toString(); bool premium = credentialsData.value("is_premium").toBool(); - std::tuple<int, int, int, int> limits(std::make_tuple( - m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(), - m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(), - m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(), - m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt() - )); + const auto user = APIUserAccount() + .id(QString("%1").arg(id)) + .name(name) + .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular) + .limits(NexusInterface::parseLimits(m_ValidateReply)); - emit credentialsReceived(name, premium, limits); + + emit credentialsReceived(user); m_ValidateReply->deleteLater(); m_ValidateReply = nullptr; m_ValidateState = VALIDATE_VALID; emit validateSuccessful(true); + } else { m_ApiKey.clear(); m_ValidateState = VALIDATE_NOT_VALID; diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index da7736cc..1bdeae40 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -20,7 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #ifndef NXMACCESSMANAGER_H #define NXMACCESSMANAGER_H - +#include "apiuseraccount.h" #include <QNetworkAccessManager> #include <QTimer> #include <QNetworkReply> @@ -46,7 +46,7 @@ public: bool validateAttempted() const; bool validateWaiting() const; - void apiCheck(const QString &apiKey); + void apiCheck(const QString &apiKey, bool force=false); void showCookies() const; @@ -55,6 +55,7 @@ public: QString userAgent(const QString &subModule = QString()) const; QString apiKey() const; + void clearApiKey(); void startValidationCheck(); @@ -78,7 +79,7 @@ signals: void validateFailed(const QString &message); - void credentialsReceived(const QString &userName, bool premium, std::tuple<int, int, int, int> limits); + void credentialsReceived(const APIUserAccount& user); private slots: @@ -93,7 +94,6 @@ protected: QIODevice *device); private: - QTimer m_ValidateTimeout; QNetworkReply *m_ValidateReply; QProgressDialog *m_ProgressDialog { nullptr }; @@ -102,7 +102,6 @@ private: QString m_ApiKey; - bool m_ValidateAttempted; enum { VALIDATE_NOT_CHECKED, VALIDATE_CHECKING, @@ -111,7 +110,6 @@ private: VALIDATE_REFUSED, VALIDATE_VALID } m_ValidateState = VALIDATE_NOT_CHECKED; - }; #endif // NXMACCESSMANAGER_H diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 28ddc09c..1aa831a0 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -15,112 +15,112 @@ <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="141"/> - <source>Used Software</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="aboutdialog.ui" line="154"/> - <source>Thanks</source> + <location filename="aboutdialog.ui" line="156"/> + <source><html><head/><body><p>Source code can be found at <a href="https://github.com/ModOrganizer2/modorganizer"><span style=" text-decoration: underline; color:#007af4;">GitHub</span></a>.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="160"/> - <source>Lead Developers/ Maintainers</source> + <location filename="aboutdialog.ui" line="164"/> + <source>Used Software</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="170"/> - <source>LePresidente (Project Lead)</source> + <location filename="aboutdialog.ui" line="177"/> + <source>Thanks</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="201"/> - <source>Mo2 devs and Contributors</source> + <location filename="aboutdialog.ui" line="183"/> + <source>Lead Developers && Maintainers</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="216"/> - <source>Project579</source> + <location filename="aboutdialog.ui" line="193"/> + <source>LePresidente (Project Lead)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="221"/> - <source>przester</source> + <location filename="aboutdialog.ui" line="219"/> + <source>MO2 Developers && Contributors</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="232"/> + <location filename="aboutdialog.ui" line="260"/> <source>Translators</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="257"/> + <location filename="aboutdialog.ui" line="285"/> <source>Cyb3r (Dutch)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="267"/> + <location filename="aboutdialog.ui" line="295"/> <source>fruttyx (French)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="282"/> + <location filename="aboutdialog.ui" line="310"/> <source>Yoplala (French)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="287"/> + <location filename="aboutdialog.ui" line="315"/> <source>Faron (German)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="297"/> + <location filename="aboutdialog.ui" line="325"/> + <source>yohru (Japanese)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="aboutdialog.ui" line="330"/> <source>Mordan (Greek)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="310"/> + <location filename="aboutdialog.ui" line="343"/> <source>Yoosk (Polish)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="315"/> + <location filename="aboutdialog.ui" line="348"/> <source>Brgodfx (Portuguese)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="320"/> + <location filename="aboutdialog.ui" line="353"/> <source>zDas (Portuguese)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="340"/> + <location filename="aboutdialog.ui" line="373"/> <source>Jax (Swedish)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="345"/> - <source>yohru (Japanese)</source> + <location filename="aboutdialog.ui" line="378"/> + <source>Nubbie (Swedish)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="350"/> - <source>...and all other Transifex contributors!</source> + <location filename="aboutdialog.ui" line="383"/> + <source>...and all other contributors!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="364"/> + <location filename="aboutdialog.ui" line="397"/> <source>Other Supporters && Contributors</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="374"/> + <location filename="aboutdialog.ui" line="407"/> <source>Tannin (Original Creator)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="491"/> + <location filename="aboutdialog.ui" line="524"/> <source>Close</source> <translation type="unfinished"></translation> </message> @@ -171,6 +171,24 @@ p, li { white-space: pre-wrap; } </message> </context> <context> + <name>AdvancedConflictListModel</name> + <message> + <location filename="modinfodialogconflicts.cpp" line="334"/> + <source>Overwrites</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="335"/> + <source>File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="336"/> + <source>Overwritten By</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>BrowserDialog</name> <message> <location filename="browserdialog.ui" line="14"/> @@ -269,6 +287,39 @@ p, li { white-space: pre-wrap; } </message> </context> <context> + <name>ConflictsTab</name> + <message> + <location filename="modinfodialogconflicts.cpp" line="695"/> + <source>&Hide</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="700"/> + <source>&Unhide</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="703"/> + <source>&Open/Execute</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="706"/> + <source>&Preview</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="709"/> + <source>Open in &Explorer</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="712"/> + <source>&Go to...</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>CredentialsDialog</name> <message> <location filename="credentialsdialog.ui" line="14"/> @@ -323,93 +374,108 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="downloadlist.cpp" line="72"/> - <source>Size</source> + <source>Mod name</source> <translation type="unfinished"></translation> </message> <message> <location filename="downloadlist.cpp" line="73"/> - <source>Status</source> + <source>Version</source> <translation type="unfinished"></translation> </message> <message> <location filename="downloadlist.cpp" line="74"/> + <source>Nexus ID</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="downloadlist.cpp" line="75"/> + <source>Size</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="downloadlist.cpp" line="76"/> + <source>Status</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="downloadlist.cpp" line="77"/> <source>Filetime</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="89"/> + <location filename="downloadlist.cpp" line="92"/> <source>< game %1 mod %2 file %3 ></source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="90"/> + <location filename="downloadlist.cpp" line="93"/> <source>Unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="91"/> + <location filename="downloadlist.cpp" line="94"/> <source>Pending</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="101"/> + <location filename="downloadlist.cpp" line="128"/> <source>Started</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="102"/> + <location filename="downloadlist.cpp" line="129"/> <source>Canceling</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="103"/> + <location filename="downloadlist.cpp" line="130"/> <source>Pausing</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="104"/> + <location filename="downloadlist.cpp" line="131"/> <source>Canceled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="105"/> + <location filename="downloadlist.cpp" line="132"/> <source>Paused</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="106"/> + <location filename="downloadlist.cpp" line="133"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="107"/> - <location filename="downloadlist.cpp" line="108"/> - <location filename="downloadlist.cpp" line="109"/> + <location filename="downloadlist.cpp" line="134"/> + <location filename="downloadlist.cpp" line="135"/> + <location filename="downloadlist.cpp" line="136"/> <source>Fetching Info</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="110"/> + <location filename="downloadlist.cpp" line="137"/> <source>Downloaded</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="111"/> + <location filename="downloadlist.cpp" line="138"/> <source>Installed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="112"/> + <location filename="downloadlist.cpp" line="139"/> <source>Uninstalled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="130"/> + <location filename="downloadlist.cpp" line="157"/> <source>Pending download</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlist.cpp" line="134"/> + <location filename="downloadlist.cpp" line="161"/> <source>Information missing, please select "Query Info" from the context menu to re-retrieve.</source> <translation type="unfinished"></translation> </message> @@ -417,145 +483,156 @@ p, li { white-space: pre-wrap; } <context> <name>DownloadListWidget</name> <message> - <location filename="downloadlistwidget.cpp" line="201"/> + <location filename="downloadlistwidget.cpp" line="210"/> <source>Install</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="203"/> + <location filename="downloadlistwidget.cpp" line="212"/> <source>Query Info</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="205"/> + <location filename="downloadlistwidget.cpp" line="214"/> <source>Visit on Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="206"/> + <location filename="downloadlistwidget.cpp" line="215"/> <source>Open File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="207"/> - <location filename="downloadlistwidget.cpp" line="219"/> - <location filename="downloadlistwidget.cpp" line="224"/> + <location filename="downloadlistwidget.cpp" line="216"/> + <location filename="downloadlistwidget.cpp" line="228"/> + <location filename="downloadlistwidget.cpp" line="233"/> <source>Show in Folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="211"/> - <location filename="downloadlistwidget.cpp" line="222"/> + <location filename="downloadlistwidget.cpp" line="220"/> + <location filename="downloadlistwidget.cpp" line="231"/> <source>Delete</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="213"/> + <location filename="downloadlistwidget.cpp" line="222"/> <source>Un-Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="215"/> + <location filename="downloadlistwidget.cpp" line="224"/> <source>Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="217"/> + <location filename="downloadlistwidget.cpp" line="226"/> <source>Cancel</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="218"/> + <location filename="downloadlistwidget.cpp" line="227"/> <source>Pause</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="223"/> + <location filename="downloadlistwidget.cpp" line="232"/> <source>Resume</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="229"/> - <source>Delete Installed...</source> + <location filename="downloadlistwidget.cpp" line="238"/> + <source>Delete Installed Downloads...</source> + <oldsource>Delete Installed...</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="230"/> - <source>Delete Uninstalled...</source> + <location filename="downloadlistwidget.cpp" line="239"/> + <source>Delete Uninstalled Downloads...</source> + <oldsource>Delete Uninstalled...</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="231"/> - <source>Delete All...</source> + <location filename="downloadlistwidget.cpp" line="240"/> + <source>Delete All Downloads...</source> + <oldsource>Delete All...</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="235"/> + <location filename="downloadlistwidget.cpp" line="244"/> <source>Hide Installed...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="236"/> + <location filename="downloadlistwidget.cpp" line="245"/> <source>Hide Uninstalled...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="237"/> + <location filename="downloadlistwidget.cpp" line="246"/> <source>Hide All...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="239"/> + <location filename="downloadlistwidget.cpp" line="248"/> <source>Un-Hide All...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="262"/> - <location filename="downloadlistwidget.cpp" line="317"/> + <location filename="downloadlistwidget.cpp" line="271"/> <location filename="downloadlistwidget.cpp" line="326"/> <location filename="downloadlistwidget.cpp" line="335"/> + <location filename="downloadlistwidget.cpp" line="344"/> <source>Delete Files?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="263"/> - <source>This will permanently delete the selected download.</source> + <location filename="downloadlistwidget.cpp" line="272"/> + <source>This will permanently delete the selected download. + +Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="318"/> - <source>This will remove all finished downloads from this list and from disk.</source> + <location filename="downloadlistwidget.cpp" line="327"/> + <source>This will remove all finished downloads from this list and from disk. + +Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="327"/> - <source>This will remove all installed downloads from this list and from disk.</source> + <location filename="downloadlistwidget.cpp" line="336"/> + <source>This will remove all installed downloads from this list and from disk. + +Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="336"/> - <source>This will remove all uninstalled downloads from this list and from disk.</source> + <location filename="downloadlistwidget.cpp" line="345"/> + <source>This will remove all uninstalled downloads from this list and from disk. + +Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="344"/> <location filename="downloadlistwidget.cpp" line="353"/> <location filename="downloadlistwidget.cpp" line="362"/> - <source>Are you sure?</source> + <location filename="downloadlistwidget.cpp" line="371"/> + <source>Hide Files?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="345"/> + <location filename="downloadlistwidget.cpp" line="354"/> <source>This will remove all finished downloads from this list (but NOT from disk).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="354"/> + <location filename="downloadlistwidget.cpp" line="363"/> <source>This will remove all installed downloads from this list (but NOT from disk).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistwidget.cpp" line="363"/> + <location filename="downloadlistwidget.cpp" line="372"/> <source>This will remove all uninstalled downloads from this list (but NOT from disk).</source> <translation type="unfinished"></translation> </message> @@ -588,17 +665,17 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="557"/> + <location filename="downloadmanager.cpp" line="568"/> <source>Wrong Game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="557"/> + <location filename="downloadmanager.cpp" line="568"/> <source>The download link is for a mod for "%1" but this instance of MO has been set up for "%2".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="565"/> + <location filename="downloadmanager.cpp" line="576"/> <source>There is already a download queued for this file. Mod %1 @@ -606,12 +683,12 @@ File %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="571"/> + <location filename="downloadmanager.cpp" line="582"/> <source>Already Queued</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="580"/> + <location filename="downloadmanager.cpp" line="591"/> <source>There is already a download started for this file. Mod %1: %2 @@ -619,266 +696,266 @@ File %3: %4</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="615"/> + <location filename="downloadmanager.cpp" line="626"/> <source>Already Started</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="643"/> - <location filename="downloadmanager.cpp" line="776"/> + <location filename="downloadmanager.cpp" line="655"/> + <location filename="downloadmanager.cpp" line="788"/> <source>remove: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="662"/> + <location filename="downloadmanager.cpp" line="674"/> <source>failed to delete %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="669"/> + <location filename="downloadmanager.cpp" line="681"/> <source>failed to delete meta file for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="729"/> + <location filename="downloadmanager.cpp" line="741"/> <source>restore: invalid download index: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="798"/> + <location filename="downloadmanager.cpp" line="810"/> <source>cancel: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="811"/> + <location filename="downloadmanager.cpp" line="823"/> <source>pause: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="833"/> + <location filename="downloadmanager.cpp" line="845"/> <source>resume: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="844"/> + <location filename="downloadmanager.cpp" line="856"/> <source>resume (int): invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="868"/> + <location filename="downloadmanager.cpp" line="880"/> <source>No known download urls. Sorry, this download can't be resumed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="909"/> - <location filename="downloadmanager.cpp" line="961"/> + <location filename="downloadmanager.cpp" line="921"/> + <location filename="downloadmanager.cpp" line="973"/> <source>query: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="931"/> + <location filename="downloadmanager.cpp" line="943"/> <source>Please enter the nexus mod id</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="931"/> + <location filename="downloadmanager.cpp" line="943"/> <source>Mod ID:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="942"/> + <location filename="downloadmanager.cpp" line="954"/> <source>Please select the source game code for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1001"/> + <location filename="downloadmanager.cpp" line="1013"/> <source>VisitNexus: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1022"/> + <location filename="downloadmanager.cpp" line="1034"/> <source>Nexus ID for this Mod is unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1029"/> + <location filename="downloadmanager.cpp" line="1041"/> <source>OpenFile: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1046"/> + <location filename="downloadmanager.cpp" line="1058"/> <source>OpenFileInDownloadsFolder: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1082"/> + <location filename="downloadmanager.cpp" line="1093"/> <source>get pending: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1091"/> + <location filename="downloadmanager.cpp" line="1102"/> <source>get path: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1100"/> + <location filename="downloadmanager.cpp" line="1111"/> <source>Main</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1101"/> + <location filename="downloadmanager.cpp" line="1112"/> <source>Update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1102"/> + <location filename="downloadmanager.cpp" line="1113"/> <source>Optional</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1103"/> + <location filename="downloadmanager.cpp" line="1114"/> <source>Old</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1104"/> + <location filename="downloadmanager.cpp" line="1115"/> <source>Miscellaneous</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1105"/> + <location filename="downloadmanager.cpp" line="1116"/> <source>Deleted</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1106"/> + <location filename="downloadmanager.cpp" line="1117"/> <source>Unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1113"/> + <location filename="downloadmanager.cpp" line="1124"/> <source>display name: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1133"/> + <location filename="downloadmanager.cpp" line="1144"/> <source>file name: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1142"/> + <location filename="downloadmanager.cpp" line="1153"/> <source>file time: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1156"/> + <location filename="downloadmanager.cpp" line="1167"/> <source>file size: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1166"/> + <location filename="downloadmanager.cpp" line="1177"/> <source>progress: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1176"/> + <location filename="downloadmanager.cpp" line="1187"/> <source>state: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1186"/> + <location filename="downloadmanager.cpp" line="1197"/> <source>infocomplete: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1201"/> - <location filename="downloadmanager.cpp" line="1209"/> + <location filename="downloadmanager.cpp" line="1212"/> + <location filename="downloadmanager.cpp" line="1220"/> <source>mod id: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1217"/> + <location filename="downloadmanager.cpp" line="1228"/> <source>ishidden: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1226"/> + <location filename="downloadmanager.cpp" line="1237"/> <source>file info: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1236"/> + <location filename="downloadmanager.cpp" line="1247"/> <source>mark installed: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1281"/> + <location filename="downloadmanager.cpp" line="1292"/> <source>mark uninstalled: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1458"/> + <location filename="downloadmanager.cpp" line="1469"/> <source>Memory allocation error (in processing progress event).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1468"/> + <location filename="downloadmanager.cpp" line="1479"/> <source>Memory allocation error (in processing downloaded data).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1584"/> + <location filename="downloadmanager.cpp" line="1595"/> <source>Information updated</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1586"/> - <location filename="downloadmanager.cpp" line="1601"/> + <location filename="downloadmanager.cpp" line="1597"/> + <location filename="downloadmanager.cpp" line="1612"/> <source>No matching file found on Nexus! Maybe this file is no longer available or it was renamed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1588"/> + <location filename="downloadmanager.cpp" line="1599"/> <source>No file on Nexus matches the selected file by name. Please manually choose the correct one.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1716"/> + <location filename="downloadmanager.cpp" line="1727"/> <source>No download server available. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1881"/> + <location filename="downloadmanager.cpp" line="1892"/> <source>Failed to request file info from nexus: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1908"/> + <location filename="downloadmanager.cpp" line="1919"/> <source>Warning: Content type is: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1913"/> + <location filename="downloadmanager.cpp" line="1924"/> <source>Download header content length: %1 downloaded file size: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1915"/> + <location filename="downloadmanager.cpp" line="1926"/> <source>Download failed: %1 (%2)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1937"/> + <location filename="downloadmanager.cpp" line="1948"/> <source>We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2020"/> + <location filename="downloadmanager.cpp" line="2031"/> <source>failed to re-open %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2061"/> + <location filename="downloadmanager.cpp" line="2072"/> <source>Unable to write download to drive (return %1). Check the drive's available storage. @@ -889,216 +966,307 @@ Canceling download "%2"...</source> <context> <name>EditExecutablesDialog</name> <message> - <location filename="editexecutablesdialog.ui" line="20"/> + <location filename="editexecutablesdialog.ui" line="14"/> <source>Modify Executables</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="26"/> + <location filename="editexecutablesdialog.ui" line="74"/> + <source>Executables</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="94"/> + <location filename="editexecutablesdialog.ui" line="97"/> + <location filename="editexecutablesdialog.ui" line="100"/> + <source>Add an executable</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="103"/> + <source>Add</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="114"/> + <location filename="editexecutablesdialog.ui" line="117"/> + <location filename="editexecutablesdialog.ui" line="120"/> + <source>Remove the selected executable</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="123"/> + <source>Remove</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="134"/> + <location filename="editexecutablesdialog.ui" line="137"/> + <location filename="editexecutablesdialog.ui" line="140"/> + <location filename="editexecutablesdialog.ui" line="143"/> + <source>Move the executable up in the list</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="154"/> + <location filename="editexecutablesdialog.ui" line="157"/> + <location filename="editexecutablesdialog.ui" line="160"/> + <location filename="editexecutablesdialog.ui" line="163"/> + <source>Move the executable down in the list</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="174"/> + <location filename="editexecutablesdialog.ui" line="177"/> + <location filename="editexecutablesdialog.ui" line="180"/> + <source>Adds the executables provided by the game plugin and moves any existing executables out of the way</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="183"/> + <source>Reset</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="editexecutablesdialog.ui" line="196"/> <source>List of configured executables</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="29"/> + <location filename="editexecutablesdialog.ui" line="199"/> <source>This is a list of your configured executables. Executables in grey are automatically recognised and can not be modified.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="47"/> + <location filename="editexecutablesdialog.ui" line="248"/> <source>Title</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="54"/> - <location filename="editexecutablesdialog.ui" line="57"/> + <location filename="editexecutablesdialog.ui" line="255"/> + <location filename="editexecutablesdialog.ui" line="258"/> <source>Name of the executable. This is only for display purposes.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="68"/> + <location filename="editexecutablesdialog.ui" line="265"/> <source>Binary</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="75"/> - <location filename="editexecutablesdialog.ui" line="78"/> + <location filename="editexecutablesdialog.ui" line="274"/> + <location filename="editexecutablesdialog.ui" line="277"/> <source>Binary to run</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="85"/> + <location filename="editexecutablesdialog.ui" line="284"/> <source>Browse filesystem</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="88"/> + <location filename="editexecutablesdialog.ui" line="287"/> <source>Browse filesystem for the executable to run.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="91"/> - <location filename="editexecutablesdialog.ui" line="112"/> + <location filename="editexecutablesdialog.ui" line="290"/> + <location filename="editexecutablesdialog.ui" line="311"/> <source>...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="102"/> + <location filename="editexecutablesdialog.ui" line="299"/> <source>Start in</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="123"/> + <location filename="editexecutablesdialog.ui" line="320"/> <source>Arguments</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="130"/> - <location filename="editexecutablesdialog.ui" line="133"/> + <location filename="editexecutablesdialog.ui" line="327"/> + <location filename="editexecutablesdialog.ui" line="330"/> <source>Arguments to pass to the application</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="144"/> + <location filename="editexecutablesdialog.ui" line="341"/> <source>Allow the Steam AppID to be used for this executable to be changed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="147"/> + <location filename="editexecutablesdialog.ui" line="344"/> <source>Allow the Steam AppID to be used for this executable to be changed. Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game. Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID. This overwrite is already preconfigured.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="152"/> + <location filename="editexecutablesdialog.ui" line="349"/> <source>Overwrite Steam AppID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="162"/> + <location filename="editexecutablesdialog.ui" line="359"/> <source>Steam AppID to use for this executable that differs from the games AppID.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="165"/> + <location filename="editexecutablesdialog.ui" line="362"/> <source>Steam AppID to use for this executable that differs from the games AppID. Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game (usually 72850). Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID (usually 202480). This overwrite is already preconfigured.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="178"/> + <location filename="editexecutablesdialog.ui" line="375"/> <source>If this is enabled, new files are created in the specified mod instead of the "Overwrite" mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="181"/> + <location filename="editexecutablesdialog.ui" line="378"/> <source>Create Files in Mod instead of Overwrite (*)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="199"/> + <location filename="editexecutablesdialog.ui" line="396"/> <source>If this is enabled, the configured libraries will be automatically loaded when this executable is launched.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="202"/> + <location filename="editexecutablesdialog.ui" line="399"/> <source>Force Load Libraries (*)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="225"/> + <location filename="editexecutablesdialog.ui" line="422"/> <source>Configure Libraries</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="234"/> + <location filename="editexecutablesdialog.ui" line="431"/> <source>Use Application's Icon for shortcuts</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="241"/> - <source>(*) This setting is profile-specific</source> + <location filename="editexecutablesdialog.ui" line="438"/> + <source>(*) Profile Specific</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="250"/> - <location filename="editexecutablesdialog.ui" line="253"/> - <source>Add an executable</source> + <location filename="editexecutablesdialog.cpp" line="437"/> + <source>Reset plugin executables</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="256"/> - <location filename="editexecutablesdialog.cpp" line="272"/> - <source>Add</source> + <location filename="editexecutablesdialog.cpp" line="439"/> + <source>This will restore all the executables provided by the game plugin. If there are existing executables with the same names, they will be automatically renamed and left unchanged.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="267"/> - <location filename="editexecutablesdialog.ui" line="270"/> - <source>Remove the selected executable</source> + <location filename="editexecutablesdialog.cpp" line="458"/> + <source>New Executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="273"/> - <source>Remove</source> + <location filename="editexecutablesdialog.cpp" line="602"/> + <source>Select a binary</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.ui" line="301"/> - <source>Close</source> + <location filename="editexecutablesdialog.cpp" line="603"/> + <source>Executable (%1)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="195"/> - <source>Select a binary</source> + <location filename="editexecutablesdialog.cpp" line="634"/> + <source>Select a directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="196"/> - <source>Executable (%1)</source> + <location filename="editexecutablesdialog.cpp" line="683"/> + <source>Java (32-bit) required</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="221"/> - <source>Java (32-bit) required</source> + <location filename="editexecutablesdialog.cpp" line="684"/> + <source>MO requires 32-bit java to run this application. If you already have it installed, select javaw.exe from that installation as the binary.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>FileTreeTab</name> + <message> + <location filename="modinfodialogfiletree.cpp" line="24"/> + <source>&New Folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="222"/> - <source>MO requires 32-bit java to run this application. If you already have it installed, select javaw.exe from that installation as the binary.</source> + <location filename="modinfodialogfiletree.cpp" line="25"/> + <source>&Open/Execute</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="242"/> - <source>Select a directory</source> + <location filename="modinfodialogfiletree.cpp" line="26"/> + <source>&Preview</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="249"/> - <source>Confirm</source> + <location filename="modinfodialogfiletree.cpp" line="27"/> + <source>Open in &Explorer</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogfiletree.cpp" line="28"/> + <source>&Rename</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogfiletree.cpp" line="29"/> + <source>&Delete</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogfiletree.cpp" line="30"/> + <source>&Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="249"/> - <source>Really remove "%1" from executables?</source> + <location filename="modinfodialogfiletree.cpp" line="31"/> + <source>&Unhide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="276"/> - <source>Modify</source> + <location filename="modinfodialogfiletree.cpp" line="103"/> + <location filename="modinfodialogfiletree.cpp" line="109"/> + <source>New Folder</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogfiletree.cpp" line="115"/> + <source>Failed to create "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="337"/> - <location filename="editexecutablesdialog.cpp" line="357"/> - <source>Save Changes?</source> + <location filename="modinfodialogfiletree.cpp" line="180"/> + <source>Are you sure you want to delete "%1"?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="editexecutablesdialog.cpp" line="338"/> - <location filename="editexecutablesdialog.cpp" line="358"/> - <source>You made changes to the current executable, do you want to save them?</source> + <location filename="modinfodialogfiletree.cpp" line="182"/> + <source>Are you sure you want to delete the selected files?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogfiletree.cpp" line="185"/> + <source>Confirm</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogfiletree.cpp" line="220"/> + <source>Failed to delete %1</source> <translation type="unfinished"></translation> </message> </context> @@ -1106,35 +1274,28 @@ Right now the only case I know of where this needs to be overwritten is for the <name>FindDialog</name> <message> <location filename="../../uibase/src/finddialog.ui" line="14"/> - <location filename="finddialog.ui" line="14"/> <source>Find</source> <translation type="unfinished"></translation> </message> <message> <location filename="../../uibase/src/finddialog.ui" line="24"/> - <location filename="finddialog.ui" line="24"/> <source>Find what:</source> <translation type="unfinished"></translation> </message> <message> <location filename="../../uibase/src/finddialog.ui" line="31"/> <location filename="../../uibase/src/finddialog.ui" line="34"/> - <location filename="finddialog.ui" line="31"/> - <location filename="finddialog.ui" line="34"/> <source>Search term</source> <translation type="unfinished"></translation> </message> <message> <location filename="../../uibase/src/finddialog.ui" line="47"/> <location filename="../../uibase/src/finddialog.ui" line="50"/> - <location filename="finddialog.ui" line="47"/> - <location filename="finddialog.ui" line="50"/> <source>Find next occurence from current file position.</source> <translation type="unfinished"></translation> </message> <message> <location filename="../../uibase/src/finddialog.ui" line="53"/> - <location filename="finddialog.ui" line="53"/> <source>&Find Next</source> <translation type="unfinished"></translation> </message> @@ -1142,9 +1303,6 @@ Right now the only case I know of where this needs to be overwritten is for the <location filename="../../uibase/src/finddialog.ui" line="60"/> <location filename="../../uibase/src/finddialog.ui" line="63"/> <location filename="../../uibase/src/finddialog.ui" line="66"/> - <location filename="finddialog.ui" line="60"/> - <location filename="finddialog.ui" line="63"/> - <location filename="finddialog.ui" line="66"/> <source>Close</source> <translation type="unfinished"></translation> </message> @@ -1229,68 +1387,6 @@ Right now the only case I know of where this needs to be overwritten is for the </message> </context> <context> - <name>InstallDialog</name> - <message> - <location filename="installdialog.ui" line="20"/> - <source>Install Mods</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="32"/> - <source>New Mod</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="46"/> - <source>Name</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="53"/> - <source>Pick a name for the mod</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="56"/> - <source>Pick a name for the mod. This is also used as a directory name, so please don't use characters that are illegal in file names.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="65"/> - <source>Content</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="75"/> - <source>Content of the archive. You can change the directory structure by using drag&drop. Hint: Also try right clicking...</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="78"/> - <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This displays the content of the archive. &lt;data&gt; represents the base directory which will map to the game's data directory. You can change the base directory via the right-click context menu and you can move around files via drag&amp;drop</span></p></body></html></source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="121"/> - <source>Placeholder</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="141"/> - <source>OK</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="installdialog.ui" line="148"/> - <source>Cancel</source> - <translation type="unfinished"></translation> - </message> -</context> -<context> <name>InstallationManager</name> <message> <location filename="installationmanager.cpp" line="120"/> @@ -1438,12 +1534,12 @@ This is likely due to a corrupted or incompatible download or unrecognized archi <name>MOApplication</name> <message> <location filename="moapplication.cpp" line="119"/> - <source>an error occured: %1</source> + <source>an error occurred: %1</source> <translation type="unfinished"></translation> </message> <message> <location filename="moapplication.cpp" line="124"/> - <source>an error occured</source> + <source>an error occurred</source> <translation type="unfinished"></translation> </message> </context> @@ -1478,7 +1574,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi <context> <name>MOBase::TutorialControl</name> <message> - <location filename="../../uibase/src/tutorialcontrol.cpp" line="145"/> + <location filename="../../uibase/src/tutorialcontrol.cpp" line="146"/> <source>Tutorial failed to start, please check "mo_interface.log" for details.</source> <translation type="unfinished"></translation> </message> @@ -1494,150 +1590,135 @@ This is likely due to a corrupted or incompatible download or unrecognized archi <context> <name>MainWindow</name> <message> - <location filename="mainwindow.ui" line="46"/> - <location filename="mainwindow.ui" line="565"/> + <location filename="mainwindow.ui" line="61"/> + <location filename="mainwindow.ui" line="536"/> <source>Categories</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="118"/> + <location filename="mainwindow.ui" line="133"/> <source>Clear</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="137"/> + <location filename="mainwindow.ui" line="152"/> <source>If checked, only mods that match all selected categories are displayed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="140"/> + <location filename="mainwindow.ui" line="155"/> <source>And</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="150"/> + <location filename="mainwindow.ui" line="165"/> <source>If checked, all mods that match at least one of the selected categories are displayed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="153"/> + <location filename="mainwindow.ui" line="168"/> <source>Or</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="192"/> + <location filename="mainwindow.ui" line="207"/> <source>Profile</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="202"/> + <location filename="mainwindow.ui" line="217"/> <source>Pick a module collection</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="205"/> + <location filename="mainwindow.ui" line="220"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different play throughs.</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept seperate for different profiles.</span></p></body></html></source> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different playthroughs.</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept separate for different profiles.</span></p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="236"/> + <location filename="mainwindow.ui" line="251"/> <source>Open list options...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="239"/> + <location filename="mainwindow.ui" line="254"/> <source>Refresh list. This is usually not necessary unless you modified data outside the program.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="259"/> + <location filename="mainwindow.ui" line="274"/> <source>Show Open Folders menu...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="273"/> - <location filename="mainwindow.ui" line="831"/> + <location filename="mainwindow.ui" line="288"/> + <location filename="mainwindow.ui" line="802"/> <source>Restore Backup...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="287"/> - <location filename="mainwindow.ui" line="851"/> - <location filename="mainwindow.cpp" line="4663"/> + <location filename="mainwindow.ui" line="302"/> + <location filename="mainwindow.ui" line="822"/> + <location filename="mainwindow.cpp" line="4860"/> <source>Create Backup</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="301"/> - <location filename="mainwindow.ui" line="865"/> + <location filename="mainwindow.ui" line="316"/> + <location filename="mainwindow.ui" line="836"/> <source>Active:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="314"/> + <location filename="mainwindow.ui" line="329"/> <source>This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="341"/> + <location filename="mainwindow.ui" line="356"/> <source>List of available mods.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="344"/> + <location filename="mainwindow.ui" line="359"/> <source>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="432"/> - <location filename="mainwindow.ui" line="584"/> - <location filename="mainwindow.ui" line="966"/> - <location filename="mainwindow.ui" line="1302"/> + <location filename="mainwindow.ui" line="447"/> + <location filename="mainwindow.ui" line="555"/> + <location filename="mainwindow.ui" line="937"/> + <location filename="mainwindow.ui" line="1273"/> <source>Filter</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="462"/> - <source>Nexus API Queued and Remaining Requests</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.ui" line="465"/> - <source><html><head/><body><p>This tracks the number of queued Nexus API requests on the left (<span style=" font-weight:600;">Q</span>) and the remaining daily (<span style=" font-weight:600;">D</span>) and hourly (<span style=" font-weight:600;">H</span>) requests on the right. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.</p></body></html></source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.ui" line="480"/> - <source>API: Q: 0 | D: 0 | H: 0</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.ui" line="520"/> + <location filename="mainwindow.ui" line="504"/> <source>Clear all Filters</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="560"/> + <location filename="mainwindow.ui" line="531"/> <source>No groups</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="570"/> + <location filename="mainwindow.ui" line="541"/> <source>Nexus IDs</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="619"/> + <location filename="mainwindow.ui" line="590"/> <source>Pick a program to run.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="622"/> + <location filename="mainwindow.ui" line="593"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1647,12 +1728,12 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="670"/> + <location filename="mainwindow.ui" line="641"/> <source>Run program</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="673"/> + <location filename="mainwindow.ui" line="644"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1661,17 +1742,17 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="683"/> + <location filename="mainwindow.ui" line="654"/> <source>Run</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="724"/> + <location filename="mainwindow.ui" line="695"/> <source>Create a shortcut in your start menu or on the desktop to the specified program</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="727"/> + <location filename="mainwindow.ui" line="698"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1680,32 +1761,32 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="734"/> + <location filename="mainwindow.ui" line="705"/> <source>Shortcut</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="784"/> + <location filename="mainwindow.ui" line="755"/> <source>Plugins</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="807"/> + <location filename="mainwindow.ui" line="778"/> <source>Sort</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="878"/> + <location filename="mainwindow.ui" line="849"/> <source>This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="905"/> + <location filename="mainwindow.ui" line="876"/> <source>List of available esp/esm files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="908"/> + <location filename="mainwindow.ui" line="879"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1714,27 +1795,27 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="979"/> + <location filename="mainwindow.ui" line="950"/> <source>Archives</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="999"/> + <location filename="mainwindow.ui" line="970"/> <source><html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1002"/> + <location filename="mainwindow.ui" line="973"/> <source><html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1017"/> + <location filename="mainwindow.ui" line="988"/> <source>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1020"/> + <location filename="mainwindow.ui" line="991"/> <source>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! @@ -1742,72 +1823,72 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1049"/> + <location filename="mainwindow.ui" line="1020"/> <source>Data</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1067"/> + <location filename="mainwindow.ui" line="1038"/> <source>refresh data-directory overview</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1070"/> + <location filename="mainwindow.ui" line="1041"/> <source>Refresh the overview. This may take a moment.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1073"/> - <location filename="mainwindow.ui" line="1216"/> - <location filename="mainwindow.cpp" line="4536"/> - <location filename="mainwindow.cpp" line="5478"/> + <location filename="mainwindow.ui" line="1044"/> + <location filename="mainwindow.ui" line="1187"/> + <location filename="mainwindow.cpp" line="4733"/> + <location filename="mainwindow.cpp" line="5551"/> <source>Refresh</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1089"/> + <location filename="mainwindow.ui" line="1060"/> <source>This is an overview of your data directory as visible to the game (and tools). </source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1102"/> + <location filename="mainwindow.ui" line="1073"/> <source>File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1107"/> + <location filename="mainwindow.ui" line="1078"/> <source>Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1119"/> - <location filename="mainwindow.ui" line="1122"/> + <location filename="mainwindow.ui" line="1090"/> + <location filename="mainwindow.ui" line="1093"/> <source>Filters the above list so that only conflicts are displayed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1125"/> + <location filename="mainwindow.ui" line="1096"/> <source>Show only conflicts</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1132"/> - <location filename="mainwindow.ui" line="1138"/> + <location filename="mainwindow.ui" line="1103"/> + <location filename="mainwindow.ui" line="1109"/> <source>Filters the above list so that files from archives are not shown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1141"/> + <location filename="mainwindow.ui" line="1112"/> <source>Show files from Archives</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1151"/> + <location filename="mainwindow.ui" line="1122"/> <source>Saves</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1175"/> + <location filename="mainwindow.ui" line="1146"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1818,1097 +1899,1181 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1195"/> + <location filename="mainwindow.ui" line="1166"/> <source>Downloads</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1213"/> + <location filename="mainwindow.ui" line="1184"/> <source>Refresh downloads view</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1244"/> + <location filename="mainwindow.ui" line="1215"/> <source>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1282"/> + <location filename="mainwindow.ui" line="1253"/> <source>Show Hidden</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1347"/> - <source>Tool Bar</source> + <location filename="mainwindow.ui" line="1315"/> + <source>Main ToolBar</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1390"/> - <source>Install Mod</source> + <location filename="mainwindow.ui" line="1357"/> + <source>&File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1393"/> + <location filename="mainwindow.ui" line="1367"/> + <location filename="mainwindow.ui" line="1492"/> + <source>&Tools</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1378"/> + <location filename="mainwindow.ui" line="1588"/> + <location filename="mainwindow.ui" line="1591"/> + <source>&Help</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1387"/> + <source>&View</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1391"/> + <source>&Toolbars</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1411"/> + <source>&Run</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1426"/> + <source>Install &Mod...</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1429"/> <source>Install &Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1396"/> + <location filename="mainwindow.ui" line="1432"/> + <location filename="mainwindow.ui" line="1435"/> <source>Install a new mod from an archive</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1399"/> + <location filename="mainwindow.ui" line="1438"/> <source>Ctrl+M</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1408"/> - <source>Profiles</source> + <location filename="mainwindow.ui" line="1447"/> + <source>&Profiles...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1411"/> + <location filename="mainwindow.ui" line="1450"/> <source>&Profiles</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1414"/> - <source>Configure Profiles</source> + <location filename="mainwindow.ui" line="1453"/> + <location filename="mainwindow.ui" line="1456"/> + <source>Configure profiles</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1417"/> + <location filename="mainwindow.ui" line="1459"/> <source>Ctrl+P</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1426"/> - <source>Executables</source> + <location filename="mainwindow.ui" line="1468"/> + <source>&Executables...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1429"/> + <location filename="mainwindow.ui" line="1471"/> <source>&Executables</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1432"/> + <location filename="mainwindow.ui" line="1474"/> + <location filename="mainwindow.ui" line="1477"/> <source>Configure the executables that can be started through Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1435"/> + <location filename="mainwindow.ui" line="1480"/> <source>Ctrl+E</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1444"/> - <location filename="mainwindow.ui" line="1450"/> - <source>Tools</source> + <location filename="mainwindow.ui" line="1489"/> + <source>&Tool Plugins</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1447"/> - <source>&Tools</source> + <location filename="mainwindow.ui" line="1495"/> + <source>Tools</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1453"/> + <location filename="mainwindow.ui" line="1498"/> <source>Ctrl+I</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1462"/> - <source>Settings</source> + <location filename="mainwindow.ui" line="1507"/> + <source>&Settings...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1465"/> + <location filename="mainwindow.ui" line="1510"/> <source>&Settings</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1468"/> + <location filename="mainwindow.ui" line="1513"/> + <location filename="mainwindow.ui" line="1516"/> <source>Configure settings and workarounds</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1471"/> + <location filename="mainwindow.ui" line="1519"/> <source>Ctrl+S</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1480"/> - <source>Nexus</source> + <location filename="mainwindow.ui" line="1528"/> + <location filename="mainwindow.ui" line="1531"/> + <source>Visit &Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1483"/> - <source>Search nexus network for more mods</source> + <location filename="mainwindow.ui" line="1534"/> + <location filename="mainwindow.ui" line="1537"/> + <source>Visit the Nexus website in your browser for more mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1486"/> + <location filename="mainwindow.ui" line="1540"/> <source>Ctrl+N</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1498"/> - <location filename="mainwindow.cpp" line="5427"/> - <source>Update</source> + <location filename="mainwindow.ui" line="1552"/> + <location filename="mainwindow.ui" line="1555"/> + <source>&Update Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1501"/> + <location filename="mainwindow.ui" line="1558"/> + <location filename="mainwindow.ui" line="1561"/> <source>Mod Organizer is up-to-date</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1513"/> - <location filename="mainwindow.cpp" line="690"/> - <source>No Notifications</source> + <location filename="mainwindow.ui" line="1570"/> + <source>&Notifications...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1516"/> - <source>This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them.</source> + <location filename="mainwindow.ui" line="1573"/> + <location filename="mainwindow.ui" line="1576"/> + <source>Open the notifications dialog</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1525"/> - <location filename="mainwindow.ui" line="1528"/> - <source>Help</source> + <location filename="mainwindow.ui" line="1579"/> + <source>This button will be highlighted on the toolbar if MO discovered potential problems in your setup and provide tips on how to fix them.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1531"/> + <location filename="mainwindow.ui" line="1594"/> + <location filename="mainwindow.ui" line="1597"/> + <source>Show help options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1600"/> <source>Ctrl+H</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1540"/> - <source>Endorse MO</source> + <location filename="mainwindow.ui" line="1609"/> + <location filename="mainwindow.ui" line="1612"/> + <source>&Endorse ModOrganizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1543"/> - <location filename="mainwindow.cpp" line="5501"/> + <location filename="mainwindow.ui" line="1615"/> + <location filename="mainwindow.ui" line="1618"/> + <location filename="mainwindow.cpp" line="5579"/> <source>Endorse Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1548"/> - <source>Copy Log to Clipboard</source> + <location filename="mainwindow.ui" line="1623"/> + <location filename="mainwindow.ui" line="1626"/> + <source>Copy &Log</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1629"/> + <location filename="mainwindow.ui" line="1632"/> + <source>Copy log to clipboard</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1557"/> - <source>Change Game</source> + <location filename="mainwindow.ui" line="1641"/> + <source>&Change Game...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.ui" line="1560"/> + <location filename="mainwindow.ui" line="1644"/> + <source>&Change Game</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1647"/> + <location filename="mainwindow.ui" line="1650"/> <source>Open the Instance selection dialog to manage a different Game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="337"/> - <source>Toolbar</source> + <location filename="mainwindow.ui" line="1655"/> + <location filename="mainwindow.ui" line="1658"/> + <source>E&xit</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1661"/> + <location filename="mainwindow.ui" line="1664"/> + <source>Exits Mod Organizer</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1672"/> + <source>M&ain Toolbar</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="338"/> + <location filename="mainwindow.ui" line="1680"/> + <source>&Small Icons</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1688"/> + <source>Lar&ge Icons</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1696"/> + <source>&Icons Only</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1704"/> + <source>&Text Only</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1712"/> + <source>I&cons and Text</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1720"/> + <source>M&edium Icons</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1728"/> + <source>&Menu</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.ui" line="1736"/> + <source>Status &bar</source> + <oldsource>St&atus bar</oldsource> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.cpp" line="341"/> + <source>Toolbar and Menu</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.cpp" line="342"/> <source>Desktop</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="339"/> + <location filename="mainwindow.cpp" line="343"/> <source>Start Menu</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="364"/> + <location filename="mainwindow.cpp" line="368"/> <source>There is no supported sort mechanism for this game. You will probably have to use a third-party tool.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="510"/> + <location filename="mainwindow.cpp" line="578"/> <source>Crash on exit</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="511"/> + <location filename="mainwindow.cpp" line="579"/> <source>MO crashed while exiting. Some settings may not be saved. Error: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="678"/> - <source>Notifications</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="679"/> + <location filename="mainwindow.cpp" line="929"/> <source>There are notifications to read</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="691"/> + <location filename="mainwindow.cpp" line="948"/> <source>There are no notifications</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="758"/> - <location filename="mainwindow.cpp" line="4673"/> - <location filename="mainwindow.cpp" line="4677"/> + <location filename="mainwindow.cpp" line="1035"/> + <location filename="mainwindow.cpp" line="4870"/> + <location filename="mainwindow.cpp" line="4874"/> <source>Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="762"/> + <location filename="mainwindow.cpp" line="1039"/> <source>Won't Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="777"/> + <location filename="mainwindow.cpp" line="1056"/> <source>Help on UI</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="781"/> + <location filename="mainwindow.cpp" line="1060"/> <source>Documentation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="785"/> + <location filename="mainwindow.cpp" line="1064"/> <source>Chat on Discord</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="789"/> + <location filename="mainwindow.cpp" line="1068"/> <source>Report Issue</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="793"/> + <location filename="mainwindow.cpp" line="1072"/> <source>Tutorials</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="832"/> + <location filename="mainwindow.cpp" line="1111"/> <source>About</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="833"/> + <location filename="mainwindow.cpp" line="1112"/> <source>About Qt</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="892"/> + <location filename="mainwindow.cpp" line="1171"/> <source>Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="893"/> + <location filename="mainwindow.cpp" line="1172"/> <source>Please enter a name for the new profile</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="901"/> + <location filename="mainwindow.cpp" line="1180"/> <source>failed to create profile: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="945"/> + <location filename="mainwindow.cpp" line="1234"/> <source>Show tutorial?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="946"/> + <location filename="mainwindow.cpp" line="1235"/> <source>You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="982"/> + <location filename="mainwindow.cpp" line="1278"/> <source>Downloads in progress</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="983"/> + <location filename="mainwindow.cpp" line="1279"/> <source>There are still downloads in progress, do you really want to quit?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1100"/> + <location filename="mainwindow.cpp" line="1403"/> <source>Plugin "%1" failed: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1102"/> + <location filename="mainwindow.cpp" line="1405"/> <source>Plugin "%1" failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1186"/> + <location filename="mainwindow.cpp" line="1485"/> <source>Browse Mod Page</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1393"/> + <location filename="mainwindow.cpp" line="1713"/> <source>Also in: <br></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1404"/> + <location filename="mainwindow.cpp" line="1724"/> <source>No conflict</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1489"/> + <location filename="mainwindow.cpp" line="1809"/> <source><Edit...></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1767"/> + <location filename="mainwindow.cpp" line="2087"/> <source>This bsa is enabled in the ini file so it may be required!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1829"/> + <location filename="mainwindow.cpp" line="2149"/> <source>Activating Network Proxy</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1905"/> + <location filename="mainwindow.cpp" line="2252"/> <source>Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2007"/> + <location filename="mainwindow.cpp" line="2365"/> <source>Choose Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2008"/> + <location filename="mainwindow.cpp" line="2366"/> <source>Mod Archive</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2195"/> + <location filename="mainwindow.cpp" line="2473"/> <source>Start Tutorial?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2196"/> + <location filename="mainwindow.cpp" line="2474"/> <source>You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2374"/> + <location filename="mainwindow.cpp" line="2648"/> <source>failed to spawn notepad.exe: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2414"/> + <location filename="mainwindow.cpp" line="2688"/> <source>failed to change origin name: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2438"/> + <location filename="mainwindow.cpp" line="2712"/> <source>failed to move "%1" from mod "%2" to "%3": %4</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2462"/> + <location filename="mainwindow.cpp" line="2736"/> <source><Contains %1></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2497"/> + <location filename="mainwindow.cpp" line="2771"/> <source><Checked></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2498"/> + <location filename="mainwindow.cpp" line="2772"/> <source><Unchecked></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2499"/> + <location filename="mainwindow.cpp" line="2773"/> <source><Update></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2500"/> + <location filename="mainwindow.cpp" line="2774"/> <source><Mod Backup></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2501"/> + <location filename="mainwindow.cpp" line="2775"/> <source><Managed by MO></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2502"/> + <location filename="mainwindow.cpp" line="2776"/> <source><Managed outside MO></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2503"/> + <location filename="mainwindow.cpp" line="2777"/> <source><No category></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2504"/> + <location filename="mainwindow.cpp" line="2778"/> <source><Conflicted></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2505"/> + <location filename="mainwindow.cpp" line="2779"/> <source><Not Endorsed></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2551"/> + <location filename="mainwindow.cpp" line="2825"/> <source>failed to rename mod: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2564"/> + <location filename="mainwindow.cpp" line="2838"/> <source>Overwrite?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2565"/> + <location filename="mainwindow.cpp" line="2839"/> <source>This will replace the existing mod "%1". Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2568"/> + <location filename="mainwindow.cpp" line="2842"/> <source>failed to remove mod "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2572"/> - <location filename="mainwindow.cpp" line="5253"/> - <location filename="mainwindow.cpp" line="5277"/> + <location filename="mainwindow.cpp" line="2846"/> + <location filename="mainwindow.cpp" line="5379"/> + <location filename="mainwindow.cpp" line="5403"/> <source>failed to rename "%1" to "%2"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2644"/> - <location filename="mainwindow.cpp" line="4239"/> - <location filename="mainwindow.cpp" line="4247"/> - <location filename="mainwindow.cpp" line="4802"/> + <location filename="mainwindow.cpp" line="2934"/> + <location filename="mainwindow.cpp" line="4437"/> + <location filename="mainwindow.cpp" line="4445"/> + <location filename="mainwindow.cpp" line="5004"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2645"/> + <location filename="mainwindow.cpp" line="2935"/> <source>Remove the following mods?<br><ul>%1</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2660"/> + <location filename="mainwindow.cpp" line="2950"/> <source>failed to remove mod: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2692"/> - <location filename="mainwindow.cpp" line="2695"/> - <location filename="mainwindow.cpp" line="2705"/> + <location filename="mainwindow.cpp" line="2982"/> + <location filename="mainwindow.cpp" line="2985"/> + <location filename="mainwindow.cpp" line="2995"/> <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2692"/> + <location filename="mainwindow.cpp" line="2982"/> <source>Installation file no longer exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2696"/> + <location filename="mainwindow.cpp" line="2986"/> <source>Mods installed with old versions of MO can't be reinstalled in this way.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2706"/> + <location filename="mainwindow.cpp" line="2996"/> <source>Failed to create backup.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2723"/> - <source>You need to be logged in with Nexus to resume a download</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="2739"/> - <location filename="mainwindow.cpp" line="2765"/> - <location filename="mainwindow.cpp" line="2799"/> - <location filename="mainwindow.cpp" line="2824"/> - <source>You need to be logged in with Nexus to endorse</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="2750"/> - <location filename="mainwindow.cpp" line="2758"/> + <location filename="mainwindow.cpp" line="3024"/> <source>Endorsing multiple mods will take a while. Please wait...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2810"/> - <location filename="mainwindow.cpp" line="2817"/> + <location filename="mainwindow.cpp" line="3060"/> <source>Unendorsing multiple mods will take a while. Please wait...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2845"/> - <location filename="mainwindow.cpp" line="2868"/> - <location filename="mainwindow.cpp" line="2893"/> - <source>You need to be logged in with Nexus to track</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="2952"/> + <location filename="mainwindow.cpp" line="3149"/> <source>Failed to display overwrite dialog: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3139"/> + <location filename="mainwindow.cpp" line="3349"/> <source>Opening Nexus Links</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3140"/> + <location filename="mainwindow.cpp" line="3350"/> <source>You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3169"/> - <source>Nexus ID for this Mod is unknown</source> + <location filename="mainwindow.cpp" line="3377"/> + <source>Nexus ID for this mod is unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3181"/> + <location filename="mainwindow.cpp" line="3388"/> <source>Opening Web Pages</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3182"/> + <location filename="mainwindow.cpp" line="3389"/> <source>You are trying to open %1 Web Pages. Are you sure you want to do this?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3211"/> - <source>Web page for this mod is unknown</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="3398"/> + <location filename="mainwindow.cpp" line="3595"/> <source><table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3453"/> + <location filename="mainwindow.cpp" line="3650"/> <source><table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3485"/> - <location filename="mainwindow.cpp" line="3623"/> - <location filename="mainwindow.cpp" line="4598"/> + <location filename="mainwindow.cpp" line="3682"/> + <location filename="mainwindow.cpp" line="3820"/> + <location filename="mainwindow.cpp" line="4795"/> <source>Create Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3486"/> + <location filename="mainwindow.cpp" line="3683"/> <source>This will create an empty mod. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3495"/> - <location filename="mainwindow.cpp" line="3633"/> + <location filename="mainwindow.cpp" line="3692"/> + <location filename="mainwindow.cpp" line="3830"/> <source>A mod with this name already exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3523"/> + <location filename="mainwindow.cpp" line="3720"/> <source>Create Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3524"/> + <location filename="mainwindow.cpp" line="3721"/> <source>This will create a new separator. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3531"/> + <location filename="mainwindow.cpp" line="3728"/> <source>A separator with this name already exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3624"/> + <location filename="mainwindow.cpp" line="3821"/> <source>This will move all files from overwrite into a new, regular mod. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3705"/> + <location filename="mainwindow.cpp" line="3902"/> <source>Move successful.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3726"/> - <location filename="mainwindow.cpp" line="6085"/> + <location filename="mainwindow.cpp" line="3923"/> + <location filename="mainwindow.cpp" line="6141"/> <source>Are you sure?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3727"/> + <location filename="mainwindow.cpp" line="3924"/> <source>About to recursively delete: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4121"/> + <location filename="mainwindow.cpp" line="4319"/> <source>Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4122"/> + <location filename="mainwindow.cpp" line="4320"/> <source>The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4142"/> - <location filename="mainwindow.cpp" line="5395"/> + <location filename="mainwindow.cpp" line="4340"/> <source>Sorry</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4143"/> + <location filename="mainwindow.cpp" line="4341"/> <source>I don't know a versioning scheme where %1 is newer than %2.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4239"/> + <location filename="mainwindow.cpp" line="4437"/> <source>Really enable all visible mods?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4247"/> + <location filename="mainwindow.cpp" line="4445"/> <source>Really disable all visible mods?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4327"/> + <location filename="mainwindow.cpp" line="4522"/> <source>Export to csv</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4330"/> + <location filename="mainwindow.cpp" line="4525"/> <source>CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4333"/> + <location filename="mainwindow.cpp" line="4528"/> <source>Select what mods you want export:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4334"/> + <location filename="mainwindow.cpp" line="4529"/> <source>All installed mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4335"/> + <location filename="mainwindow.cpp" line="4530"/> <source>Only active (checked) mods from your current profile</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4336"/> + <location filename="mainwindow.cpp" line="4531"/> <source>All currently visible mods in the mod list</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4357"/> + <location filename="mainwindow.cpp" line="4552"/> <source>Choose what Columns to export:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4360"/> + <location filename="mainwindow.cpp" line="4555"/> <source>Mod_Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4362"/> + <location filename="mainwindow.cpp" line="4557"/> <source>Mod_Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4364"/> + <location filename="mainwindow.cpp" line="4559"/> <source>Notes_column</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4365"/> + <location filename="mainwindow.cpp" line="4560"/> <source>Mod_Status</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4366"/> + <location filename="mainwindow.cpp" line="4562"/> <source>Primary_Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4367"/> + <location filename="mainwindow.cpp" line="4563"/> <source>Nexus_ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4368"/> + <location filename="mainwindow.cpp" line="4564"/> <source>Mod_Nexus_URL</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4369"/> + <location filename="mainwindow.cpp" line="4565"/> <source>Mod_Version</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4370"/> + <location filename="mainwindow.cpp" line="4566"/> <source>Install_Date</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4371"/> + <location filename="mainwindow.cpp" line="4567"/> <source>Download_File_Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4478"/> + <location filename="mainwindow.cpp" line="4675"/> <source>export failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4497"/> + <location filename="mainwindow.cpp" line="4694"/> <source>Open Game folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4499"/> + <location filename="mainwindow.cpp" line="4696"/> <source>Open MyGames folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4501"/> + <location filename="mainwindow.cpp" line="4698"/> <source>Open INIs folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4505"/> + <location filename="mainwindow.cpp" line="4702"/> <source>Open Instance folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4507"/> + <location filename="mainwindow.cpp" line="4704"/> <source>Open Mods folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4509"/> + <location filename="mainwindow.cpp" line="4706"/> <source>Open Profile folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4511"/> + <location filename="mainwindow.cpp" line="4708"/> <source>Open Downloads folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4515"/> + <location filename="mainwindow.cpp" line="4712"/> <source>Open MO2 Install folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4517"/> + <location filename="mainwindow.cpp" line="4714"/> <source>Open MO2 Plugins folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4519"/> + <location filename="mainwindow.cpp" line="4716"/> <source>Open MO2 Logs folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4527"/> + <location filename="mainwindow.cpp" line="4724"/> <source>Install Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4528"/> + <location filename="mainwindow.cpp" line="4725"/> <source>Create empty mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4529"/> + <location filename="mainwindow.cpp" line="4726"/> <source>Create Separator</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4533"/> + <location filename="mainwindow.cpp" line="4730"/> <source>Enable all visible</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4534"/> + <location filename="mainwindow.cpp" line="4731"/> <source>Disable all visible</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4535"/> + <location filename="mainwindow.cpp" line="4732"/> <source>Check for updates</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4537"/> + <location filename="mainwindow.cpp" line="4734"/> <source>Export to csv...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4546"/> - <location filename="mainwindow.cpp" line="4562"/> + <location filename="mainwindow.cpp" line="4743"/> + <location filename="mainwindow.cpp" line="4759"/> <source>Send to</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4547"/> - <location filename="mainwindow.cpp" line="4563"/> + <location filename="mainwindow.cpp" line="4744"/> + <location filename="mainwindow.cpp" line="4760"/> <source>Top</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4548"/> - <location filename="mainwindow.cpp" line="4564"/> + <location filename="mainwindow.cpp" line="4745"/> + <location filename="mainwindow.cpp" line="4761"/> <source>Bottom</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4549"/> - <location filename="mainwindow.cpp" line="4565"/> + <location filename="mainwindow.cpp" line="4746"/> + <location filename="mainwindow.cpp" line="4762"/> <source>Priority...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4550"/> + <location filename="mainwindow.cpp" line="4747"/> <source>Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4589"/> + <location filename="mainwindow.cpp" line="4786"/> <source>All Mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4597"/> + <location filename="mainwindow.cpp" line="4794"/> <source>Sync to Mods...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4599"/> + <location filename="mainwindow.cpp" line="4796"/> <source>Move content to Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4600"/> + <location filename="mainwindow.cpp" line="4797"/> <source>Clear Overwrite...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4602"/> - <location filename="mainwindow.cpp" line="4720"/> + <location filename="mainwindow.cpp" line="4799"/> + <location filename="mainwindow.cpp" line="4922"/> <source>Open in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4604"/> + <location filename="mainwindow.cpp" line="4801"/> <source>Restore Backup</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4605"/> + <location filename="mainwindow.cpp" line="4802"/> <source>Remove Backup...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4608"/> - <location filename="mainwindow.cpp" line="4627"/> + <location filename="mainwindow.cpp" line="4805"/> + <location filename="mainwindow.cpp" line="4824"/> <source>Change Categories</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4612"/> - <location filename="mainwindow.cpp" line="4632"/> + <location filename="mainwindow.cpp" line="4809"/> + <location filename="mainwindow.cpp" line="4829"/> <source>Primary Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4616"/> + <location filename="mainwindow.cpp" line="4813"/> <source>Rename Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4617"/> + <location filename="mainwindow.cpp" line="4814"/> <source>Remove Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4620"/> + <location filename="mainwindow.cpp" line="4817"/> <source>Select Color...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4622"/> + <location filename="mainwindow.cpp" line="4819"/> <source>Reset Color</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4639"/> + <location filename="mainwindow.cpp" line="4836"/> <source>Change versioning scheme</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4643"/> + <location filename="mainwindow.cpp" line="4840"/> <source>Force-check updates</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4645"/> + <location filename="mainwindow.cpp" line="4842"/> <source>Un-ignore update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4648"/> + <location filename="mainwindow.cpp" line="4845"/> <source>Ignore update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4653"/> - <location filename="mainwindow.cpp" line="6198"/> + <location filename="mainwindow.cpp" line="4850"/> + <location filename="mainwindow.cpp" line="6265"/> <source>Enable selected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4654"/> - <location filename="mainwindow.cpp" line="6199"/> + <location filename="mainwindow.cpp" line="4851"/> + <location filename="mainwindow.cpp" line="6266"/> <source>Disable selected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4660"/> + <location filename="mainwindow.cpp" line="4857"/> <source>Rename Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4661"/> + <location filename="mainwindow.cpp" line="4858"/> <source>Reinstall Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4662"/> + <location filename="mainwindow.cpp" line="4859"/> <source>Remove Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4670"/> + <location filename="mainwindow.cpp" line="4867"/> <source>Un-Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4674"/> + <location filename="mainwindow.cpp" line="4871"/> <source>Won't endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4680"/> + <location filename="mainwindow.cpp" line="4877"/> <source>Endorsement state unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4690"/> + <location filename="mainwindow.cpp" line="4887"/> <source>Start tracking</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4693"/> + <location filename="mainwindow.cpp" line="4890"/> <source>Stop tracking</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4696"/> + <location filename="mainwindow.cpp" line="4893"/> <source>Tracked state unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4707"/> + <location filename="mainwindow.cpp" line="4904"/> <source>Ignore missing data</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4711"/> + <location filename="mainwindow.cpp" line="4908"/> <source>Mark as converted/working</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4715"/> + <location filename="mainwindow.cpp" line="4912"/> <source>Visit on Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4717"/> - <source>Visit web page</source> + <location filename="mainwindow.cpp" line="4918"/> + <source>Visit on %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4724"/> + <location filename="mainwindow.cpp" line="4926"/> <source>Information...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4731"/> - <location filename="mainwindow.cpp" line="6251"/> + <location filename="mainwindow.cpp" line="4933"/> + <location filename="mainwindow.cpp" line="6318"/> <source>Exception: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4733"/> - <location filename="mainwindow.cpp" line="6253"/> + <location filename="mainwindow.cpp" line="4935"/> + <location filename="mainwindow.cpp" line="6320"/> <source>Unknown exception</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4762"/> + <location filename="mainwindow.cpp" line="4964"/> <source><All></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4764"/> + <location filename="mainwindow.cpp" line="4966"/> <source><Multiple></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4799"/> + <location filename="mainwindow.cpp" line="5001"/> <source>%1 more</source> <translation type="unfinished"></translation> </message> <message numerus="yes"> - <location filename="mainwindow.cpp" line="4803"/> + <location filename="mainwindow.cpp" line="5005"/> <source>Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin.</source> <translation type="unfinished"> <numerusform></numerusform> @@ -2916,12 +3081,12 @@ You can also use online editors and converters instead.</source> </translation> </message> <message> - <location filename="mainwindow.cpp" line="4848"/> + <location filename="mainwindow.cpp" line="5050"/> <source>Enable Mods...</source> <translation type="unfinished"></translation> </message> <message numerus="yes"> - <location filename="mainwindow.cpp" line="4863"/> + <location filename="mainwindow.cpp" line="5065"/> <source>Delete %n save(s)</source> <translation type="unfinished"> <numerusform></numerusform> @@ -2929,22 +3094,12 @@ You can also use online editors and converters instead.</source> </translation> </message> <message> - <location filename="mainwindow.cpp" line="4905"/> - <source>failed to remove %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="4927"/> - <source>failed to create %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="4957"/> + <location filename="mainwindow.cpp" line="5124"/> <source>Restarting MO</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4958"/> + <location filename="mainwindow.cpp" line="5125"/> <source>Changing the managed game directory requires restarting MO. Any pending downloads will be paused. @@ -2952,373 +3107,348 @@ Click OK to restart MO now.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4978"/> + <location filename="mainwindow.cpp" line="5145"/> <source>Can't change download directory while downloads are in progress!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5124"/> + <location filename="mainwindow.cpp" line="5292"/> <source>failed to write to file %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5130"/> + <location filename="mainwindow.cpp" line="5298"/> <source>%1 written</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5172"/> - <source>Select binary</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="5172"/> - <source>Binary</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="5198"/> + <location filename="mainwindow.cpp" line="5322"/> <source>Enter Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5199"/> + <location filename="mainwindow.cpp" line="5323"/> <source>Please enter a name for the executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5213"/> + <location filename="mainwindow.cpp" line="5342"/> <source>Not an executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5213"/> + <location filename="mainwindow.cpp" line="5342"/> <source>This is not a recognized executable.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5238"/> - <location filename="mainwindow.cpp" line="5263"/> + <location filename="mainwindow.cpp" line="5364"/> + <location filename="mainwindow.cpp" line="5389"/> <source>Replace file?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5238"/> + <location filename="mainwindow.cpp" line="5364"/> <source>There already is a hidden version of this file. Replace it?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5241"/> - <location filename="mainwindow.cpp" line="5266"/> + <location filename="mainwindow.cpp" line="5367"/> + <location filename="mainwindow.cpp" line="5392"/> <source>File operation failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5241"/> - <location filename="mainwindow.cpp" line="5266"/> + <location filename="mainwindow.cpp" line="5367"/> + <location filename="mainwindow.cpp" line="5392"/> <source>Failed to remove "%1". Maybe you lack the required file permissions?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5263"/> + <location filename="mainwindow.cpp" line="5389"/> <source>There already is a visible version of this file. Replace it?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5307"/> - <location filename="mainwindow.cpp" line="6865"/> + <location filename="mainwindow.cpp" line="5433"/> + <location filename="mainwindow.cpp" line="6928"/> <source>Set Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5307"/> + <location filename="mainwindow.cpp" line="5433"/> <source>Set the priority of the selected plugins</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5362"/> - <source>file not found: %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="5375"/> - <source>failed to generate preview for %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="5395"/> - <source>Sorry, can't preview anything. This function currently does not support extracting from bsas.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="5429"/> + <location filename="mainwindow.cpp" line="5497"/> <source>Update available</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5458"/> + <location filename="mainwindow.cpp" line="5524"/> <source>Open/Execute</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5459"/> + <location filename="mainwindow.cpp" line="5525"/> <source>Add as Executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5463"/> + <location filename="mainwindow.cpp" line="5529"/> <source>Preview</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5469"/> + <location filename="mainwindow.cpp" line="5542"/> <source>Un-Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5471"/> + <location filename="mainwindow.cpp" line="5544"/> <source>Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5477"/> + <location filename="mainwindow.cpp" line="5550"/> <source>Write To File...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5502"/> + <location filename="mainwindow.cpp" line="5580"/> <source>Do you want to endorse Mod Organizer on %1 now?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5516"/> + <location filename="mainwindow.cpp" line="5594"/> <source>Abstain from Endorsing Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5517"/> + <location filename="mainwindow.cpp" line="5595"/> <source>Are you sure you want to abstain from endorsing Mod Organizer 2? You will have to visit the mod page on the %1 Nexus site to change your mind.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5598"/> - <location filename="mainwindow.cpp" line="5599"/> + <location filename="mainwindow.cpp" line="5675"/> + <location filename="mainwindow.cpp" line="5676"/> <source>Thank you for endorsing MO2! :)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5601"/> - <location filename="mainwindow.cpp" line="5602"/> + <location filename="mainwindow.cpp" line="5678"/> + <location filename="mainwindow.cpp" line="5679"/> <source>Please reconsider endorsing MO2 on Nexus!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5837"/> + <location filename="mainwindow.cpp" line="5914"/> <source>Thank you!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5837"/> + <location filename="mainwindow.cpp" line="5914"/> <source>Thank you for your endorsement!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5840"/> + <location filename="mainwindow.cpp" line="5917"/> <source>Okay.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5840"/> + <location filename="mainwindow.cpp" line="5917"/> <source>This mod will not be endorsed and will no longer ask you to endorse.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5899"/> + <location filename="mainwindow.cpp" line="5976"/> <source>Mod ID %1 no longer seems to be available on Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5901"/> + <location filename="mainwindow.cpp" line="5978"/> <source>Request to Nexus failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5937"/> - <location filename="mainwindow.cpp" line="5999"/> + <location filename="mainwindow.cpp" line="5994"/> + <location filename="mainwindow.cpp" line="6056"/> <source>failed to read %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5949"/> - <location filename="mainwindow.cpp" line="6441"/> + <location filename="mainwindow.cpp" line="6006"/> + <location filename="mainwindow.cpp" line="6493"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5949"/> + <location filename="mainwindow.cpp" line="6006"/> <source>failed to extract %1 (errorcode %2)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5981"/> + <location filename="mainwindow.cpp" line="6038"/> <source>Extract BSA</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6010"/> + <location filename="mainwindow.cpp" line="6067"/> <source>This archive contains invalid hashes. Some files may be broken.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6056"/> + <location filename="mainwindow.cpp" line="6113"/> <source>Extract...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6086"/> + <location filename="mainwindow.cpp" line="6142"/> <source>This will restart MO, continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6133"/> + <location filename="mainwindow.cpp" line="6189"/> <source>Edit Categories...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6134"/> + <location filename="mainwindow.cpp" line="6190"/> <source>Deselect filter</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6187"/> - <source>Remove</source> + <location filename="mainwindow.cpp" line="6249"/> + <source>Remove '%1' from the toolbar</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6203"/> + <location filename="mainwindow.cpp" line="6270"/> <source>Enable all</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6204"/> + <location filename="mainwindow.cpp" line="6271"/> <source>Disable all</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6225"/> + <location filename="mainwindow.cpp" line="6292"/> <source>Unlock load order</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6228"/> + <location filename="mainwindow.cpp" line="6295"/> <source>Lock load order</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6238"/> + <location filename="mainwindow.cpp" line="6305"/> <source>Open Origin in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6243"/> + <location filename="mainwindow.cpp" line="6310"/> <source>Open Origin Info...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6387"/> + <location filename="mainwindow.cpp" line="6439"/> <source>depends on missing "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6391"/> + <location filename="mainwindow.cpp" line="6443"/> <source>incompatible with "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6417"/> + <location filename="mainwindow.cpp" line="6469"/> <source>Please wait while LOOT is running</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6514"/> + <location filename="mainwindow.cpp" line="6566"/> <source>loot failed. Exit code was: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6536"/> + <location filename="mainwindow.cpp" line="6588"/> <source>failed to start loot</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6539"/> + <location filename="mainwindow.cpp" line="6591"/> <source>failed to run loot: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6543"/> - <source>Errors occured</source> + <location filename="mainwindow.cpp" line="6595"/> + <source>Errors occurred</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6590"/> + <location filename="mainwindow.cpp" line="6642"/> <source>Backup of load order created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6600"/> + <location filename="mainwindow.cpp" line="6652"/> <source>Choose backup to restore</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6613"/> + <location filename="mainwindow.cpp" line="6665"/> <source>No Backups</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6613"/> + <location filename="mainwindow.cpp" line="6665"/> <source>There are no backups to restore</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6634"/> - <location filename="mainwindow.cpp" line="6656"/> + <location filename="mainwindow.cpp" line="6686"/> + <location filename="mainwindow.cpp" line="6708"/> <source>Restore failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6635"/> - <location filename="mainwindow.cpp" line="6657"/> + <location filename="mainwindow.cpp" line="6687"/> + <location filename="mainwindow.cpp" line="6709"/> <source>Failed to restore the backup. Errorcode: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6646"/> - <source>Backup of modlist created</source> + <location filename="mainwindow.cpp" line="6698"/> + <source>Backup of mod list created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6752"/> + <location filename="mainwindow.cpp" line="6804"/> <source>A file with the same name has already been downloaded. What would you like to do?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6754"/> + <location filename="mainwindow.cpp" line="6806"/> <source>Overwrite</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6755"/> + <location filename="mainwindow.cpp" line="6807"/> <source>Rename new file</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6756"/> + <location filename="mainwindow.cpp" line="6808"/> <source>Ignore file</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6865"/> + <location filename="mainwindow.cpp" line="6928"/> <source>Set the priority of the selected mods</source> <translation type="unfinished"></translation> </message> @@ -3414,6 +3544,17 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.</s <source>remove: invalid mod index %1</source> <translation type="unfinished"></translation> </message> + <message> + <location filename="modinfo.cpp" line="322"/> + <source>All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfo.cpp" line="325"/> + <source>You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</source> + <oldsource>You have mods that haven't been checked within 30 days using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</oldsource> + <translation type="unfinished"></translation> + </message> </context> <context> <name>ModInfoBackup</name> @@ -3432,226 +3573,230 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.</s </message> <message> <location filename="modinfodialog.ui" line="30"/> - <source>Textfiles</source> + <location filename="modinfodialog.ui" line="58"/> + <source>Text Files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="42"/> + <location filename="modinfodialog.ui" line="65"/> <source>A list of text-files in the mod directory.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="45"/> + <location filename="modinfodialog.ui" line="68"/> <source>A list of text-files in the mod directory like readmes. </source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="67"/> - <location filename="modinfodialog.ui" line="181"/> - <source>Save</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.ui" line="77"/> - <source>INI-Files</source> + <location filename="modinfodialog.ui" line="105"/> + <source>INI Files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="91"/> + <location filename="modinfodialog.ui" line="121"/> <source>Ini Files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="104"/> + <location filename="modinfodialog.ui" line="128"/> <source>This is a list of .ini files in the mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="107"/> + <location filename="modinfodialog.ui" line="131"/> <source>This is a list of .ini files in the mod. These are usually used to configure the behaviour of mods if there are configurable parameters.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="117"/> - <source>Ini Tweaks *This feature is non-functional*</source> + <location filename="modinfodialog.ui" line="163"/> + <source>Images</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="136"/> - <source>This is a list of ini tweaks (ini modifications that can be toggled).</source> + <location filename="modinfodialog.ui" line="229"/> + <source>Show .dds files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="139"/> - <source>This is a list of ini tweaks. Ini Tweaks are (usually small) fragments of ini files that are applied over existing settings in skyrim.ini/skyrimprefs.ini. Each tweak can be toggled individually. You should check the description of the mod wether the tweaks are really optional.</source> + <location filename="modinfodialog.ui" line="273"/> + <source>Open in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="175"/> - <source>Save changes to the file.</source> + <location filename="modinfodialog.ui" line="287"/> + <source>0x0</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="178"/> - <source>Save changes to the file. This overwrites the original. There is no automatic backup!</source> + <location filename="modinfodialog.ui" line="305"/> + <location filename="modinfodialog.ui" line="330"/> + <source>Optional ESPs</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="191"/> - <source>Images</source> + <location filename="modinfodialog.ui" line="337"/> + <source>List of esps, esms, and esls that can not be loaded by the game.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="246"/> - <source>Images located in the mod.</source> + <location filename="modinfodialog.ui" line="340"/> + <source>List of esps, esms, and esls contained in this plugin that currently can not be loaded by the game. They will not even appear in the esp-list in the main MO-window. +They usually contain optional functionality, see the readme. + +Most mods do not have optional esps, so chances are good you are looking at an empty list.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="249"/> - <source>This lists all the images (.jpg and .png) in the mod directory, like screenshots and such. Click one to get a larger view.</source> + <location filename="modinfodialog.ui" line="384"/> + <source>Move a file to the data directory.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="282"/> - <location filename="modinfodialog.ui" line="301"/> - <source>Optional ESPs</source> + <location filename="modinfodialog.ui" line="387"/> + <source>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes "available", it will not necessarily be loaded! That is configured in the main window of omo.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="288"/> - <source>List of esps, esms, and esls that can not be loaded by the game.</source> + <location filename="modinfodialog.ui" line="407"/> + <source>Make the selected mod in the lower list unavailable.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="291"/> - <source>List of esps, esms, and esls contained in this plugin that currently can not be loaded by the game. They will not even appear in the esp-list in the main MO-window. -They usually contain optional functionality, see the readme. - -Most mods do not have optional esps, so chances are good you are looking at an empty list.</source> + <location filename="modinfodialog.ui" line="410"/> + <source>The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become "invisible" to the game. It can then no longer be activated.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="316"/> - <source>Make the selected mod in the lower list unavailable.</source> + <location filename="modinfodialog.ui" line="460"/> + <source>Available ESPs</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="319"/> - <source>The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become "invisible" to the game. It can then no longer be activated.</source> + <location filename="modinfodialog.ui" line="467"/> + <source>ESPs in the data directory and thus visible to the game.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="345"/> - <source>Move a file to the data directory.</source> + <location filename="modinfodialog.ui" line="470"/> + <source><html><head/><body><p>These are the mod files that are in the (virtual) data directory of your game and will thus be selectable in the esp list in the main window.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="348"/> - <source>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes "available", it will not necessarily be loaded! That is configured in the main window of omo.</source> + <location filename="modinfodialog.ui" line="488"/> + <source>Conflicts</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="383"/> - <source>ESPs in the data directory and thus visible to the game.</source> + <location filename="modinfodialog.ui" line="498"/> + <source>General</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="386"/> - <source>These are the mod files that are in the (virtual) data directory of your game and will thus be selecteable in the esp list in the main window.</source> + <location filename="modinfodialog.ui" line="539"/> + <source>The following conflicted files are provided by this mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="393"/> - <source>Available ESPs</source> + <location filename="modinfodialog.ui" line="596"/> + <source>The following conflicted files are provided by other mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="401"/> - <source>Conflicts</source> + <location filename="modinfodialog.ui" line="650"/> + <source>The following files have no conflicts</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="409"/> - <source>The following conflicted files are provided by this mod</source> + <location filename="modinfodialog.ui" line="707"/> + <source>Advanced</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="459"/> - <location filename="modinfodialog.ui" line="515"/> - <source>File</source> + <location filename="modinfodialog.ui" line="777"/> + <source>Whether files that have no conflicts should be visible in the list</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="464"/> - <source>Overwritten Mods</source> + <location filename="modinfodialog.ui" line="780"/> + <source>Show files that have no conflicts</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="474"/> - <source>The following conflicted files are provided by other mods</source> + <location filename="modinfodialog.ui" line="790"/> + <source>Shows all mods overwriting or being overwritten by this mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="520"/> - <source>Providing Mod</source> + <location filename="modinfodialog.ui" line="793"/> + <source>Show all conflicting mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="530"/> - <source>Non-Conflicted files</source> + <location filename="modinfodialog.ui" line="803"/> + <source>Shows only the nearest conflicting mods, in order of priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="550"/> + <location filename="modinfodialog.ui" line="806"/> + <source>Show nearest conflicting mod</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialog.ui" line="828"/> + <source>Filter</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialog.ui" line="849"/> <source>Categories</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="573"/> + <location filename="modinfodialog.ui" line="869"/> <source>Primary Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="590"/> + <location filename="modinfodialog.ui" line="886"/> <source>Nexus Info</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="598"/> + <location filename="modinfodialog.ui" line="909"/> <source>Mod ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="605"/> + <location filename="modinfodialog.ui" line="916"/> <source>Mod ID for this mod on Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="608"/> + <location filename="modinfodialog.ui" line="919"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Mod ID for this mod on Nexus. This is filled in automatically if you downloaded and installed the mod from inside MO. Otherwise you can enter it manually. To find the correct id, find the mod on nexus. The URL will look like this: <a href=" https://www.nexusmods.com/skyrimspecialedition/mods/6194"><span style=" text-decoration: underline; color:#0000ff;">https://www.nexusmods.com/skyrimspecialedition/mods/6194</span></a>. In this example, 6194 is the id you're looking for. Besides: The above is the link to Mod Organizer2 on Nexus. Why not go there now and endorse us?</p></body></html></source> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Mod ID for this mod on Nexus. This is filled in automatically if you downloaded and installed the mod from inside MO. Otherwise you can enter it manually. To find the correct id, find the mod on nexus. The URL will look like this: <a href=" https://www.nexusmods.com/skyrimspecialedition/mods/6194"><span style=" text-decoration: underline; color:#0000ff;">https://www.nexusmods.com/skyrimspecialedition/mods/6194</span></a>. In this example, 6194 is the id you're looking for. Besides: The above is the link to Mod Organizer 2 on Nexus. Why not go there now and endorse us?</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="635"/> + <location filename="modinfodialog.ui" line="930"/> <source>Source Game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="642"/> + <location filename="modinfodialog.ui" line="937"/> <source>Source game for this mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="645"/> + <location filename="modinfodialog.ui" line="940"/> <source><html><head/><body><p>Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="665"/> + <location filename="modinfodialog.ui" line="947"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -3660,76 +3805,83 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="672"/> + <location filename="modinfodialog.ui" line="954"/> <source>Version</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="702"/> + <location filename="modinfodialog.ui" line="1004"/> + <location filename="modinfodialog.ui" line="1010"/> <source>Refresh</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="705"/> + <location filename="modinfodialog.ui" line="1007"/> <source>Refresh all information from Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="719"/> - <source>Description</source> + <location filename="modinfodialog.ui" line="1021"/> + <location filename="modinfodialog.ui" line="1146"/> + <source>Open in Browser</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="741"/> - <source>about:blank</source> + <location filename="modinfodialog.ui" line="1032"/> + <source>Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="767"/> - <source>Endorse</source> + <location filename="modinfodialog.ui" line="1043"/> + <source>Track</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialog.ui" line="1110"/> + <source>about:blank</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="782"/> - <source>Web page URL (only used if invalid NexusID) :</source> + <location filename="modinfodialog.ui" line="1136"/> + <source>Use Custom URL</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="795"/> + <location filename="modinfodialog.ui" line="1157"/> <source>Notes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="801"/> - <location filename="modinfodialog.ui" line="804"/> - <location filename="modinfodialog.ui" line="807"/> + <location filename="modinfodialog.ui" line="1163"/> + <location filename="modinfodialog.ui" line="1166"/> + <location filename="modinfodialog.ui" line="1169"/> <source>Enter comments about the mod here. These are displayed in the notes column of the mod list.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="814"/> - <location filename="modinfodialog.ui" line="817"/> - <location filename="modinfodialog.ui" line="823"/> + <location filename="modinfodialog.ui" line="1176"/> + <location filename="modinfodialog.ui" line="1179"/> + <location filename="modinfodialog.ui" line="1185"/> <source>Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="831"/> + <location filename="modinfodialog.ui" line="1193"/> <source>Filetree</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="845"/> + <location filename="modinfodialog.ui" line="1207"/> <source>Open Mod in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="870"/> + <location filename="modinfodialog.ui" line="1232"/> <source>A directory view of this mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="873"/> + <location filename="modinfodialog.ui" line="1235"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -3739,295 +3891,20 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="900"/> + <location filename="modinfodialog.ui" line="1265"/> <source>Previous</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="907"/> + <location filename="modinfodialog.ui" line="1275"/> <source>Next</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialog.ui" line="927"/> + <location filename="modinfodialog.ui" line="1298"/> <source>Close</source> <translation type="unfinished"></translation> </message> - <message> - <location filename="modinfodialog.cpp" line="234"/> - <source>&Delete</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="235"/> - <source>&Rename</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="236"/> - <source>&Hide</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="237"/> - <source>&Unhide</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="238"/> - <source>&Open</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="239"/> - <source>&New Folder</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="504"/> - <location filename="modinfodialog.cpp" line="519"/> - <source>Save changes?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="504"/> - <location filename="modinfodialog.cpp" line="519"/> - <source>Save changes to "%1"?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="710"/> - <source>File Exists</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="710"/> - <source>A file with that name exists, please enter a new one</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="727"/> - <source>failed to move file</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="752"/> - <source>failed to create directory "optional"</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="796"/> - <source>Info requested, please wait</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="804"/> - <source>Main</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="805"/> - <source>Update</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="806"/> - <source>Optional</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="807"/> - <source>Old</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="808"/> - <source>Miscellaneous</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="809"/> - <source>Deleted</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="810"/> - <source>Unknown</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="821"/> - <source>Current Version: %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="825"/> - <source>No update available</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="842"/> - <source><div style="text-align: center;"><h1>Uh oh!</h1><p>Sorry, there is no description available for this mod. :(</p></div></source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="858"/> - <source><a href="%1">Visit on Nexus</a></source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="957"/> - <source>Failed to delete %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="973"/> - <location filename="modinfodialog.cpp" line="979"/> - <location filename="modinfodialog.cpp" line="998"/> - <location filename="modinfodialog.cpp" line="1003"/> - <source>Confirm</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="973"/> - <location filename="modinfodialog.cpp" line="998"/> - <source>Are sure you want to delete "%1"?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="979"/> - <location filename="modinfodialog.cpp" line="1003"/> - <source>Are sure you want to delete the selected files?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1077"/> - <location filename="modinfodialog.cpp" line="1083"/> - <source>New Folder</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1089"/> - <source>Failed to create "%1"</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1193"/> - <location filename="modinfodialog.cpp" line="1217"/> - <source>Replace file?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1193"/> - <source>There already is a hidden version of this file. Replace it?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1196"/> - <location filename="modinfodialog.cpp" line="1220"/> - <source>File operation failed</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1196"/> - <location filename="modinfodialog.cpp" line="1220"/> - <source>Failed to remove "%1". Maybe you lack the required file permissions?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1207"/> - <location filename="modinfodialog.cpp" line="1230"/> - <source>failed to rename %1 to %2</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1217"/> - <source>There already is a visible version of this file. Replace it?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1293"/> - <source>Select binary</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1293"/> - <source>Binary</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1365"/> - <source>file not found: %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1378"/> - <source>failed to generate preview for %1</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1394"/> - <source>Sorry</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1394"/> - <source>Sorry, can't preview anything. This function currently does not support extracting from bsas.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1408"/> - <source>Un-Hide</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1410"/> - <source>Hide</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1413"/> - <location filename="modinfodialog.cpp" line="1433"/> - <source>Open/Execute</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1417"/> - <location filename="modinfodialog.cpp" line="1437"/> - <source>Preview</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1488"/> - <source>Name</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1488"/> - <source>Please enter a name</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1492"/> - <location filename="modinfodialog.cpp" line="1495"/> - <source>Error</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1492"/> - <source>Invalid name. Must be a valid file name</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1495"/> - <source>A tweak by that name exists</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinfodialog.cpp" line="1509"/> - <source>Create Tweak</source> - <translation type="unfinished"></translation> - </message> </context> <context> <name>ModInfoForeign</name> @@ -4063,18 +3940,12 @@ p, li { white-space: pre-wrap; } <context> <name>ModInfoRegular</name> <message> - <location filename="modinforegular.cpp" line="197"/> - <location filename="modinforegular.cpp" line="200"/> - <source>failed to write %1/meta.ini: error %2</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="modinforegular.cpp" line="651"/> + <location filename="modinforegular.cpp" line="731"/> <source>%1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinforegular.cpp" line="655"/> + <location filename="modinforegular.cpp" line="735"/> <source>Categories: <br></source> <translation type="unfinished"></translation> </message> @@ -4285,123 +4156,123 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1101"/> + <location filename="modlist.cpp" line="1103"/> <source>drag&drop failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1180"/> + <location filename="modlist.cpp" line="1182"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1181"/> + <location filename="modlist.cpp" line="1183"/> <source>Are you sure you want to remove "%1"?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1248"/> + <location filename="modlist.cpp" line="1250"/> <source>Flags</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1249"/> + <location filename="modlist.cpp" line="1251"/> <source>Content</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1250"/> + <location filename="modlist.cpp" line="1252"/> <source>Mod Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1251"/> + <location filename="modlist.cpp" line="1253"/> <source>Version</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1252"/> + <location filename="modlist.cpp" line="1254"/> <source>Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1253"/> + <location filename="modlist.cpp" line="1255"/> <source>Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1254"/> + <location filename="modlist.cpp" line="1256"/> <source>Source Game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1255"/> + <location filename="modlist.cpp" line="1257"/> <source>Nexus ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1256"/> + <location filename="modlist.cpp" line="1258"/> <source>Installation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1257"/> + <location filename="modlist.cpp" line="1259"/> <source>Notes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1258"/> - <location filename="modlist.cpp" line="1294"/> + <location filename="modlist.cpp" line="1260"/> + <location filename="modlist.cpp" line="1296"/> <source>unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1266"/> + <location filename="modlist.cpp" line="1268"/> <source>Name of your mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1267"/> + <location filename="modlist.cpp" line="1269"/> <source>Version of the mod (if available)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1268"/> + <location filename="modlist.cpp" line="1270"/> <source>Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1270"/> + <location filename="modlist.cpp" line="1272"/> <source>Category of the mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1271"/> + <location filename="modlist.cpp" line="1273"/> <source>The source game which was the origin of this mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1272"/> + <location filename="modlist.cpp" line="1274"/> <source>Id of the mod as used on Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1273"/> + <location filename="modlist.cpp" line="1275"/> <source>Emblemes to highlight things that might require attention.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1274"/> + <location filename="modlist.cpp" line="1276"/> <source>Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr><tr><td><img src=":/MO/gui/content/inifile" width=32/></td><td>INI files</td></tr><tr><td><img src=":/MO/gui/content/modgroup" width=32/></td><td>ModGroup files</td></tr></table></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1292"/> + <location filename="modlist.cpp" line="1294"/> <source>Time this mod was installed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1293"/> + <location filename="modlist.cpp" line="1295"/> <source>User notes about the mod</source> <translation type="unfinished"></translation> </message> @@ -4430,12 +4301,12 @@ p, li { white-space: pre-wrap; } <context> <name>MyFileSystemModel</name> <message> - <location filename="overwriteinfodialog.cpp" line="48"/> + <location filename="overwriteinfodialog.cpp" line="49"/> <source>Overwrites</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="60"/> + <location filename="overwriteinfodialog.cpp" line="61"/> <source>not implemented</source> <translation type="unfinished"></translation> </message> @@ -4449,27 +4320,27 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="235"/> + <location filename="nxmaccessmanager.cpp" line="246"/> <source>There was a timeout during the request</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="257"/> + <location filename="nxmaccessmanager.cpp" line="268"/> <source>Unknown error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="295"/> + <location filename="nxmaccessmanager.cpp" line="308"/> <source>Validation failed, please reauthenticate in the Settings -> Nexus tab: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="300"/> + <location filename="nxmaccessmanager.cpp" line="313"/> <source>Could not parse response. Invalid JSON.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="306"/> + <location filename="nxmaccessmanager.cpp" line="319"/> <source>Unknown error.</source> <translation type="unfinished"></translation> </message> @@ -4477,7 +4348,7 @@ p, li { white-space: pre-wrap; } <context> <name>NXMUrl</name> <message> - <location filename="../../uibase/src/nxmurl.cpp" line="33"/> + <location filename="../../uibase/src/nxmurl.cpp" line="34"/> <source>invalid nxm-link: %1</source> <translation type="unfinished"></translation> </message> @@ -4485,257 +4356,403 @@ p, li { white-space: pre-wrap; } <context> <name>NexusInterface</name> <message> - <location filename="nexusinterface.cpp" line="246"/> + <location filename="nexusinterface.cpp" line="284"/> <source>Failed to guess mod id for "%1", please pick the correct one</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="806"/> + <location filename="nexusinterface.cpp" line="674"/> + <source>You must authorize MO2 in Settings -> Nexus to use the Nexus API.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusinterface.cpp" line="683"/> + <source>You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusinterface.cpp" line="735"/> + <source>Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusinterface.cpp" line="842"/> <source>empty response</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="873"/> + <location filename="nexusinterface.cpp" line="900"/> <source>invalid response</source> <translation type="unfinished"></translation> </message> </context> <context> + <name>NexusManualKeyDialog</name> + <message> + <location filename="nexusmanualkey.ui" line="14"/> + <source>Manual Nexus API Key</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="50"/> + <source>1. Get your personal API key</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="57"/> + <source>Open Browser</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="97"/> + <source>2. Enter your personal API key</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="117"/> + <source>Paste</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="124"/> + <source>Clear</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="146"/> + <source>Enter API key here</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusmanualkey.ui" line="171"/> + <source><b>Sharing this key with anyone could get your Nexus account banned. You can always revoke this key from your account on the Nexus website.</b></source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NexusTab</name> + <message> + <location filename="modinfodialognexus.cpp" line="130"/> + <source>Current Version: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialognexus.cpp" line="134"/> + <source>No update available</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialognexus.cpp" line="166"/> + <source>Tracked</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialognexus.cpp" line="169"/> + <source>Untracked</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialognexus.cpp" line="226"/> + <source> + <div style="text-align: center;"> + <p>This mod does not have a valid Nexus ID. You can add a custom web + page for it in the "Custom URL" box below.</p> + </div></source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>NoConflictListModel</name> + <message> + <location filename="modinfodialogconflicts.cpp" line="322"/> + <source>File</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>OrganizerCore</name> <message> - <location filename="organizercore.cpp" line="408"/> - <location filename="organizercore.cpp" line="435"/> + <location filename="organizercore.cpp" line="393"/> + <location filename="organizercore.cpp" line="420"/> <source>Failed to write settings</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="409"/> - <source>An error occured trying to update MO settings to %1: %2</source> + <location filename="organizercore.cpp" line="394"/> + <source>An error occurred trying to update MO settings to %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="430"/> + <location filename="organizercore.cpp" line="415"/> <source>File is write protected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="432"/> + <location filename="organizercore.cpp" line="417"/> <source>Invalid file format (probably a bug)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="433"/> + <location filename="organizercore.cpp" line="418"/> <source>Unknown error %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="436"/> - <source>An error occured trying to write back MO settings to %1: %2</source> + <location filename="organizercore.cpp" line="421"/> + <source>An error occurred trying to write back MO settings to %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="684"/> - <location filename="organizercore.cpp" line="695"/> + <location filename="organizercore.cpp" line="646"/> + <location filename="organizercore.cpp" line="657"/> <source>Download started</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="698"/> + <location filename="organizercore.cpp" line="660"/> <source>Download failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="981"/> - <location filename="organizercore.cpp" line="1018"/> - <location filename="organizercore.cpp" line="1029"/> - <location filename="organizercore.cpp" line="1087"/> + <location filename="organizercore.cpp" line="958"/> + <location filename="organizercore.cpp" line="995"/> + <location filename="organizercore.cpp" line="1006"/> + <location filename="organizercore.cpp" line="1064"/> <source>Installation cancelled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="982"/> - <location filename="organizercore.cpp" line="1030"/> + <location filename="organizercore.cpp" line="959"/> + <location filename="organizercore.cpp" line="1007"/> <source>Another installation is currently in progress.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="994"/> - <location filename="organizercore.cpp" line="1059"/> + <location filename="organizercore.cpp" line="971"/> + <location filename="organizercore.cpp" line="1036"/> <source>Installation successful</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1002"/> - <location filename="organizercore.cpp" line="1069"/> + <location filename="organizercore.cpp" line="979"/> + <location filename="organizercore.cpp" line="1046"/> <source>Configure Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1003"/> - <location filename="organizercore.cpp" line="1070"/> + <location filename="organizercore.cpp" line="980"/> + <location filename="organizercore.cpp" line="1047"/> <source>This mod contains ini tweaks. Do you want to configure them now?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1015"/> - <location filename="organizercore.cpp" line="1080"/> + <location filename="organizercore.cpp" line="992"/> + <location filename="organizercore.cpp" line="1057"/> <source>mod not found: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1019"/> - <location filename="organizercore.cpp" line="1088"/> + <location filename="organizercore.cpp" line="996"/> + <location filename="organizercore.cpp" line="1065"/> <source>The mod was not installed completely.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1301"/> + <location filename="organizercore.cpp" line="1335"/> + <source>file not found: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1348"/> + <source>failed to generate preview for %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1409"/> + <source>Sorry</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1410"/> + <source>Sorry, can't preview anything. This function currently does not support extracting from bsas.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1420"/> + <source>File '%1' not found.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1428"/> + <source>Failed to generate preview for %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1525"/> <source>Executable not found: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1336"/> + <location filename="organizercore.cpp" line="1560"/> <source>Start Steam?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1337"/> + <location filename="organizercore.cpp" line="1561"/> <source>Steam is required to be running already to correctly start the game. Should MO try to start steam now?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1360"/> + <location filename="organizercore.cpp" line="1584"/> <source>Steam: Access Denied</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1361"/> - <source>MO was denied access to the Steam process. This normally indicates that Steam is being run as administrator while MO is not. This can cause issues launching the game. It is recommended to not run Steam as administrator unless absolutely neccessary. + <location filename="organizercore.cpp" line="1585"/> + <source>MO was denied access to the Steam process. This normally indicates that Steam is being run as administrator while MO is not. This can cause issues launching the game. It is recommended to not run Steam as administrator unless absolutely necessary. Restart MO as administrator?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1408"/> + <location filename="organizercore.cpp" line="1632"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1416"/> + <location filename="organizercore.cpp" line="1640"/> <source>Windows Event Log Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1417"/> + <location filename="organizercore.cpp" line="1641"/> <source>The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed. Continue launching %1?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1429"/> + <location filename="organizercore.cpp" line="1653"/> <source>Blacklisted Executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1430"/> + <location filename="organizercore.cpp" line="1654"/> <source>The executable you are attempted to launch is blacklisted in the virtual file system. This will likely prevent the executable, and any executables that are launched by this one, from seeing any mods. This could extend to INI files, save games and any other virtualized files. Continue launching %1?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1527"/> + <location filename="organizercore.cpp" line="1752"/> <source>No profile set</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1834"/> + <location filename="organizercore.cpp" line="2059"/> <source>Failed to refresh list of esps: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1943"/> + <location filename="organizercore.cpp" line="2168"/> <source>Multiple esps/esls activated, please check that they don't conflict.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2031"/> + <location filename="organizercore.cpp" line="2232"/> + <source>You need to be logged in with Nexus</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="2271"/> <source>Download?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2032"/> + <location filename="organizercore.cpp" line="2272"/> <source>A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2167"/> - <location filename="organizercore.cpp" line="2216"/> + <location filename="organizercore.cpp" line="2407"/> + <location filename="organizercore.cpp" line="2456"/> <source>failed to update mod list: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2223"/> - <location filename="organizercore.cpp" line="2240"/> + <location filename="organizercore.cpp" line="2463"/> + <location filename="organizercore.cpp" line="2480"/> <source>login successful</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2247"/> + <location filename="organizercore.cpp" line="2487"/> <source>Login failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2248"/> + <location filename="organizercore.cpp" line="2488"/> <source>Login failed, try again?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2257"/> + <location filename="organizercore.cpp" line="2497"/> <source>login failed: %1. Download will not be associated with an account</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2265"/> + <location filename="organizercore.cpp" line="2505"/> <source>login failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2275"/> + <location filename="organizercore.cpp" line="2515"/> <source>login failed: %1. You need to log-in with Nexus to update MO.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2328"/> + <location filename="organizercore.cpp" line="2568"/> <source>MO1 "Script Extender" load mechanism has left hook.dll in your game folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2331"/> - <location filename="organizercore.cpp" line="2347"/> + <location filename="organizercore.cpp" line="2571"/> + <location filename="organizercore.cpp" line="2587"/> <source>Description missing</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2340"/> + <location filename="organizercore.cpp" line="2580"/> <source><a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2374"/> + <location filename="organizercore.cpp" line="2614"/> <source>failed to save load order: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="2446"/> + <location filename="organizercore.cpp" line="2686"/> <source>The designated write target "%1" is not enabled.</source> <translation type="unfinished"></translation> </message> </context> <context> + <name>OverwriteConflictListModel</name> + <message> + <location filename="modinfodialogconflicts.cpp" line="296"/> + <source>File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="297"/> + <source>Overwritten Mods</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>OverwriteInfoDialog</name> <message> <location filename="overwriteinfodialog.ui" line="14"/> @@ -4753,68 +4770,81 @@ Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="89"/> + <location filename="overwriteinfodialog.cpp" line="90"/> <source>&Delete</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="90"/> + <location filename="overwriteinfodialog.cpp" line="91"/> <source>&Rename</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="91"/> + <location filename="overwriteinfodialog.cpp" line="92"/> <source>&Open</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="92"/> + <location filename="overwriteinfodialog.cpp" line="93"/> <source>&New Folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="113"/> + <location filename="overwriteinfodialog.cpp" line="114"/> <source>mod not found: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="148"/> + <location filename="overwriteinfodialog.cpp" line="149"/> <source>Failed to delete "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="164"/> - <location filename="overwriteinfodialog.cpp" line="170"/> - <location filename="overwriteinfodialog.cpp" line="189"/> - <location filename="overwriteinfodialog.cpp" line="194"/> + <location filename="overwriteinfodialog.cpp" line="165"/> + <location filename="overwriteinfodialog.cpp" line="171"/> + <location filename="overwriteinfodialog.cpp" line="190"/> + <location filename="overwriteinfodialog.cpp" line="195"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="164"/> - <location filename="overwriteinfodialog.cpp" line="189"/> - <source>Are sure you want to delete "%1"?</source> + <location filename="overwriteinfodialog.cpp" line="165"/> + <location filename="overwriteinfodialog.cpp" line="190"/> + <source>Are you sure you want to delete "%1"?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="170"/> - <location filename="overwriteinfodialog.cpp" line="194"/> - <source>Are sure you want to delete the selected files?</source> + <location filename="overwriteinfodialog.cpp" line="171"/> + <location filename="overwriteinfodialog.cpp" line="195"/> + <source>Are you sure you want to delete the selected files?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="244"/> - <location filename="overwriteinfodialog.cpp" line="250"/> + <location filename="overwriteinfodialog.cpp" line="240"/> + <location filename="overwriteinfodialog.cpp" line="246"/> <source>New Folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="overwriteinfodialog.cpp" line="256"/> + <location filename="overwriteinfodialog.cpp" line="252"/> <source>Failed to create "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> + <name>OverwrittenConflictListModel</name> + <message> + <location filename="modinfodialogconflicts.cpp" line="309"/> + <source>File</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogconflicts.cpp" line="310"/> + <source>Providing Mod</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>PluginContainer</name> <message> <location filename="plugincontainer.cpp" line="330"/> @@ -5012,16 +5042,21 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="problemsdialog.cpp" line="51"/> - <location filename="problemsdialog.cpp" line="52"/> + <location filename="problemsdialog.cpp" line="56"/> + <location filename="problemsdialog.cpp" line="57"/> <source>Fix</source> <translation type="unfinished"></translation> </message> <message> - <location filename="problemsdialog.cpp" line="58"/> + <location filename="problemsdialog.cpp" line="63"/> <source>No guided fix</source> <translation type="unfinished"></translation> </message> + <message> + <location filename="problemsdialog.cpp" line="71"/> + <source>(There are no notifications)</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Profile</name> @@ -5097,10 +5132,6 @@ p, li { white-space: pre-wrap; } Missing files: </source> - <oldsource>Some of your profile-specific game INI files were missing. They will now be copied from the vanilla game folder. You might want double-check your settings. - -Missing files: -</oldsource> <translation type="unfinished"></translation> </message> <message> @@ -5385,7 +5416,8 @@ p, li { white-space: pre-wrap; } <location filename="../../uibase/src/report.cpp" line="34"/> <location filename="../../uibase/src/report.cpp" line="37"/> <location filename="main.cpp" line="98"/> - <location filename="organizercore.cpp" line="722"/> + <location filename="organizercore.cpp" line="684"/> + <location filename="organizercore.cpp" line="699"/> <source>Error</source> <translation type="unfinished"></translation> </message> @@ -5395,29 +5427,29 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="../../uibase/src/utility.cpp" line="86"/> + <location filename="../../uibase/src/utility.cpp" line="88"/> <source>removal of "%1" failed: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="../../uibase/src/utility.cpp" line="93"/> + <location filename="../../uibase/src/utility.cpp" line="95"/> <source>removal of "%1" failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="../../uibase/src/utility.cpp" line="97"/> + <location filename="../../uibase/src/utility.cpp" line="99"/> <source>"%1" doesn't exist (remove)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="../../uibase/src/utility.cpp" line="278"/> - <location filename="../../uibase/src/utility.cpp" line="303"/> + <location filename="../../uibase/src/utility.cpp" line="444"/> + <location filename="../../uibase/src/utility.cpp" line="469"/> <source>failed to create directory "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="../../uibase/src/utility.cpp" line="287"/> - <location filename="../../uibase/src/utility.cpp" line="310"/> + <location filename="../../uibase/src/utility.cpp" line="453"/> + <location filename="../../uibase/src/utility.cpp" line="476"/> <source>failed to copy "%1" to "%2"</source> <translation type="unfinished"></translation> </message> @@ -5480,8 +5512,39 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="gameinfoimpl.cpp" line="43"/> - <source>invalid game type %1</source> + <location filename="filerenamer.cpp" line="100"/> + <source>The hidden file "%1" already exists. Replace it?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filerenamer.cpp" line="103"/> + <source>The visible file "%1" already exists. Replace it?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filerenamer.cpp" line="113"/> + <source>Replace file?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filerenamer.cpp" line="152"/> + <location filename="filerenamer.cpp" line="176"/> + <source>File operation failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filerenamer.cpp" line="153"/> + <source>Failed to remove "%1". Maybe you lack the required file permissions?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filerenamer.cpp" line="177"/> + <source>failed to rename %1 to %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="48"/> + <source>Filter</source> <translation type="unfinished"></translation> </message> <message> @@ -5497,7 +5560,7 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="installationmanager.cpp" line="73"/> - <location filename="selfupdater.cpp" line="81"/> + <location filename="selfupdater.cpp" line="82"/> <source>invalid 7-zip32.dll: %1</source> <translation type="unfinished"></translation> </message> @@ -5523,7 +5586,7 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="instancemanager.cpp" line="105"/> - <source>Be Carefull! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.</source> + <source>Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.</source> <translation type="unfinished"></translation> </message> <message> @@ -5561,7 +5624,7 @@ If the folder was still in use, restart MO and try again.</source> </message> <message> <location filename="instancemanager.cpp" line="151"/> - <location filename="instancemanager.cpp" line="222"/> + <location filename="instancemanager.cpp" line="224"/> <source>Canceled</source> <translation type="unfinished"></translation> </message> @@ -5626,18 +5689,18 @@ If the folder was still in use, restart MO and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="268"/> + <location filename="instancemanager.cpp" line="270"/> <location filename="profile.cpp" line="69"/> <source>failed to create %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="271"/> + <location filename="instancemanager.cpp" line="273"/> <source>Data directory created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="272"/> + <location filename="instancemanager.cpp" line="274"/> <source>New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings.</source> <translation type="unfinished"></translation> </message> @@ -5707,7 +5770,7 @@ If the folder was still in use, restart MO and try again.</source> </message> <message> <location filename="main.cpp" line="99"/> - <location filename="organizercore.cpp" line="723"/> + <location filename="organizercore.cpp" line="685"/> <source>Failed to create "%1". Your user account probably lacks permission.</source> <translation type="unfinished"></translation> </message> @@ -5717,107 +5780,160 @@ If the folder was still in use, restart MO and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="315"/> + <location filename="main.cpp" line="308"/> + <location filename="main.cpp" line="346"/> + <location filename="main.cpp" line="360"/> + <source>The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2's VFS and will not run correctly.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="main.cpp" line="320"/> <source>Could not use configuration settings for game "%1", path "%2".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="319"/> - <location filename="main.cpp" line="342"/> - <location filename="main.cpp" line="359"/> + <location filename="main.cpp" line="324"/> + <location filename="main.cpp" line="353"/> + <location filename="main.cpp" line="375"/> <source>Please select the installation of %1 to manage</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="320"/> - <location filename="main.cpp" line="343"/> - <location filename="main.cpp" line="360"/> + <location filename="main.cpp" line="325"/> + <location filename="main.cpp" line="354"/> + <location filename="main.cpp" line="376"/> <source>Please select the game to manage</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="368"/> + <location filename="main.cpp" line="384"/> <source>Canceled finding %1 in "%2".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="369"/> + <location filename="main.cpp" line="385"/> <source>Canceled finding game in "%1".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="374"/> + <location filename="main.cpp" line="391"/> <source>%1 not identified in "%2". The directory is required to contain the game binary.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="375"/> - <source>No game identified in "%1". The directory is required to contain the game binary.</source> + <location filename="main.cpp" line="399"/> + <source>No game identified in "%1". The directory is required to contain the game binary.<br><br><b>These are the games supported by Mod Organizer:</b><ul>%2</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="533"/> + <location filename="main.cpp" line="598"/> <source>Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="570"/> + <location filename="main.cpp" line="635"/> <source>failed to start shortcut: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="592"/> + <location filename="main.cpp" line="657"/> <source>failed to start application: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="686"/> - <location filename="settings.cpp" line="1211"/> + <location filename="main.cpp" line="805"/> + <location filename="settings.cpp" line="1186"/> <source>Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="687"/> + <location filename="main.cpp" line="806"/> <source>An instance of Mod Organizer is already running</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="702"/> + <location filename="main.cpp" line="821"/> <source>Failed to set up instance</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="958"/> + <location filename="mainwindow.cpp" line="1247"/> <source>Please use "Help" from the toolbar to get usage instructions to all elements</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1453"/> - <location filename="mainwindow.cpp" line="5079"/> + <location filename="mainwindow.cpp" line="1773"/> + <location filename="mainwindow.cpp" line="5247"/> <source><Manage...></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1465"/> + <location filename="mainwindow.cpp" line="1785"/> <source>failed to parse profile %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="171"/> + <location filename="modinfodialogesps.cpp" line="314"/> + <source>File Exists</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogesps.cpp" line="315"/> + <source>A file with that name exists, please enter a new one</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogesps.cpp" line="334"/> + <location filename="modinfodialogesps.cpp" line="382"/> + <source>Failed to move file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogesps.cpp" line="366"/> + <source>Failed to create directory "optional"</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogtextfiles.cpp" line="140"/> + <source>Save changes?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modinfodialogtextfiles.cpp" line="141"/> + <source>Save changes to "%1"?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="172"/> <source>Failed to start "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="174"/> + <location filename="organizercore.cpp" line="175"/> <source>Waiting</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="175"/> + <location filename="organizercore.cpp" line="176"/> <source>Please press OK once you're logged into steam.</source> <translation type="unfinished"></translation> </message> <message> + <location filename="organizercore.cpp" line="700"/> + <source>One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is incompatible with MO2's VFS system.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1249"/> + <source>Select binary</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="organizercore.cpp" line="1250"/> + <source>Binary</source> + <translation type="unfinished"></translation> + </message> + <message> <location filename="plugincontainer.cpp" line="177"/> <source>failed to initialize plugin %1: %2</source> <translation type="unfinished"></translation> @@ -5849,29 +5965,29 @@ If the folder was still in use, restart MO and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="1218"/> + <location filename="settings.cpp" line="1193"/> <source>Script Extender</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="1225"/> + <location filename="settings.cpp" line="1200"/> <source>Proxy DLL</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="147"/> + <location filename="spawn.cpp" line="146"/> <source>failed to spawn "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="152"/> + <location filename="spawn.cpp" line="151"/> <source>Elevation required</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="153"/> + <location filename="spawn.cpp" line="152"/> <source>This process requires elevation to run. -This is a potential security risk so I highly advice you to investigate if +This is a potential security risk so I highly advise you to investigate if "%1" can be installed to work without elevation. @@ -5880,11 +5996,36 @@ You will be asked if you want to allow helper.exe to make changes to the system. <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="164"/> - <location filename="spawn.cpp" line="178"/> + <location filename="spawn.cpp" line="163"/> + <location filename="spawn.cpp" line="177"/> <source>failed to spawn "%1": %2</source> <translation type="unfinished"></translation> </message> + <message> + <location filename="statusbar.cpp" line="37"/> + <source>This tracks the number of queued Nexus API requests, as well as the remaining daily and hourly requests. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="statusbar.cpp" line="56"/> + <source>Loading...</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="texteditor.cpp" line="462"/> + <source>&Save</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="texteditor.cpp" line="469"/> + <source>&Word wrap</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="texteditor.cpp" line="474"/> + <source>&Open in Explorer</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>QueryOverwriteDialog</name> @@ -5996,63 +6137,63 @@ You will be asked if you want to allow helper.exe to make changes to the system. <context> <name>SelfUpdater</name> <message> - <location filename="selfupdater.cpp" line="95"/> + <location filename="selfupdater.cpp" line="96"/> <source>archive.dll not loaded: "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="178"/> + <location filename="selfupdater.cpp" line="179"/> <source>New update available (%1)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="179"/> + <location filename="selfupdater.cpp" line="180"/> <source>Do you want to install update? All your mods and setup will be left untouched. Select Show Details option to see the full change-log.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="183"/> + <location filename="selfupdater.cpp" line="184"/> <source>Install</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="200"/> + <location filename="selfupdater.cpp" line="201"/> <source>Download failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="201"/> + <location filename="selfupdater.cpp" line="202"/> <source>Failed to find correct download, please try again later.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="216"/> + <location filename="selfupdater.cpp" line="217"/> <source>Update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="217"/> + <location filename="selfupdater.cpp" line="218"/> <source>Download in progress</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="308"/> + <location filename="selfupdater.cpp" line="309"/> <source>Download failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="319"/> + <location filename="selfupdater.cpp" line="320"/> <source>Failed to install update: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="344"/> - <source>Failed to start %1: %2</source> + <location filename="selfupdater.cpp" line="338"/> + <source>Failed to start %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="selfupdater.cpp" line="353"/> + <location filename="selfupdater.cpp" line="346"/> <source>Error</source> <translation type="unfinished"></translation> </message> @@ -6060,46 +6201,40 @@ Select Show Details option to see the full change-log.</source> <context> <name>Settings</name> <message> - <location filename="settings.cpp" line="143"/> + <location filename="settings.cpp" line="146"/> <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="144"/> - <source>Sorry, failed to start the helper application</source> + <location filename="settings.cpp" line="146"/> + <source>Failed to start the helper application</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="552"/> - <location filename="settings.cpp" line="571"/> + <location filename="settings.cpp" line="566"/> + <location filename="settings.cpp" line="585"/> <source>attempt to store setting for unknown plugin "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="731"/> - <location filename="settings.cpp" line="986"/> - <source>Error</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="settings.cpp" line="732"/> - <source>Failed to retrieve a Nexus API key! Please try again. A browser window should open asking you to authorize.</source> + <location filename="settings.cpp" line="783"/> + <source>Restart Mod Organizer?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="803"/> + <location filename="settings.cpp" line="784"/> <source>In order to finish configuration changes, MO must be restarted. Restart it now?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="987"/> - <source>Failed to create "%1", you may not have the necessary permission. path remains unchanged.</source> + <location filename="settings.cpp" line="967"/> + <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="802"/> - <source>Restart Mod Organizer?</source> + <location filename="settings.cpp" line="968"/> + <source>Failed to create "%1", you may not have the necessary permission. path remains unchanged.</source> <translation type="unfinished"></translation> </message> </context> @@ -6341,7 +6476,7 @@ If you use pre-releases, never contact me directly by e-mail or via private mess </message> <message> <location filename="settingsdialog.ui" line="444"/> - <source>Important: All directories have to be writeable!</source> + <source>Important: All directories have to be writable!</source> <translation type="unfinished"></translation> </message> <message> @@ -6362,11 +6497,6 @@ If you use pre-releases, never contact me directly by e-mail or via private mess p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &quot;Connect to Nexus&quot; will open a Nexus webpage to authorise Mod Organizer. You will need to be logged into your Nexus account. The authorisation is stored in the Windows Credential Manager. Your Nexus username and password are not required or stored by Mod Organizer.</p></body></html></source> - <oldsource><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Allows automatic log-in when the Nexus-Page for the game is clicked. Please note that the obfuscation with which the password is stored in modorganizer.ini is not very strong. If you're worried someone might steal your password, don't store it here.</span></p></body></html></oldsource> <translation type="unfinished"></translation> </message> <message> @@ -6375,158 +6505,168 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="500"/> + <location filename="settingsdialog.ui" line="506"/> + <source>Manually enter the API key and try to login</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="settingsdialog.ui" line="509"/> + <source>Enter API Key Manually</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="settingsdialog.ui" line="516"/> <source>Clear the stored Nexus API key and force reauthorization.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="503"/> - <source>Revoke Nexus Authorization</source> + <location filename="settingsdialog.ui" line="519"/> + <source>Disconnect from Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="514"/> + <location filename="settingsdialog.ui" line="530"/> <source>Remove cache and cookies.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="517"/> + <location filename="settingsdialog.ui" line="533"/> <source>Clear Cache</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="548"/> + <location filename="settingsdialog.ui" line="564"/> <source>Disable automatic internet features</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="551"/> + <location filename="settingsdialog.ui" line="567"/> <source>Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="554"/> + <location filename="settingsdialog.ui" line="570"/> <source>Offline Mode</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="561"/> + <location filename="settingsdialog.ui" line="577"/> <source>Use a proxy for network connections.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="564"/> + <location filename="settingsdialog.ui" line="580"/> <source>Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="567"/> + <location filename="settingsdialog.ui" line="583"/> <source>Use HTTP Proxy (Uses System Settings)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="574"/> + <location filename="settingsdialog.ui" line="590"/> <source>Endorsement Integration</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="584"/> - <location filename="settingsdialog.ui" line="587"/> + <location filename="settingsdialog.ui" line="600"/> + <location filename="settingsdialog.ui" line="603"/> <source><html><head/><body><p>By default, a counter is displayed under the mod list. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="590"/> + <location filename="settingsdialog.ui" line="606"/> <source>Hide API Request Counter</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="601"/> + <location filename="settingsdialog.ui" line="617"/> <source>Associate with "Download with manager" links</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="630"/> + <location filename="settingsdialog.ui" line="646"/> <source>Known Servers (updated on download)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="651"/> + <location filename="settingsdialog.ui" line="667"/> <source>Preferred Servers (Drag & Drop)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="686"/> + <location filename="settingsdialog.ui" line="702"/> <source>Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="692"/> + <location filename="settingsdialog.ui" line="708"/> <source>Username</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="702"/> + <location filename="settingsdialog.ui" line="718"/> <source>Password</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="732"/> + <location filename="settingsdialog.ui" line="748"/> <source>If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="759"/> + <location filename="settingsdialog.ui" line="775"/> <source>Plugins</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="784"/> + <location filename="settingsdialog.ui" line="800"/> <source>Author:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="798"/> + <location filename="settingsdialog.ui" line="814"/> <source>Version:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="812"/> + <location filename="settingsdialog.ui" line="828"/> <source>Description:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="850"/> + <location filename="settingsdialog.ui" line="866"/> <source>Key</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="855"/> + <location filename="settingsdialog.ui" line="871"/> <source>Value</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="867"/> + <location filename="settingsdialog.ui" line="883"/> <source>Blacklisted Plugins (use <del> to remove):</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="878"/> + <location filename="settingsdialog.ui" line="894"/> <source>Workarounds</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="886"/> + <location filename="settingsdialog.ui" line="902"/> <source>Steam App ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="906"/> + <location filename="settingsdialog.ui" line="922"/> <source>The Steam AppID for your game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="909"/> + <location filename="settingsdialog.ui" line="925"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -6542,17 +6682,17 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="940"/> + <location filename="settingsdialog.ui" line="956"/> <source>Load Mechanism</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="960"/> + <location filename="settingsdialog.ui" line="976"/> <source>Select loading mechanism. See help for details.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="963"/> + <location filename="settingsdialog.ui" line="979"/> <source>Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -6563,28 +6703,28 @@ If you use the Steam version of Oblivion the default will NOT work. In this case <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="983"/> + <location filename="settingsdialog.ui" line="999"/> <source>Enforces that inactive ESPs and ESMs are never loaded.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="986"/> + <location filename="settingsdialog.ui" line="1002"/> <source>It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="990"/> + <location filename="settingsdialog.ui" line="1006"/> <source>Hide inactive ESPs/ESMs</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="997"/> + <location filename="settingsdialog.ui" line="1013"/> <source>Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1000"/> + <location filename="settingsdialog.ui" line="1016"/> <source>By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -6592,66 +6732,66 @@ If you disable this feature, MO will only display official DLCs this way. Please <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1006"/> + <location filename="settingsdialog.ui" line="1022"/> <source>Display mods installed outside MO</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1016"/> + <location filename="settingsdialog.ui" line="1032"/> <source>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1019"/> + <location filename="settingsdialog.ui" line="1035"/> <source>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1023"/> + <location filename="settingsdialog.ui" line="1039"/> <source>Force-enable game files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1033"/> - <location filename="settingsdialog.ui" line="1036"/> + <location filename="settingsdialog.ui" line="1049"/> + <location filename="settingsdialog.ui" line="1052"/> <source>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1039"/> + <location filename="settingsdialog.ui" line="1055"/> <source>Lock GUI when running executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1049"/> + <location filename="settingsdialog.ui" line="1065"/> <source>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1052"/> + <location filename="settingsdialog.ui" line="1068"/> <source><html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1055"/> + <location filename="settingsdialog.ui" line="1071"/> <source>Enable parsing of Archives (Experimental Feature)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1067"/> - <location filename="settingsdialog.ui" line="1071"/> + <location filename="settingsdialog.ui" line="1083"/> + <location filename="settingsdialog.ui" line="1087"/> <source>For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1075"/> + <location filename="settingsdialog.ui" line="1091"/> <source>Back-date BSAs</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1086"/> + <location filename="settingsdialog.ui" line="1102"/> <source>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -6660,48 +6800,48 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1093"/> + <location filename="settingsdialog.ui" line="1109"/> <source>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1096"/> + <location filename="settingsdialog.ui" line="1112"/> <source>Configure Executables Blacklist</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1106"/> - <location filename="settingsdialog.ui" line="1109"/> + <location filename="settingsdialog.ui" line="1122"/> + <location filename="settingsdialog.ui" line="1125"/> <source>Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1112"/> + <location filename="settingsdialog.ui" line="1128"/> <source>Reset Window Geometries</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1135"/> + <location filename="settingsdialog.ui" line="1151"/> <source>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1146"/> + <location filename="settingsdialog.ui" line="1162"/> <source>Diagnostics</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1154"/> + <location filename="settingsdialog.ui" line="1170"/> <source>Max Dumps To Keep</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1174"/> + <location filename="settingsdialog.ui" line="1190"/> <source>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1177"/> + <location filename="settingsdialog.ui" line="1193"/> <source> Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -6709,12 +6849,12 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1189"/> + <location filename="settingsdialog.ui" line="1205"/> <source>Hint: right click link and copy link location</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1192"/> + <location filename="settingsdialog.ui" line="1208"/> <source> Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -6724,17 +6864,17 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1209"/> + <location filename="settingsdialog.ui" line="1225"/> <source>Crash Dumps</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1216"/> + <location filename="settingsdialog.ui" line="1232"/> <source>Decides which type of crash dumps are collected when injected processes crash.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1219"/> + <location filename="settingsdialog.ui" line="1235"/> <source> Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -6745,80 +6885,81 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1229"/> + <location filename="settingsdialog.ui" line="1245"/> <source>None</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1234"/> + <location filename="settingsdialog.ui" line="1250"/> <source>Mini (recommended)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1239"/> + <location filename="settingsdialog.ui" line="1255"/> <source>Data</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1244"/> + <location filename="settingsdialog.ui" line="1260"/> <source>Full</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1272"/> + <location filename="settingsdialog.ui" line="1288"/> <source>Log Level</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1279"/> + <location filename="settingsdialog.ui" line="1295"/> <source>Decides the amount of data printed to "ModOrganizer.log"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1282"/> + <location filename="settingsdialog.ui" line="1298"/> <source> Decides the amount of data printed to "ModOrganizer.log". - "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. + "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. </source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1289"/> + <location filename="settingsdialog.ui" line="1305"/> <source>Debug</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1294"/> + <location filename="settingsdialog.ui" line="1310"/> <source>Info (recommended)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1299"/> + <location filename="settingsdialog.ui" line="1315"/> <source>Warning</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1304"/> + <location filename="settingsdialog.ui" line="1320"/> + <location filename="settingsdialog.cpp" line="492"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="117"/> + <location filename="settingsdialog.cpp" line="170"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="118"/> + <location filename="settingsdialog.cpp" line="171"/> <source>Changing the mod directory affects all your profiles! Mods not present (or named differently) in the new location will be disabled in all profiles. There is no way to undo this unless you backed up your profiles manually. Proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="154"/> + <location filename="settingsdialog.cpp" line="207"/> <source>Executables Blacklist</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="155"/> + <location filename="settingsdialog.cpp" line="208"/> <source>Enter one executable per line to be blacklisted from the virtual file system. Mods and other virtualized files will not be visible to these executables and any executables launched by them. @@ -6829,82 +6970,53 @@ Example: <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="188"/> + <location filename="settingsdialog.cpp" line="241"/> <source>Select base directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="199"/> + <location filename="settingsdialog.cpp" line="252"/> <source>Select download directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="210"/> + <location filename="settingsdialog.cpp" line="263"/> <source>Select mod directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="221"/> + <location filename="settingsdialog.cpp" line="274"/> <source>Select cache directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="232"/> + <location filename="settingsdialog.cpp" line="285"/> <source>Select profiles directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="243"/> + <location filename="settingsdialog.cpp" line="296"/> <source>Select overwrite directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="253"/> + <location filename="settingsdialog.cpp" line="306"/> <source>Select game executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="332"/> + <location filename="settingsdialog.cpp" line="385"/> <source>Confirm?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.cpp" line="333"/> + <location filename="settingsdialog.cpp" line="386"/> <source>This will make all dialogs show up again where you checked the "Remember selection"-box. Continue?</source> <translation type="unfinished"></translation> </message> -</context> -<context> - <name>SimpleInstallDialog</name> - <message> - <location filename="simpleinstalldialog.ui" line="14"/> - <source>Quick Install</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="simpleinstalldialog.ui" line="22"/> - <source>Name</source> - <translation type="unfinished"></translation> - </message> <message> - <location filename="simpleinstalldialog.ui" line="49"/> - <location filename="simpleinstalldialog.ui" line="52"/> - <source>Opens a Dialog that allows custom modifications.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="simpleinstalldialog.ui" line="55"/> - <source>Manual</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="simpleinstalldialog.ui" line="62"/> - <source>OK</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="simpleinstalldialog.ui" line="72"/> - <source>Cancel</source> + <location filename="settingsdialog.cpp" line="493"/> + <source>Failed to retrieve a Nexus API key! Please try again. A browser window should open asking you to authorize.</source> <translation type="unfinished"></translation> </message> </context> @@ -7160,7 +7272,7 @@ On Windows XP: </message> <message> <location filename="tutorials/tutorial_conflictresolution_main.js" line="59"/> - <source><img src="qrc:///MO/gui/emblem_conflict_redundant" /> indicates that the mod is completely overwrtten by another. You could as well disable it.</source> + <source><img src="qrc:///MO/gui/emblem_conflict_redundant" /> indicates that the mod is completely overwritten by another. You could as well disable it.</source> <translation type="unfinished"></translation> </message> <message> @@ -7286,15 +7398,12 @@ Please go along with the tutorial because some things can't be demonstrated <message> <location filename="tutorials/tutorial_firststeps_main.js" line="17"/> <source>The highlighted button provides hints on solving potential problems MO recognized automatically.</source> - <oldsource>The highlighted button provides hints on solving problems MO recognized automatically.</oldsource> <translation type="unfinished"></translation> </message> <message> <location filename="tutorials/tutorial_firststeps_main.js" line="19"/> <source> There IS a notification now but you may want to hold off on clearing it until after completing the tutorial.</source> - <oldsource> -There IS a problem now but you may want to hold off on fixing it until after completing the tutorial.</oldsource> <translation type="unfinished"></translation> </message> <message> @@ -7303,88 +7412,88 @@ There IS a problem now but you may want to hold off on fixing it until after com <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="39"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="40"/> <source>Finally there are tooltips on almost every part of Mod Organizer. If there is a control in MO you don't understand, please try hovering over it to get a short description or use "Help on UI" from the help menu to get a longer explanation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="46"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="47"/> <source>This list displays all mods installed through MO. It also displays installed DLCs and some mods installed outside MO but you have limited control over those in MO.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="53"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="54"/> <source>Before we start installing mods, let's have a quick look at the settings.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="65"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="66"/> <source>Now it's time to install a few mods!Please go along with this because we need a few mods installed to demonstrate other features</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="71"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="72"/> <source>There are a few ways to get mods into ModOrganizer. If you associated MO with NXM links in the settings you can now use your regular browser to send downloads from Nexus to MO. Click on "Nexus" to open nexus, find a mod and click the green download buttons on Nexus saying "Download with Manager".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="83"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="84"/> <source>You can also install mods from disk using the "Install Mod" button.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="89"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="90"/> <source>Downloads will appear on the "Downloads"-tab here. You have to download and install at least one mod to proceed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="97"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="98"/> <source>Great, you just installed your first mod. Please note that the installation procedure may differ based on how a mod was packaged.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="103"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="104"/> <source>Now you know all about downloading and installing mods but they are not enabled yet...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="108"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="109"/> <source>Install a few more mods if you want, then enable mods by checking them in the left pane. Mods that aren't enabled have no effect on the game whatsoever. </source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="117"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="118"/> <source>For some mods, enabling it on the left pane is all you have to do...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="122"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="123"/> <source>...but most contain plugins. These are plugins for the game and are required to add stuff to the game (new weapons, armors, quests, areas, ...). Please open the "Plugins"-tab to get a list of plugins.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="133"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="134"/> <source>You will notice some plugins are grayed out. These are part of the main game and can't be disabled.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="139"/> - <source>A single mod may contain zero, one or multiple esps. Some or all may be optional. If in doubt, please consult the documentation of the indiviual mod. To do so, right-click the mod and select "Information".</source> + <location filename="tutorials/tutorial_firststeps_main.js" line="140"/> + <source>A single mod may contain zero, one or multiple esps. Some or all may be optional. If in doubt, please consult the documentation of the individual mod. To do so, right-click the mod and select "Information".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="148"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="149"/> <source>Now you know how to download, install and enable mods. It's important you always start the game from inside MO, otherwise the mods you installed here won't work.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="156"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="157"/> <source>This combobox lets you choose <i>what</i> to start. This way you can start the game, Launcher, Script Extender, the Creation Kit or other tools. If you miss a tool you can also configure this list but that is an advanced topic.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_firststeps_main.js" line="164"/> + <location filename="tutorials/tutorial_firststeps_main.js" line="165"/> <source>This completes the basic tutorial. As homework go play a bit! After you have installed more mods you may want to read the tutorial on conflict resolution.</source> <translation type="unfinished"></translation> </message> @@ -7403,7 +7512,7 @@ It's important you always start the game from inside MO, otherwise the mods </message> <message> <location filename="tutorials/tutorial_firststeps_modinfo.js" line="19"/> - <source>We may re-visit this screen in later tutorials.</source> + <source>We may revisit this screen in later tutorials.</source> <translation type="unfinished"></translation> </message> </context> @@ -7422,115 +7531,205 @@ Please open the "Nexus"-tab</source> </message> <message> <location filename="tutorials/tutorial_firststeps_settings.js" line="19"/> - <source>You can also store your Nexus-credentials here for automatic login. The password is stored unencrypted on your disk!</source> + <source>Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile.</source> + <oldsource>Use this interface to obtain an API key from NexusMods.This is used for all API connections - downloads, updatesetc. MO2 uses the Windows Credential Manager to storethis data securely. If the SSO page on Nexus is failing,use the manual entry and copy the API key from your profile.</oldsource> <translation type="unfinished"></translation> </message> </context> <context> <name>tutorial_primer_main</name> <message> - <location filename="tutorials/tutorial_primer_main.js" line="71"/> + <location filename="tutorials/tutorial_primer_main.js" line="72"/> <source>This window shows all the mods that are installed. The column headers can be used for sorting. Only checked mods are active in the current profile.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="72"/> + <location filename="tutorials/tutorial_primer_main.js" line="73"/> <source>Each profile is a separate set of enabled mods and ini settings.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="73"/> + <location filename="tutorials/tutorial_primer_main.js" line="74"/> + <source>Perform various actions on your mod list, such as refreshing data and checking for mod updates.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="75"/> + <source>Quick access to various directories, such as your MO2 mods, profiles, saves, and your active game location.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="76"/> + <source>Restore a mod list backup.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="77"/> + <source>Create a backup of your current mod list.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="78"/> + <source>Running counter of your active mods. Hover to see a more detailed breakdown.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="79"/> <source>The dropdown allows various ways of grouping the mods shown in the mod list.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="74"/> + <location filename="tutorials/tutorial_primer_main.js" line="80"/> <source>Show/hide the category pane.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="75"/> + <location filename="tutorials/tutorial_primer_main.js" line="81"/> <source>Quickly filter the mod list as you type.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="76"/> + <location filename="tutorials/tutorial_primer_main.js" line="82"/> <source>Switch between information views.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="77"/> + <location filename="tutorials/tutorial_primer_main.js" line="83"/> <source>This shows mod categories and some meta categories (in angle-brackets). Select some to filter the mod list. For example select "<Checked>" to show only active mods.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="78"/> + <location filename="tutorials/tutorial_primer_main.js" line="84"/> <source>Customizable list for choosing the program to run.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="79"/> + <location filename="tutorials/tutorial_primer_main.js" line="85"/> <source>When this button is clicked, Mod Organizer creates a virtual directory structure then runs the program selected to the left.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="80"/> + <location filename="tutorials/tutorial_primer_main.js" line="86"/> <source>Will create a shortcut for quick access. The shortcut can be placed in the toolbar at the top, in the Start Menu or on the Windows Desktop.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="81"/> + <location filename="tutorials/tutorial_primer_main.js" line="87"/> <source>Log messages produced by MO. Please note that messages with a light bulb usually don't require your attention.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="83"/> + <location filename="tutorials/tutorial_primer_main.js" line="88"/> + <source>Indicator of your current NexusMods API request limits.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="90"/> + <source>Change/manage MO2 instances or switch to portable mode.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="91"/> + <source>Browse to and manually install a mod from an archive on your computer.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="92"/> + <source>Automatically open NexusMods to browse and install mods via the API.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="93"/> + <source>Manage your MO2 profiles.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="94"/> + <source>Open the executable editor to add and modify applications you wish to run with MO2.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="95"/> + <source>Select from a collection of additional tools, such as an INI editor, integrated FNIS updater, and more.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="96"/> <source>Configure Mod Organizer.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="84"/> + <location filename="tutorials/tutorial_primer_main.js" line="97"/> + <source>See the status of and/or endorse MO2 on NexusMods.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="98"/> <source>Notifications about the current setup.</source> - <oldsource>Reports potential Problems about the current setup.</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="85"/> + <location filename="tutorials/tutorial_primer_main.js" line="99"/> <source>Activates if there is an update for MO. Please note that if, for any reason, MO can't communicate with NMM, this will not work either.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="89"/> + <location filename="tutorials/tutorial_primer_main.js" line="100"/> + <source>Access more information about MO2, including these tutorials, a link to the development discord, information about the devs and dependencies.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="104"/> <source>Plugins (esp/esm/esl files) of the mods in the current profile. They need to be checked to be loaded.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="90"/> + <location filename="tutorials/tutorial_primer_main.js" line="105"/> <source>Automatically sort plugins using the bundled LOOT application.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="91"/> + <location filename="tutorials/tutorial_primer_main.js" line="106"/> + <source>Restore a backup of your plugin list order.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="107"/> + <source>Save a backup of your plugin list order.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="108"/> + <source>Counter of your total active plugins. Hover to see a breakdown of plugin types.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="109"/> <source>Quickly filter plugin list as you type.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="97"/> + <location filename="tutorials/tutorial_primer_main.js" line="112"/> + <source>All the asset archives (.bsa files) for all active mods.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="tutorials/tutorial_primer_main.js" line="115"/> <source>The directory tree and all files that the program will see.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="100"/> + <location filename="tutorials/tutorial_primer_main.js" line="118"/> <source>Save game browser. Shows all the saves for the current profile and whether or not the current mod-load status is correct.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="103"/> + <location filename="tutorials/tutorial_primer_main.js" line="121"/> <source>Shows the mods that have been downloaded and if they’ve been installed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="tutorials/tutorial_primer_main.js" line="111"/> + <location filename="tutorials/tutorial_primer_main.js" line="129"/> <source>Click to quit</source> <translation type="unfinished"></translation> </message> diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 8212d248..81ec7f43 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -35,6 +35,7 @@ #include "instancemanager.h" #include <scriptextender.h> #include "helper.h" +#include "previewdialog.h" #include <QApplication> #include <QCoreApplication> @@ -111,7 +112,7 @@ static bool renameFile(const QString &oldName, const QString &newName, static std::wstring getProcessName(HANDLE process) { wchar_t buffer[MAX_PATH]; - wchar_t *fileName = L"unknown"; + const wchar_t *fileName = L"unknown"; if (process == nullptr) return fileName; @@ -267,6 +268,7 @@ bool checkService() return serviceRunning; } + OrganizerCore::OrganizerCore(const QSettings &initSettings) : m_UserInterface(nullptr) , m_PluginContainer(nullptr) @@ -363,34 +365,17 @@ QString OrganizerCore::commitSettings(const QString &iniFile) QSettings::Status OrganizerCore::storeSettings(const QString &fileName) { QSettings settings(fileName, QSettings::IniFormat); + if (m_UserInterface != nullptr) { m_UserInterface->storeSettings(settings); } + if (m_CurrentProfile != nullptr) { settings.setValue("selected_profile", m_CurrentProfile->name().toUtf8().constData()); } - settings.remove("customExecutables"); - settings.beginWriteArray("customExecutables"); - std::vector<Executable>::const_iterator current, end; - m_ExecutablesList.getExecutables(current, end); - int count = 0; - for (; current != end; ++current) { - const Executable &item = *current; - settings.setArrayIndex(count++); - settings.setValue("title", item.m_Title); - settings.setValue("custom", item.isCustom()); - settings.setValue("toolbar", item.isShownOnToolbar()); - settings.setValue("ownicon", item.usesOwnIcon()); - if (item.isCustom()) { - settings.setValue("binary", item.m_BinaryInfo.absoluteFilePath()); - settings.setValue("arguments", item.m_Arguments); - settings.setValue("workingDirectory", item.m_WorkingDirectory); - settings.setValue("steamAppID", item.m_SteamAppID); - } - } - settings.endArray(); + m_ExecutablesList.store(settings); FileDialogMemory::save(settings); @@ -406,7 +391,7 @@ void OrganizerCore::storeSettings() if (!shellCopy(iniFile, iniFile + ".new", true, qApp->activeWindow())) { QMessageBox::critical( qApp->activeWindow(), tr("Failed to write settings"), - tr("An error occured trying to update MO settings to %1: %2") + tr("An error occurred trying to update MO settings to %1: %2") .arg(iniFile, windowsErrorString(::GetLastError()))); return; } @@ -433,7 +418,7 @@ void OrganizerCore::storeSettings() : tr("Unknown error %1").arg(result); QMessageBox::critical( qApp->activeWindow(), tr("Failed to write settings"), - tr("An error occured trying to write back MO settings to %1: %2") + tr("An error occurred trying to write back MO settings to %1: %2") .arg(writeTarget, reason)); } } @@ -506,30 +491,7 @@ void OrganizerCore::updateExecutablesList(QSettings &settings) return; } - m_ExecutablesList.init(managedGame()); - - qDebug("setting up configured executables"); - - int numCustomExecutables = settings.beginReadArray("customExecutables"); - for (int i = 0; i < numCustomExecutables; ++i) { - settings.setArrayIndex(i); - - Executable::Flags flags; - if (settings.value("custom", true).toBool()) - flags |= Executable::CustomExecutable; - if (settings.value("toolbar", false).toBool()) - flags |= Executable::ShowInToolbar; - if (settings.value("ownicon", false).toBool()) - flags |= Executable::UseApplicationIcon; - - m_ExecutablesList.addExecutable( - settings.value("title").toString(), settings.value("binary").toString(), - settings.value("arguments").toString(), - settings.value("workingDirectory", "").toString(), - settings.value("steamAppID", "").toString(), flags); - } - - settings.endArray(); + m_ExecutablesList.load(managedGame(), settings); // TODO this has nothing to do with executables list move to an appropriate // function! @@ -729,12 +691,27 @@ bool OrganizerCore::createDirectory(const QString &path) { } } +bool OrganizerCore::checkPathSymlinks() { + bool hasSymlink = (QFileInfo(m_Settings.getProfileDirectory()).isSymLink() || + QFileInfo(m_Settings.getModDirectory()).isSymLink() || + QFileInfo(m_Settings.getOverwriteDirectory()).isSymLink()); + if (hasSymlink) { + QMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("One of the configured MO2 directories (profiles, mods, or overwrite) " + "is on a path containing a symbolic (or other) link. This is incompatible " + "with MO2's VFS system.")); + return false; + } + return true; +} + bool OrganizerCore::bootstrap() { return createDirectory(m_Settings.getProfileDirectory()) && createDirectory(m_Settings.getModDirectory()) && createDirectory(m_Settings.getDownloadDirectory()) && createDirectory(m_Settings.getOverwriteDirectory()) && - createDirectory(QString::fromStdWString(crashDumpsPath())) && cycleDiagnostics(); + createDirectory(QString::fromStdWString(crashDumpsPath())) && + checkPathSymlinks() && cycleDiagnostics(); } void OrganizerCore::createDefaultProfile() @@ -1004,8 +981,8 @@ MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName, "want to configure them now?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - m_UserInterface->displayModInformation(modInfo, modIndex, - ModInfoDialog::TAB_INIFILES); + m_UserInterface->displayModInformation( + modInfo, modIndex, ModInfoTabIDs::IniFiles); } m_ModInstalled(modName); m_DownloadManager.markInstalled(fileName); @@ -1071,8 +1048,8 @@ void OrganizerCore::installDownload(int index) "want to configure them now?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - m_UserInterface->displayModInformation(modInfo, modIndex, - ModInfoDialog::TAB_INIFILES); + m_UserInterface->displayModInformation( + modInfo, modIndex, ModInfoTabIDs::IniFiles); } m_ModInstalled(modName); @@ -1220,6 +1197,253 @@ QStringList OrganizerCore::modsSortedByProfilePriority() const return res; } +QString OrganizerCore::findJavaInstallation(const QString& jarFile) +{ + if (!jarFile.isEmpty()) { + // try to find java automatically based on the given jar file + std::wstring jarFileW = jarFile.toStdWString(); + + WCHAR buffer[MAX_PATH]; + if (::FindExecutableW(jarFileW.c_str(), nullptr, buffer) > (HINSTANCE)32) { + DWORD binaryType = 0UL; + if (!::GetBinaryTypeW(buffer, &binaryType)) { + qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); + } else if (binaryType == SCS_32BIT_BINARY || binaryType == SCS_64BIT_BINARY) { + return QString::fromWCharArray(buffer); + } + } + } + + // second attempt: look to the registry + QSettings reg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); + if (reg.contains("CurrentVersion")) { + QString currentVersion = reg.value("CurrentVersion").toString(); + return reg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); + } + + // not found + return {}; +} + +bool OrganizerCore::getFileExecutionContext( + QWidget* parent, const QFileInfo &targetInfo, + QFileInfo &binaryInfo, QString &arguments, FileExecutionTypes& type) +{ + QString extension = targetInfo.suffix(); + if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || + (extension.compare("com", Qt::CaseInsensitive) == 0) || + (extension.compare("bat", Qt::CaseInsensitive) == 0)) { + binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); + arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + type = FileExecutionTypes::Executable; + return true; + } else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { + binaryInfo = targetInfo; + type = FileExecutionTypes::Executable; + return true; + } else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { + auto java = findJavaInstallation(targetInfo.absoluteFilePath()); + + if (java.isEmpty()) { + java = QFileDialog::getOpenFileName( + parent, QObject::tr("Select binary"), + QString(), QObject::tr("Binary") + " (*.exe)"); + } + + if (java.isEmpty()) { + return false; + } + + binaryInfo = QFileInfo(java); + arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + type = FileExecutionTypes::Executable; + + return true; + } else { + type = FileExecutionTypes::Other; + return true; + } +} + +bool OrganizerCore::executeFileVirtualized( + QWidget* parent, const QFileInfo& targetInfo) +{ + QFileInfo binaryInfo; + QString arguments; + FileExecutionTypes type; + + if (!getFileExecutionContext(parent, targetInfo, binaryInfo, arguments, type)) { + return false; + } + + switch (type) + { + case FileExecutionTypes::Executable: { + spawnBinaryDirect( + binaryInfo, arguments, currentProfile()->name(), + targetInfo.absolutePath(), "", ""); + + return true; + } + + case FileExecutionTypes::Other: { + ::ShellExecuteW(nullptr, L"open", + ToWString(targetInfo.absoluteFilePath()).c_str(), + nullptr, nullptr, SW_SHOWNORMAL); + + return true; + } + } + + // nop + return false; +} + +bool OrganizerCore::previewFileWithAlternatives( + QWidget* parent, QString fileName, int selectedOrigin) +{ + fileName = QDir::fromNativeSeparators(fileName); + + // what we have is an absolute path to the file in its actual location (for the primary origin) + // what we want is the path relative to the virtual data directory + + // we need to look in the virtual directory for the file to make sure the info is up to date. + + // check if the file comes from the actual data folder instead of a mod + QDir gameDirectory = managedGame()->dataDirectory().absolutePath(); + QString relativePath = gameDirectory.relativeFilePath(fileName); + QDir dirRelativePath = gameDirectory.relativeFilePath(fileName); + + // if the file is on a different drive the dirRelativePath will actually be an + // absolute path so we make sure that is not the case + if (!dirRelativePath.isAbsolute() && !relativePath.startsWith("..")) { + fileName = relativePath; + } + else { + // crude: we search for the next slash after the base mod directory to skip + // everything up to the data-relative directory + int offset = settings().getModDirectory().size() + 1; + offset = fileName.indexOf("/", offset); + fileName = fileName.mid(offset + 1); + } + + + + const FileEntry::Ptr file = directoryStructure()->searchFile(ToWString(fileName), nullptr); + + if (file.get() == nullptr) { + reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); + return false; + } + + // set up preview dialog + PreviewDialog preview(fileName); + auto addFunc = [&](int originId) { + FilesOrigin &origin = directoryStructure()->getOriginByID(originId); + QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; + if (QFile::exists(filePath)) { + // it's very possible the file doesn't exist, because it's inside an archive. we don't support that + QWidget *wid = m_PluginContainer->previewGenerator().genPreview(filePath); + if (wid == nullptr) { + reportError(tr("failed to generate preview for %1").arg(filePath)); + } + else { + preview.addVariant(ToQString(origin.getName()), wid); + } + } + }; + + if (selectedOrigin == -1) { + // don't bother with the vector of origins, just add them as they come + addFunc(file->getOrigin()); + for (auto alt : file->getAlternatives()) { + addFunc(alt.first); + } + } else { + std::vector<int> origins; + + // start with the primary origin + origins.push_back(file->getOrigin()); + + // add other origins, push to front if it's the selected one + for (auto alt : file->getAlternatives()) { + if (alt.first == selectedOrigin) { + origins.insert(origins.begin(), alt.first); + } else { + origins.push_back(alt.first); + } + } + + // can't be empty; either the primary origin was the selected one, or it + // was one of the alternatives, which got inserted in front + + if (origins[0] != selectedOrigin) { + // sanity check, this shouldn't happen unless the caller passed an + // incorrect id + + qWarning().nospace() + << "selected preview origin " << selectedOrigin << " not found in " + << "list of alternatives"; + } + + for (int id : origins) { + addFunc(id); + } + } + + if (preview.numVariants() > 0) { + QSettings &s = settings().directInterface(); + QString key = QString("geometry/%1").arg(preview.objectName()); + if (s.contains(key)) { + preview.restoreGeometry(s.value(key).toByteArray()); + } + + preview.exec(); + + s.setValue(key, preview.saveGeometry()); + + return true; + } + else { + QMessageBox::information( + parent, tr("Sorry"), + tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); + + return false; + } +} + +bool OrganizerCore::previewFile( + QWidget* parent, const QString& originName, const QString& path) +{ + if (!QFile::exists(path)) { + reportError(tr("File '%1' not found.").arg(path)); + return false; + } + + PreviewDialog preview(path); + + QWidget *wid = m_PluginContainer->previewGenerator().genPreview(path); + if (wid == nullptr) { + reportError(tr("Failed to generate preview for %1").arg(path)); + return false; + } + + preview.addVariant(originName, wid); + + QSettings &s = settings().directInterface(); + QString key = QString("geometry/%1").arg(preview.objectName()); + if (s.contains(key)) { + preview.restoreGeometry(s.value(key).toByteArray()); + } + + preview.exec(); + + s.setValue(key, preview.saveGeometry()); + + return true; +} + void OrganizerCore::spawnBinary(const QFileInfo &binary, const QString &arguments, const QDir ¤tDirectory, @@ -1361,7 +1585,7 @@ HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, tr("MO was denied access to the Steam process. This normally indicates that " "Steam is being run as administrator while MO is not. This can cause issues " "launching the game. It is recommended to not run Steam as administrator unless " - "absolutely neccessary.\n\n" + "absolutely necessary.\n\n" "Restart MO as administrator?"), QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); if (result == QDialogButtonBox::Yes) { @@ -1492,19 +1716,20 @@ HANDLE OrganizerCore::runShortcut(const MOShortcut& shortcut) .arg(shortcut.instance(),shortcut.executable()) .toLocal8Bit().constData()); - Executable& exe = m_ExecutablesList.find(shortcut.executable()); + const Executable& exe = m_ExecutablesList.get(shortcut.executable()); + auto forcedLibaries = m_CurrentProfile->determineForcedLibraries(shortcut.executable()); if (!m_CurrentProfile->forcedLibrariesEnabled(shortcut.executable())) { forcedLibaries.clear(); } return spawnBinaryDirect( - exe.m_BinaryInfo, exe.m_Arguments, + exe.binaryInfo(), exe.arguments(), m_CurrentProfile->name(), - exe.m_WorkingDirectory.length() != 0 - ? exe.m_WorkingDirectory - : exe.m_BinaryInfo.absolutePath(), - exe.m_SteamAppID, + exe.workingDirectory().length() != 0 + ? exe.workingDirectory() + : exe.binaryInfo().absolutePath(), + exe.steamAppID(), "", forcedLibaries); } @@ -1543,13 +1768,13 @@ HANDLE OrganizerCore::startApplication(const QString &executable, currentDirectory = binary.absolutePath(); } try { - const Executable &exe = m_ExecutablesList.findByBinary(binary); - steamAppID = exe.m_SteamAppID; + const Executable &exe = m_ExecutablesList.getByBinary(binary); + steamAppID = exe.steamAppID(); customOverwrite - = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) + = m_CurrentProfile->setting("custom_overwrites", exe.title()) .toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); + if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); } } catch (const std::runtime_error &) { // nop @@ -1557,20 +1782,20 @@ HANDLE OrganizerCore::startApplication(const QString &executable, } else { // only a file name, search executables list try { - const Executable &exe = m_ExecutablesList.find(executable); - steamAppID = exe.m_SteamAppID; + const Executable &exe = m_ExecutablesList.get(executable); + steamAppID = exe.steamAppID(); customOverwrite - = m_CurrentProfile->setting("custom_overwrites", exe.m_Title) + = m_CurrentProfile->setting("custom_overwrites", exe.title()) .toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title); + if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); } if (arguments == "") { - arguments = exe.m_Arguments; + arguments = exe.arguments(); } - binary = exe.m_BinaryInfo; + binary = exe.binaryInfo(); if (cwd.length() == 0) { - currentDirectory = exe.m_WorkingDirectory; + currentDirectory = exe.workingDirectory(); } } catch (const std::runtime_error &) { qWarning("\"%s\" not set up as executable", @@ -1994,6 +2219,21 @@ void OrganizerCore::updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::P } } +void OrganizerCore::loggedInAction(QWidget* parent, std::function<void ()> f) +{ + if (NexusInterface::instance(m_PluginContainer)->getAccessManager()->validated()) { + f(); + } else { + QString apiKey; + if (settings().getNexusApiKey(apiKey)) { + doAfterLogin([f]{ f(); }); + NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus"), parent); + } + } +} + void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply) { if (m_PluginContainer != nullptr) { diff --git a/src/organizercore.h b/src/organizercore.h index bfb72529..99b1c5f2 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -56,6 +56,7 @@ namespace MOBase { class IPluginGame;
}
+
class OrganizerCore : public QObject, public MOBase::IPluginDiagnose
{
@@ -87,6 +88,12 @@ private: typedef boost::signals2::signal<void (const QString&)> SignalModInstalled;
public:
+ enum class FileExecutionTypes
+ {
+ Executable = 1,
+ Other = 2
+ };
+
static bool isNxmLink(const QString &link) { return link.startsWith("nxm://", Qt::CaseInsensitive); }
OrganizerCore(const QSettings &initSettings);
@@ -139,6 +146,17 @@ public: void updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> modInfos);
void doAfterLogin(const std::function<void()> &function) { m_PostLoginTasks.append(function); }
+ void loggedInAction(QWidget* parent, std::function<void ()> f);
+
+ static QString findJavaInstallation(const QString& jarFile={});
+
+ static bool getFileExecutionContext(
+ QWidget* parent, const QFileInfo &targetInfo,
+ QFileInfo &binaryInfo, QString &arguments, FileExecutionTypes& type);
+
+ bool executeFileVirtualized(QWidget* parent, const QFileInfo& targetInfo);
+ bool previewFileWithAlternatives(QWidget* parent, QString filename, int selectedOrigin=-1);
+ bool previewFile(QWidget* parent, const QString& originName, const QString& path);
void spawnBinary(const QFileInfo &binary, const QString &arguments = "",
const QDir ¤tDirectory = QDir(),
@@ -165,6 +183,7 @@ public: void loginFailedUpdate(const QString &message);
static bool createAndMakeWritable(const QString &path);
+ bool checkPathSymlinks();
bool bootstrap();
void createDefaultProfile();
diff --git a/src/overwriteinfodialog.cpp b/src/overwriteinfodialog.cpp index 3d82cf17..5ee8d76c 100644 --- a/src/overwriteinfodialog.cpp +++ b/src/overwriteinfodialog.cpp @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "ui_overwriteinfodialog.h"
#include "report.h"
#include "utility.h"
+#include "organizercore.h"
#include <QMessageBox>
#include <QMenu>
#include <QShortcut>
@@ -161,13 +162,13 @@ void OverwriteInfoDialog::delete_activated() }
else if (selection->selectedRows().count() == 1) {
QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0));
- if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName),
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
}
else {
- if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"),
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
@@ -186,12 +187,12 @@ void OverwriteInfoDialog::deleteTriggered() return;
} else if (m_FileSelection.count() == 1) {
QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0));
- if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName),
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
} else {
- if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"),
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
@@ -217,12 +218,7 @@ void OverwriteInfoDialog::renameTriggered() void OverwriteInfoDialog::openFile(const QModelIndex &index)
{
- QString fileName = m_FileSystemModel->filePath(index);
-
- HINSTANCE res = ::ShellExecuteW(nullptr, L"open", ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOW);
- if ((INT_PTR)res <= 32) {
- qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res);
- }
+ shell::OpenFile(m_FileSystemModel->filePath(index));
}
@@ -263,7 +259,7 @@ void OverwriteInfoDialog::createDirectoryTriggered() void OverwriteInfoDialog::on_explorerButton_clicked()
{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ shell::ExploreFile(m_ModInfo->absolutePath());
}
void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint &pos)
@@ -294,5 +290,6 @@ void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint & m_FileSelection.clear();
m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0));
}
- menu.exec(ui->filesView->mapToGlobal(pos));
+
+ menu.exec(ui->filesView->viewport()->mapToGlobal(pos));
}
diff --git a/src/pch.cpp b/src/pch.cpp new file mode 100644 index 00000000..1d9f38c5 --- /dev/null +++ b/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/pch.h b/src/pch.h new file mode 100644 index 00000000..1d8df43a --- /dev/null +++ b/src/pch.h @@ -0,0 +1,255 @@ +// std +#include <algorithm> +#include <assert.h> +#include <bitset> +#include <cstdarg> +#include <cstdint> +#include <ctime> +#include <exception> +#include <functional> +#include <iomanip> +#include <iostream> +#include <limits.h> +#include <list> +#include <map> +#include <memory> +#include <optional> +#include <regex> +#include <set> +#include <sstream> +#include <stddef.h> +#include <stdexcept> +#include <string.h> +#include <string> +#include <tuple> +#include <utility> +#include <vector> +#include <wchar.h> + +// windows +#include <Windows.h> +#include <DbgHelp.h> +#include <eh.h> +#include <LMCons.h> +#include <Psapi.h> +#include <Shellapi.h> +#include <shlobj.h> +#include <Shlwapi.h> +#include <tchar.h> +#include <wincred.h> + +// boost +#include <boost/algorithm/string.hpp> +#include <boost/algorithm/string/predicate.hpp> +#include <boost/assign.hpp> +#include <boost/bind.hpp> +#include <boost/function.hpp> +#include <boost/fusion/algorithm/iteration/for_each.hpp> +#include <boost/fusion/container.hpp> +#include <boost/fusion/include/at_key.hpp> +#include <boost/fusion/include/for_each.hpp> +#include <boost/fusion/sequence/intrinsic/at_key.hpp> +#include <boost/ptr_container/ptr_vector.hpp> +#include <boost/range/adaptor/reversed.hpp> +#include <boost/scoped_array.hpp> +#include <boost/scoped_ptr.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/signals2.hpp> +#include <boost/thread.hpp> +#include <boost/uuid/uuid_generators.hpp> +#include <boost/uuid/uuid_io.hpp> + +// openssl +#include <tlhelp32.h> + +// qt +#include <QAbstractButton> +#include <QAbstractItemDelegate> +#include <QAbstractItemModel> +#include <QAbstractItemView> +#include <QAbstractProxyModel> +#include <QAbstractTableModel> +#include <QAction> +#include <QApplication> +#include <QAtomicInt> +#include <QBuffer> +#include <QButtonGroup> +#include <QByteArray> +#include <QCheckBox> +#include <QClipboard> +#include <QCloseEvent> +#include <QColor> +#include <QColorDialog> +#include <QComboBox> +#include <QCommandLinkButton> +#include <QContextMenuEvent> +#include <QCoreApplication> +#include <QCryptographicHash> +#include <QCursor> +#include <QDataStream> +#include <QDate> +#include <QDateTime> +#include <QDebug> +#include <QDesktopServices> +#include <QDesktopWidget> +#include <QDialog> +#include <QDialogButtonBox> +#include <QDir> +#include <QDirIterator> +#include <QDragEnterEvent> +#include <QDropEvent> +#include <QEvent> +#include <QFile> +#include <QFileDialog> +#include <QFIleIconProvider> +#include <QFileInfo> +#include <QFileSystemModel> +#include <QFileSystemWatcher> +#include <QFlags> +#include <QFont> +#include <QFontDatabase> +#include <QFuture> +#include <QFutureWatcher> +#include <QHash> +#include <QHBoxLayout> +#include <QHeaderView> +#include <QIcon> +#include <QImageReader> +#include <QInputDialog> +#include <QIODevice> +#include <QItemDelegate> +#include <QItemSelection> +#include <QItemSelectionModel> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonValueRef> +#include <QKeyEvent> +#include <QLabel> +#include <QLCDNumber> +#include <QLibrary> +#include <QLibraryInfo> +#include <QLineEdit> +#include <QList> +#include <QListWidget> +#include <QListWidgetItem> +#include <QLocale> +#include <QLocalServer> +#include <QLocalSocket> +#include <QMainWindow> +#include <QMap> +#include <QMenu> +#include <QMessageBox> +#include <QMetaType> +#include <QMimeData> +#include <QModelIndex> +#include <QMouseEvent> +#include <QMultiHash> +#include <QMutex> +#include <QMutexLocker> +#include <QNetworkAccessManager> +#include <QNetworkCookie> +#include <QNetworkCookieJar> +#include <QNetworkDiskCache> +#include <QNetworkInterface> +#include <QNetworkProxy> +#include <QNetworkProxyFactory> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QObject> +#include <QPainter> +#include <QPalette> +#include <QPersistentModelIndex> +#include <QPixmap> +#include <QPixmapCache> +#include <QPluginLoader> +#include <QPoint> +#include <QPointer> +#include <QProcess> +#include <QProgressBar> +#include <QProgressDialog> +#include <QProxyStyle> +#include <QPushButton> +#include <QQueue> +#include <QRadioButton> +#include <QRect> +#include <QRegExp> +#include <QRegExpValidator> +#include <QRegularExpression> +#include <QResizeEvent> +#include <QScopedArrayPointer> +#include <QScopedPointer> +#include <QScrollBar> +#include <QSet> +#include <QSettings> +#include <QSharedMemory> +#include <QSharedPointer> +#include <QShortcut> +#include <QSignalMapper> +#include <QSize> +#include <QSizePolicy> +#include <QSortFilterProxyModel> +#include <QSpinBox> +#include <QSplashScreen> +#include <QSslSocket> +#include <QStandardItemModel> +#include <qstandardpaths.h> +#include <QStandardPaths> +#include <QString> +#include <QStringList> +#include <QStringListModel> +#include <QStyle> +#include <QStyledItemDelegate> +#include <QStyleFactory> +#include <QStyleOption> +#include <QStyleOptionSlider> +#include <QSyntaxHighlighter> +#include <Qt> +#include <QTableWidget> +#include <QTabWidget> +#include <QtAlgorithms> +#include <QtConcurrent/QtConcurrentRun> +#include <QtCore/QAbstractItemModel> +#include <QtCore/QObject> +#include <QtCore/QStack> +#include <QtDebug> +#include <QTemporaryFile> +#include <QTextBrowser> +#include <QTextCodec> +#include <QTextDocument> +#include <QTextEdit> +#include <QTextStream> +#include <QtGlobal> +#include <QtGui/QtGui> +#include <QThread> +#include <QTime> +#include <QTimer> +#include <QToolBar> +#include <QToolButton> +#include <QToolTip> +#include <QtPlatformHeaders/QWindowsWindowFunctions> +#include <QtPlugin> +#include <QTranslator> +#include <QTreeView> +#include <QTreeWidget> +#include <QTreeWidgetItem> +#include <QTreeWidgetItemIterator> +#include <QtTest/QtTest> +#include <QUrl> +#include <QUrlQuery> +#include <QVariant> +#include <QVariantList> +#include <QVariantMap> +#include <QVector> +#include <QVersionNumber> +#include <QWebEngineContextMenuData> +#include <QWebEngineHistory> +#include <QWebEnginePage> +#include <QWebEngineProfile> +#include <QWebEngineSettings> +#include <QWebEngineView> +#include <QWebSocket> +#include <QWhatsThis> +#include <QWhatsThisClickedEvent> +#include <QWidget> +#include <QWidgetAction> diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index ec9bdac3..2edb92f5 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -139,7 +139,7 @@ void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MO if (plugins.size() > 0) {
for (auto plugin : plugins) {
MOShared::FileEntry::Ptr file = directoryEntry.findFile(plugin.toStdWString());
- if (file->getOrigin() != origin.getID()) {
+ if (file && file->getOrigin() != origin.getID()) {
const std::vector<std::pair<int, std::pair<std::wstring, int>>> alternatives = file->getAlternatives();
if (std::find_if(alternatives.begin(), alternatives.end(), [&](const std::pair<int, std::pair<std::wstring, int>>& element) { return element.first == origin.getID(); }) == alternatives.end())
continue;
diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp index 795baab0..1e8e800f 100644 --- a/src/problemsdialog.cpp +++ b/src/problemsdialog.cpp @@ -1,5 +1,6 @@ #include "problemsdialog.h"
#include "ui_problemsdialog.h"
+#include "organizercore.h"
#include <utility.h>
#include <iplugin.h>
#include <iplugindiagnose.h>
@@ -10,8 +11,9 @@ using namespace MOBase;
-ProblemsDialog::ProblemsDialog(std::vector<QObject *> pluginObjects, QWidget *parent)
- : QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginObjects(pluginObjects)
+ProblemsDialog::ProblemsDialog(std::vector<QObject *> pluginObjects, QWidget *parent) :
+ QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginObjects(pluginObjects),
+ m_hasProblems(false)
{
ui->setupUi(this);
@@ -29,7 +31,9 @@ ProblemsDialog::~ProblemsDialog() void ProblemsDialog::runDiagnosis()
{
+ m_hasProblems = false;
ui->problemsWidget->clear();
+
for(QObject *pluginObj : m_PluginObjects) {
IPlugin *plugin = qobject_cast<IPlugin*>(pluginObj);
if (plugin != nullptr && !plugin->isActive())
@@ -46,6 +50,7 @@ void ProblemsDialog::runDiagnosis() newItem->setData(0, Qt::UserRole, diagnose->fullDescription(key));
ui->problemsWidget->addTopLevelItem(newItem);
+ m_hasProblems = true;
if (diagnose->hasGuidedFix(key)) {
newItem->setText(1, tr("Fix"));
@@ -59,11 +64,25 @@ void ProblemsDialog::runDiagnosis() }
}
}
+
+ if (!m_hasProblems) {
+ auto* item = new QTreeWidgetItem;
+
+ item->setText(0, tr("(There are no notifications)"));
+ item->setText(1, "");
+ item->setData(0, Qt::UserRole, QString());
+
+ QFont font = item->font(0);
+ font.setItalic(true);
+ item->setFont(0, font);
+
+ ui->problemsWidget->addTopLevelItem(item);
+ }
}
bool ProblemsDialog::hasProblems() const
{
- return ui->problemsWidget->topLevelItemCount() != 0;
+ return m_hasProblems;
}
void ProblemsDialog::selectionChanged()
@@ -87,5 +106,5 @@ void ProblemsDialog::startFix() void ProblemsDialog::urlClicked(const QUrl &url)
{
- ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ shell::OpenLink(url);
}
diff --git a/src/problemsdialog.h b/src/problemsdialog.h index a48a5de1..c211e4f5 100644 --- a/src/problemsdialog.h +++ b/src/problemsdialog.h @@ -21,20 +21,20 @@ public: ~ProblemsDialog();
bool hasProblems() const;
-private:
+private:
void runDiagnosis();
private slots:
-
void selectionChanged();
void urlClicked(const QUrl &url);
void startFix();
-private:
+private:
Ui::ProblemsDialog *ui;
std::vector<QObject *> m_PluginObjects;
+ bool m_hasProblems;
};
#endif // PROBLEMSDIALOG_H
diff --git a/src/profile.cpp b/src/profile.cpp index 4ccaa641..ef387027 100644 --- a/src/profile.cpp +++ b/src/profile.cpp @@ -982,7 +982,7 @@ int Profile::getPriorityMinimum() const return m_ModIndexByPriority.begin()->first; } -bool Profile::forcedLibrariesEnabled(const QString &executable) +bool Profile::forcedLibrariesEnabled(const QString &executable) const { return setting("forced_libraries", executable + "/enabled", false).toBool(); } @@ -992,7 +992,7 @@ void Profile::setForcedLibrariesEnabled(const QString &executable, bool enabled) storeSetting("forced_libraries", executable + "/enabled", enabled); } -QList<ExecutableForcedLoadSetting> Profile::determineForcedLibraries(const QString &executable) +QList<ExecutableForcedLoadSetting> Profile::determineForcedLibraries(const QString &executable) const { QList<ExecutableForcedLoadSetting> results; diff --git a/src/profile.h b/src/profile.h index a7ba7e91..bc7964f8 100644 --- a/src/profile.h +++ b/src/profile.h @@ -330,9 +330,9 @@ public: int getPriorityMinimum() const; - bool forcedLibrariesEnabled(const QString &executable); + bool forcedLibrariesEnabled(const QString &executable) const; void setForcedLibrariesEnabled(const QString &executable, bool enabled); - QList<MOBase::ExecutableForcedLoadSetting> determineForcedLibraries(const QString &executable); + QList<MOBase::ExecutableForcedLoadSetting> determineForcedLibraries(const QString &executable) const; void storeForcedLibraries(const QString &executable, const QList<MOBase::ExecutableForcedLoadSetting> &values); void removeForcedLibraries(const QString &executable); diff --git a/src/resources.qrc b/src/resources.qrc index 8645b27e..6fc33293 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -1,106 +1,108 @@ <RCC> - <qresource prefix="/MO/gui"> - <file>resources/help-browser.png</file> - <file alias="add">resources/list-add.png</file> - <file>resources/document-save.png</file> - <file>resources/edit-find-replace.png</file> - <file>resources/go-jump.png</file> - <file>resources/media-playback-start.png</file> - <file alias="remove">resources/process-stop.png</file> - <file>resources/system-search.png</file> - <file>resources/view-refresh.png</file> - <file alias="installer">resources/system-installer.png</file> - <file>resources/start-here.png</file> - <file>resources/list-remove.png</file> - <file>resources/document-properties.png</file> - <file>resources/go-up.png</file> - <file>resources/go-down.png</file> - <file>resources/switch-instance-icon.png</file> - <file alias="profiles">resources/contact-new.png</file> - <file alias="settings">resources/preferences-system.png</file> - <file alias="executable">resources/application-x-executable.png</file> - <file alias="information">resources/dialog-information.png</file> - <file alias="locked">resources/emblem-readonly.png</file> - <file alias="next">resources/go-next_16.png</file> - <file alias="previous">resources/go-previous_16.png</file> - <file alias="refresh">resources/view-refresh_16.png</file> - <file alias="update_available">resources/software-update-available.png</file> - <file alias="important">resources/emblem-important.png</file> - <file alias="check">resources/check.png</file> - <file alias="warning">resources/dialog-warning.png</file> - <file alias="emblem_backup">resources/symbol-backup.png</file> - <file alias="icon_tools">resources/applications-accessories.png</file> - <file alias="problem">resources/emblem-unreadable.png</file> - <file>resources/internet-web-browser.png</file> - <file alias="update">resources/system-software-update.png</file> - <file alias="help">resources/help-browser_32.png</file> - <file>resources/system-installer.png</file> - <file alias="icon_executable">resources/function.png</file> - <file alias="plugins">resources/plugins.png</file> - <file alias="edit_clear">resources/edit-clear.png</file> - <file alias="link">resources/dynamic-blue-right.png</file> - <file alias="icon_favorite">resources/icon-favorite.png</file> - <file alias="emblem_notendorsed">resources/emblem-favorite.png</file> - <file alias="emblem_conflict">resources/error.png</file> - <file alias="run">resources/show.png</file> - <file alias="splash">splash.png</file> - <file alias="emblem_conflict_mixed">resources/conflict-mixed.png</file> - <file alias="emblem_conflict_overwrite">resources/conflict-overwrite.png</file> - <file alias="emblem_conflict_overwritten">resources/conflict-overwritten.png</file> - <file alias="emblem_conflict_redundant">resources/conflict-redundant.png</file> - <file alias="archive_loose_conflict_mixed">resources/conflict-mixed-blue.png</file> - <file alias="archive_loose_conflict_overwrite">resources/conflict-overwrite-blue.png</file> - <file alias="archive_loose_conflict_overwritten">resources/red-archive-conflict-loser.png</file> - <file alias="emblem_notes">resources/accessories-text-editor.png</file> - <file alias="version_date">resources/x-office-calendar.png</file> - <file alias="warning_16">resources/dialog-warning_16.png</file> - <file alias="attachment">resources/mail-attachment.png</file> - <file alias="backup">resources/document-save_32.png</file> - <file alias="restore">resources/edit-undo.png</file> - <file alias="sort">resources/arrange-boxes.png</file> - <file alias="badge_1">resources/badge_1.png</file> - <file alias="badge_2">resources/badge_2.png</file> - <file alias="badge_3">resources/badge_3.png</file> - <file alias="badge_4">resources/badge_4.png</file> - <file alias="badge_5">resources/badge_5.png</file> - <file alias="badge_6">resources/badge_6.png</file> - <file alias="badge_7">resources/badge_7.png</file> - <file alias="badge_8">resources/badge_8.png</file> - <file alias="badge_9">resources/badge_9.png</file> - <file alias="badge_more">resources/badge_more.png</file> - <file alias="active">resources/status_active.png</file> - <file alias="awaiting">resources/status_awaiting.png</file> - <file alias="inactive">resources/status_inactive.png</file> - <file alias="app_icon">resources/mo_icon.png</file> - <file alias="package">resources/package.png</file> - <file alias="instance_switch">resources/switch-instance-icon.png</file> - <file alias="open_folder">resources/open-Folder-Icon.png</file> - <file alias="multiply_red">resources/multiply-red.png</file> - <file alias="archive_conflict_loser">resources/archive-conflict-loser.png</file> - <file alias="archive_conflict_mixed">resources/archive-conflict-mixed.png</file> - <file alias="archive_conflict_neutral">resources/archive-conflict-neutral.png</file> - <file alias="archive_conflict_winner">resources/archive-conflict-winner.png</file> - <file alias="alternate_game_alt">resources/game-warning.png</file> - <file alias="alternate_game">resources/game-warning-16.png</file> - <file alias="tracked">resources/tracked.png</file> - </qresource> - <qresource prefix="/MO/gui/content"> - <file alias="plugin">resources/contents/jigsaw-piece.png</file> - <file alias="skyproc">resources/contents/hand-of-god.png</file> - <file alias="texture">resources/contents/empty-chessboard.png</file> - <file alias="music">resources/contents/double-quaver.png</file> - <file alias="sound">resources/contents/lyre.png</file> - <file alias="interface">resources/contents/usable.png</file> - <file alias="skse">resources/contents/checkbox-tree.png</file> - <file alias="script">resources/contents/tinker.png</file> - <file alias="mesh">resources/contents/breastplate.png</file> - <file alias="string">resources/contents/conversation.png</file> - <file alias="bsa">resources/contents/locked-chest.png</file> - <file alias="menu">resources/contents/config.png</file> - <file alias="inifile">resources/contents/feather-and-scroll.png</file> - <file alias="modgroup">resources/contents/xedit.png</file> - </qresource> - <qresource prefix="/qt/etc"> - <file>qt.conf</file> - </qresource> + <qresource prefix="/MO/gui"> + <file alias="save">resources/save.svg</file> + <file alias="word-wrap">resources/word-wrap.svg</file> + <file>resources/help-browser.png</file> + <file alias="add">resources/list-add.png</file> + <file>resources/document-save.png</file> + <file>resources/edit-find-replace.png</file> + <file>resources/go-jump.png</file> + <file>resources/media-playback-start.png</file> + <file alias="remove">resources/process-stop.png</file> + <file>resources/system-search.png</file> + <file>resources/view-refresh.png</file> + <file alias="installer">resources/system-installer.png</file> + <file>resources/start-here.png</file> + <file>resources/list-remove.png</file> + <file>resources/document-properties.png</file> + <file>resources/go-up.png</file> + <file>resources/go-down.png</file> + <file>resources/switch-instance-icon.png</file> + <file alias="profiles">resources/contact-new.png</file> + <file alias="settings">resources/preferences-system.png</file> + <file alias="executable">resources/application-x-executable.png</file> + <file alias="information">resources/dialog-information.png</file> + <file alias="locked">resources/emblem-readonly.png</file> + <file alias="next">resources/go-next_16.png</file> + <file alias="previous">resources/go-previous_16.png</file> + <file alias="refresh">resources/view-refresh_16.png</file> + <file alias="update_available">resources/software-update-available.png</file> + <file alias="important">resources/emblem-important.png</file> + <file alias="check">resources/check.png</file> + <file alias="warning">resources/dialog-warning.png</file> + <file alias="emblem_backup">resources/symbol-backup.png</file> + <file alias="icon_tools">resources/applications-accessories.png</file> + <file alias="problem">resources/emblem-unreadable.png</file> + <file>resources/internet-web-browser.png</file> + <file alias="update">resources/system-software-update.png</file> + <file alias="help">resources/help-browser_32.png</file> + <file>resources/system-installer.png</file> + <file alias="icon_executable">resources/function.png</file> + <file alias="plugins">resources/plugins.png</file> + <file alias="edit_clear">resources/edit-clear.png</file> + <file alias="link">resources/dynamic-blue-right.png</file> + <file alias="icon_favorite">resources/icon-favorite.png</file> + <file alias="emblem_notendorsed">resources/emblem-favorite.png</file> + <file alias="emblem_conflict">resources/error.png</file> + <file alias="run">resources/show.png</file> + <file alias="splash">splash.png</file> + <file alias="emblem_conflict_mixed">resources/conflict-mixed.png</file> + <file alias="emblem_conflict_overwrite">resources/conflict-overwrite.png</file> + <file alias="emblem_conflict_overwritten">resources/conflict-overwritten.png</file> + <file alias="emblem_conflict_redundant">resources/conflict-redundant.png</file> + <file alias="archive_loose_conflict_mixed">resources/conflict-mixed-blue.png</file> + <file alias="archive_loose_conflict_overwrite">resources/conflict-overwrite-blue.png</file> + <file alias="archive_loose_conflict_overwritten">resources/red-archive-conflict-loser.png</file> + <file alias="emblem_notes">resources/accessories-text-editor.png</file> + <file alias="version_date">resources/x-office-calendar.png</file> + <file alias="warning_16">resources/dialog-warning_16.png</file> + <file alias="attachment">resources/mail-attachment.png</file> + <file alias="backup">resources/document-save_32.png</file> + <file alias="restore">resources/edit-undo.png</file> + <file alias="sort">resources/arrange-boxes.png</file> + <file alias="badge_1">resources/badge_1.png</file> + <file alias="badge_2">resources/badge_2.png</file> + <file alias="badge_3">resources/badge_3.png</file> + <file alias="badge_4">resources/badge_4.png</file> + <file alias="badge_5">resources/badge_5.png</file> + <file alias="badge_6">resources/badge_6.png</file> + <file alias="badge_7">resources/badge_7.png</file> + <file alias="badge_8">resources/badge_8.png</file> + <file alias="badge_9">resources/badge_9.png</file> + <file alias="badge_more">resources/badge_more.png</file> + <file alias="active">resources/status_active.png</file> + <file alias="awaiting">resources/status_awaiting.png</file> + <file alias="inactive">resources/status_inactive.png</file> + <file alias="app_icon">resources/mo_icon.png</file> + <file alias="package">resources/package.png</file> + <file alias="instance_switch">resources/switch-instance-icon.png</file> + <file alias="open_folder">resources/open-Folder-Icon.png</file> + <file alias="multiply_red">resources/multiply-red.png</file> + <file alias="archive_conflict_loser">resources/archive-conflict-loser.png</file> + <file alias="archive_conflict_mixed">resources/archive-conflict-mixed.png</file> + <file alias="archive_conflict_neutral">resources/archive-conflict-neutral.png</file> + <file alias="archive_conflict_winner">resources/archive-conflict-winner.png</file> + <file alias="alternate_game_alt">resources/game-warning.png</file> + <file alias="alternate_game">resources/game-warning-16.png</file> + <file alias="tracked">resources/tracked.png</file> + </qresource> + <qresource prefix="/MO/gui/content"> + <file alias="plugin">resources/contents/jigsaw-piece.png</file> + <file alias="skyproc">resources/contents/hand-of-god.png</file> + <file alias="texture">resources/contents/empty-chessboard.png</file> + <file alias="music">resources/contents/double-quaver.png</file> + <file alias="sound">resources/contents/lyre.png</file> + <file alias="interface">resources/contents/usable.png</file> + <file alias="skse">resources/contents/checkbox-tree.png</file> + <file alias="script">resources/contents/tinker.png</file> + <file alias="mesh">resources/contents/breastplate.png</file> + <file alias="string">resources/contents/conversation.png</file> + <file alias="bsa">resources/contents/locked-chest.png</file> + <file alias="menu">resources/contents/config.png</file> + <file alias="inifile">resources/contents/feather-and-scroll.png</file> + <file alias="modgroup">resources/contents/xedit.png</file> + </qresource> + <qresource prefix="/qt/etc"> + <file>qt.conf</file> + </qresource> </RCC> diff --git a/src/resources/go-down.png b/src/resources/go-down.png Binary files differindex af237881..bf0ce4fd 100644 --- a/src/resources/go-down.png +++ b/src/resources/go-down.png diff --git a/src/resources/go-next_16.png b/src/resources/go-next_16.png Binary files differindex 6ef8de76..58742d39 100644 --- a/src/resources/go-next_16.png +++ b/src/resources/go-next_16.png diff --git a/src/resources/go-previous_16.png b/src/resources/go-previous_16.png Binary files differindex 659cd90d..b4b22d04 100644 --- a/src/resources/go-previous_16.png +++ b/src/resources/go-previous_16.png diff --git a/src/resources/go-up.png b/src/resources/go-up.png Binary files differindex b0a0cd72..a4b4e022 100644 --- a/src/resources/go-up.png +++ b/src/resources/go-up.png diff --git a/src/resources/save.svg b/src/resources/save.svg new file mode 100644 index 00000000..3fb2a8df --- /dev/null +++ b/src/resources/save.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
\ No newline at end of file diff --git a/src/resources/word-wrap.svg b/src/resources/word-wrap.svg new file mode 100644 index 00000000..63b1f1f0 --- /dev/null +++ b/src/resources/word-wrap.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3 3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
\ No newline at end of file diff --git a/src/selectiondialog.cpp b/src/selectiondialog.cpp index 55728751..089760f9 100644 --- a/src/selectiondialog.cpp +++ b/src/selectiondialog.cpp @@ -79,6 +79,14 @@ QString SelectionDialog::getChoiceString() }
}
+QString SelectionDialog::getChoiceDescription()
+{
+ if (m_Choice == nullptr)
+ return QString();
+ else
+ return m_Choice->accessibleDescription();
+}
+
void SelectionDialog::disableCancel()
{
ui->cancelButton->setEnabled(false);
diff --git a/src/selectiondialog.h b/src/selectiondialog.h index 3c4b25df..5a70aa5e 100644 --- a/src/selectiondialog.h +++ b/src/selectiondialog.h @@ -52,6 +52,7 @@ public: QVariant getChoiceData();
QString getChoiceString();
+ QString getChoiceDescription();
void disableCancel();
diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp index 4c0f9a8d..271c621b 100644 --- a/src/selfupdater.cpp +++ b/src/selfupdater.cpp @@ -31,6 +31,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "settings.h"
#include "bbcode.h"
#include "plugincontainer.h"
+#include "organizercore.h"
#include <versioninfo.h>
#include <report.h>
#include <util.h>
@@ -178,7 +179,7 @@ void SelfUpdater::startUpdate() tr("New update available (%1)")
.arg(m_UpdateCandidate["tag_name"].toString()), tr("Do you want to install update? All your mods and setup will be left untouched.\nSelect Show Details option to see the full change-log."),
QMessageBox::Yes | QMessageBox::Cancel, m_Parent);
-
+
query.setDetailedText(m_UpdateCandidate["body"].toString());
query.button(QMessageBox::Yes)->setText(tr("Install"));
@@ -329,22 +330,14 @@ void SelfUpdater::downloadCancel() void SelfUpdater::installUpdate()
{
- const QString mopath
- = QDir::fromNativeSeparators(qApp->property("dataPath").toString());
-
- std::wstring parameters = ToWString("/DIR=\"" + qApp->applicationDirPath() + "\" ");
+ const QString parameters = "/DIR=\"" + qApp->applicationDirPath() + "\" ";
- HINSTANCE res = ::ShellExecuteW(
- nullptr, L"open", m_UpdateFile.fileName().toStdWString().c_str(), parameters.c_str(),
- nullptr, SW_SHOW);
-
- if (res > (HINSTANCE)32) {
+ if (shell::Execute(m_UpdateFile.fileName(), parameters)) {
QCoreApplication::quit();
} else {
- reportError(tr("Failed to start %1: %2")
- .arg(m_UpdateFile.fileName())
- .arg((INT_PTR)res));
+ reportError(tr("Failed to start %1").arg(m_UpdateFile.fileName()));
}
+
m_UpdateFile.remove();
}
diff --git a/src/settings.cpp b/src/settings.cpp index 231487bb..5cb2524f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -81,6 +81,7 @@ private: int m_SortRole; }; + Settings *Settings::s_Instance = nullptr; @@ -130,18 +131,19 @@ bool Settings::pluginBlacklisted(const QString &fileName) const void Settings::registerAsNXMHandler(bool force) { - std::wstring nxmPath = ToWString(QCoreApplication::applicationDirPath() + "/nxmhandler.exe"); - std::wstring executable = ToWString(QCoreApplication::applicationFilePath()); - std::wstring mode = force ? L"forcereg" : L"reg"; - std::wstring parameters = mode + L" " + m_GamePlugin->gameShortName().toStdWString(); - for (QString altGame : m_GamePlugin->validShortNames()) { - parameters += L"," + altGame.toStdWString(); + const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe"; + const auto executable = QCoreApplication::applicationFilePath(); + + QString mode = force ? "forcereg" : "reg"; + QString parameters = mode + " " + m_GamePlugin->gameShortName(); + for (const QString& altGame : m_GamePlugin->validShortNames()) { + parameters += "," + altGame; } - parameters += L" \"" + executable + L"\""; - HINSTANCE res = ::ShellExecuteW(nullptr, L"open", nxmPath.c_str(), parameters.c_str(), nullptr, SW_SHOWNORMAL); - if ((INT_PTR)res <= 32) { - QMessageBox::critical(nullptr, tr("Failed"), - tr("Sorry, failed to start the helper application")); + parameters += " \"" + executable + "\""; + + if (!shell::Execute(nxmPath, parameters)) { + QMessageBox::critical( + nullptr, tr("Failed"), tr("Failed to start the helper application")); } } @@ -216,12 +218,11 @@ QString Settings::deObfuscate(const QString key) result = QString::fromWCharArray(charData, creds->CredentialBlobSize / sizeof(wchar_t)); CredFree(creds); } else { - if (GetLastError() != ERROR_NOT_FOUND) { - wchar_t buffer[256]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - buffer, (sizeof(buffer) / sizeof(wchar_t)), NULL); - qCritical() << "Retrieving encrypted data failed:" << buffer; + const auto e = GetLastError(); + if (e != ERROR_NOT_FOUND) { + qCritical().nospace() + << "Retrieving encrypted data failed: " + << formatSystemMessageQ(e); } } delete[] keyData; @@ -362,6 +363,31 @@ bool Settings::getNexusApiKey(QString &apiKey) const return true; } +bool Settings::setNexusApiKey(const QString& apiKey) +{ + if (!obfuscate("APIKEY", apiKey)) { + const auto e = GetLastError(); + + qCritical().nospace() + << "Storing API key failed: " + << formatSystemMessageQ(e); + + return false; + } + + return true; +} + +bool Settings::clearNexusApiKey() +{ + return setNexusApiKey(""); +} + +bool Settings::hasNexusApiKey() const +{ + return !deObfuscate("APIKEY").isEmpty(); +} + bool Settings::getSteamLogin(QString &username, QString &password) const { if (m_Settings.contains("Settings/steam_username")) { @@ -451,17 +477,6 @@ QString Settings::executablesBlacklist() const ).toString(); } -void Settings::setNexusApiKey(QString apiKey) -{ - if (!obfuscate("APIKEY", apiKey)) { - wchar_t buffer[256]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - buffer, (sizeof(buffer) / sizeof(wchar_t)), NULL); - qCritical() << "Storing API key failed:" << buffer; - } -} - void Settings::setSteamLogin(QString username, QString password) { if (username == "") { @@ -471,11 +486,10 @@ void Settings::setSteamLogin(QString username, QString password) m_Settings.setValue("Settings/steam_username", username); } if (!obfuscate("steam_password", password)) { - wchar_t buffer[256]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - buffer, (sizeof(buffer) / sizeof(wchar_t)), NULL); - qCritical() << "Storing or deleting password failed:" << buffer; + const auto e = GetLastError(); + qCritical().nospace() + << "Storing or deleting password failed: " + << formatSystemMessageQ(e); } } @@ -705,43 +719,10 @@ void Settings::resetDialogs() QuestionBoxMemory::resetDialogs(); } -void Settings::processApiKey(const QString &apiKey) -{ - if (!obfuscate("APIKEY", apiKey)) { - wchar_t buffer[256]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - buffer, (sizeof(buffer) / sizeof(wchar_t)), NULL); - qCritical() << "Storing or deleting API key failed:" << buffer; - } -} - -void Settings::clearApiKey(QPushButton *nexusButton) -{ - obfuscate("APIKEY", ""); - nexusButton->setEnabled(true); - nexusButton->setText("Connect to Nexus"); -} - -void Settings::checkApiKey(QPushButton *nexusButton) -{ - if (deObfuscate("APIKEY").isEmpty()) { - nexusButton->setEnabled(true); - nexusButton->setText("Connect to Nexus"); - QMessageBox::warning(qApp->activeWindow(), tr("Error"), - tr("Failed to retrieve a Nexus API key! Please try again. " - "A browser window should open asking you to authorize.")); - } -} - void Settings::query(PluginContainer *pluginContainer, QWidget *parent) { - SettingsDialog dialog(pluginContainer, parent); - + SettingsDialog dialog(pluginContainer, this, parent); connect(&dialog, SIGNAL(resetDialogs()), this, SLOT(resetDialogs())); - connect(&dialog, SIGNAL(processApiKey(const QString &)), this, SLOT(processApiKey(const QString &))); - connect(&dialog, SIGNAL(closeApiConnection(QPushButton *)), this, SLOT(checkApiKey(QPushButton *))); - connect(&dialog, SIGNAL(revokeApiKey(QPushButton *)), this, SLOT(clearApiKey(QPushButton *))); std::vector<std::unique_ptr<SettingsTab>> tabs; @@ -1042,7 +1023,6 @@ void Settings::DiagnosticsTab::update() Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) : Settings::SettingsTab(parent, dialog) - , m_nexusConnect(dialog.findChild<QPushButton *>("nexusConnect")) , m_offlineBox(dialog.findChild<QCheckBox *>("offlineBox")) , m_proxyBox(dialog.findChild<QCheckBox *>("proxyBox")) , m_knownServersList(dialog.findChild<QListWidget *>("knownServersList")) @@ -1051,11 +1031,6 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) , m_endorsementBox(dialog.findChild<QCheckBox *>("endorsementBox")) , m_hideAPICounterBox(dialog.findChild<QCheckBox *>("hideAPICounterBox")) { - if (!deObfuscate("APIKEY").isEmpty()) { - m_nexusConnect->setText("Nexus API Key Stored"); - m_nexusConnect->setDisabled(true); - } - m_offlineBox->setChecked(parent->offlineMode()); m_proxyBox->setChecked(parent->useProxy()); m_endorsementBox->setChecked(parent->endorsementIntegration()); diff --git a/src/settings.h b/src/settings.h index ed49a1bc..bccd1e81 100644 --- a/src/settings.h +++ b/src/settings.h @@ -188,6 +188,24 @@ public: bool getNexusApiKey(QString &apiKey) const; /** + * @brief set the nexus login information + * + * @param username username + * @param password password + */ + bool setNexusApiKey(const QString& apiKey); + + /** + * @brief clears the nexus login information + */ + bool clearNexusApiKey(); + + /** + * @brief returns whether an API key is currently stored + */ + bool hasNexusApiKey() const; + + /** * @brief retrieve the login information for steam * * @param username (out) receives the user name for nexus @@ -241,14 +259,6 @@ public: QString executablesBlacklist() const; /** - * @brief set the nexus login information - * - * @param username username - * @param password password - */ - void setNexusApiKey(QString apiKey); - - /** * @brief set the steam login information * * @param username username @@ -306,6 +316,7 @@ public: * @return the wrapped QSettings object */ QSettings &directInterface() { return m_Settings; } + const QSettings &directInterface() const { return m_Settings; } /** * @brief retrieve a setting for one of the installed plugins @@ -480,7 +491,6 @@ private: void update(); private: - QPushButton *m_nexusConnect; QCheckBox *m_offlineBox; QCheckBox *m_proxyBox; QListWidget *m_knownServersList; @@ -537,9 +547,6 @@ private: private slots: void resetDialogs(); - void processApiKey(const QString &); - void clearApiKey(QPushButton *nexusButton); - void checkApiKey(QPushButton *nexusButton); signals: diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 74091469..95e4ceb0 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "settingsdialog.h" #include "ui_settingsdialog.h" +#include "ui_nexusmanualkey.h" #include "categoriesdialog.h" #include "helper.h" #include "noeditdelegate.h" @@ -27,6 +28,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "settings.h" #include "instancemanager.h" #include "nexusinterface.h" +#include "nxmaccessmanager.h" #include "plugincontainer.h" #include <boost/uuid/uuid_generators.hpp> @@ -47,9 +49,59 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase; -SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent) + +class NexusManualKeyDialog : public QDialog +{ +public: + NexusManualKeyDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::NexusManualKeyDialog) + { + ui->setupUi(this); + + connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); }); + connect(ui->paste, &QPushButton::clicked, [&]{ paste(); }); + connect(ui->clear, &QPushButton::clicked, [&]{ clear(); }); + } + + void accept() override + { + m_key = ui->key->toPlainText(); + QDialog::accept(); + } + + const QString& key() const + { + return m_key; + } + + void openBrowser() + { + shell::OpenLink(QUrl("https://www.nexusmods.com/users/myaccount?tab=api")); + } + + void paste() + { + const auto text = QApplication::clipboard()->text(); + if (!text.isEmpty()) { + ui->key->setPlainText(text); + } + } + + void clear() + { + ui->key->clear(); + } + +private: + std::unique_ptr<Ui::NexusManualKeyDialog> ui; + QString m_key; +}; + + +SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* settings, QWidget *parent) : TutorableDialog("SettingsDialog", parent) , ui(new Ui::SettingsDialog) + , m_settings(settings) , m_PluginContainer(pluginContainer) , m_nexusLogin(new QWebSocket) , m_KeyReceived(false) @@ -66,8 +118,9 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent connect(m_nexusLogin, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(authError(QAbstractSocket::SocketError))); connect(m_nexusLogin, SIGNAL(textMessageReceived(const QString &)), this, SLOT(receiveApiKey(const QString &))); connect(m_nexusLogin, SIGNAL(disconnected()), this, SLOT(completeApiConnection())); - connect(this, SIGNAL(retryApiConnection()), this, SLOT(on_nexusConnect_clicked())); m_loginTimer.callOnTimeout(this, &SettingsDialog::loginPing); + + updateNexusButtons(); } SettingsDialog::~SettingsDialog() @@ -88,7 +141,7 @@ QString SettingsDialog::getColoredButtonStyleSheet() const "}"); } -void SettingsDialog::setButtonColor(QPushButton *button, QColor &color) +void SettingsDialog::setButtonColor(QPushButton *button, const QColor &color) { button->setStyleSheet( QString("QPushButton {" @@ -338,10 +391,33 @@ void SettingsDialog::on_resetDialogsButton_clicked() void SettingsDialog::on_nexusConnect_clicked() { - ui->nexusConnect->setText("Connecting the API. Please login within the browser and accept the request. This will time out after 30 minutes."); - ui->nexusConnect->setDisabled(true); + fetchNexusApiKey(); +} + +void SettingsDialog::on_nexusManualKey_clicked() +{ + NexusManualKeyDialog dialog(this); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + const auto key = dialog.key(); + + if (key.isEmpty()) { + clearKey(); + } else { + if (setKey(key)) { + testApiKey(); + } + } +} + +void SettingsDialog::fetchNexusApiKey() +{ QUrl url = QUrl("wss://sso.nexusmods.com"); m_nexusLogin->open(url); + updateNexusButtons(); } void SettingsDialog::dispatchLogin() @@ -391,12 +467,17 @@ void SettingsDialog::receiveApiKey(const QString &response) if (data.contains("connection_token")) { m_AuthToken = data["connection_token"].toString(); } else { - m_KeyReceived = true; - emit processApiKey(data["api_key"].toString()); + const auto key = data["api_key"].toString(); + m_nexusLogin->close(); - ui->nexusConnect->setText("Nexus API Key Stored"); m_loginTimer.stop(); m_totalPings = 0; + + if (key.isEmpty()) { + clearKey(); + } else { + setKey(key); + } } } else { QString error("There was a problem with SSO initialization: %1"); @@ -407,10 +488,68 @@ void SettingsDialog::receiveApiKey(const QString &response) void SettingsDialog::completeApiConnection() { - if (m_KeyReceived == true || !m_loginTimer.isActive()) - emit closeApiConnection(ui->nexusConnect); - else - emit retryApiConnection(); + if (!m_KeyReceived && !m_loginTimer.isActive()) { + QMessageBox::warning(qApp->activeWindow(), tr("Error"), + tr("Failed to retrieve a Nexus API key! Please try again. " + "A browser window should open asking you to authorize.")); + + // try again + fetchNexusApiKey(); + } +} + +bool SettingsDialog::setKey(const QString& key) +{ + m_KeyReceived = true; + const bool ret = m_settings->setNexusApiKey(key); + updateNexusButtons(); + return ret; +} + +bool SettingsDialog::clearKey() +{ + m_KeyCleared = true; + const auto ret = m_settings->clearNexusApiKey(); + updateNexusButtons(); + + NexusInterface::instance(m_PluginContainer)->getAccessManager()->clearApiKey(); + + return ret; +} + +void SettingsDialog::testApiKey() +{ + QString key; + if (!m_settings->getNexusApiKey(key)) { + qWarning().nospace() << "can't test API key, nothing stored"; + return; + } + + NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(key, true); +} + +void SettingsDialog::updateNexusButtons() +{ + if (m_nexusLogin->state() != QAbstractSocket::UnconnectedState) { + // api key is in the process of being retrieved + ui->nexusConnect->setText("Connecting the API. Please login within the browser and accept the request. This will time out after 30 minutes."); + ui->nexusConnect->setEnabled(false); + ui->nexusDisconnect->setEnabled(false); + ui->nexusManualKey->setEnabled(false); + } + else if (m_settings->hasNexusApiKey()) { + // api key is present + ui->nexusConnect->setText("Nexus API Key Stored"); + ui->nexusConnect->setEnabled(false); + ui->nexusDisconnect->setEnabled(true); + ui->nexusManualKey->setEnabled(false); + } else { + // api key not present + ui->nexusConnect->setText("Connect to Nexus"); + ui->nexusConnect->setEnabled(true); + ui->nexusDisconnect->setEnabled(false); + ui->nexusManualKey->setEnabled(true); + } } void SettingsDialog::storeSettings(QListWidgetItem *pluginItem) @@ -480,10 +619,9 @@ void SettingsDialog::on_clearCacheButton_clicked() NexusInterface::instance(m_PluginContainer)->clearCache(); } -void SettingsDialog::on_revokeNexusAuthButton_clicked() +void SettingsDialog::on_nexusDisconnect_clicked() { - emit revokeApiKey(ui->nexusConnect); - m_KeyCleared = true; + clearKey(); } void SettingsDialog::normalizePath(QLineEdit *lineEdit) diff --git a/src/settingsdialog.h b/src/settingsdialog.h index 92a97c3f..858a36d4 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -29,6 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QTimer> class PluginContainer; +class Settings; namespace Ui { class SettingsDialog; @@ -44,7 +45,9 @@ class SettingsDialog : public MOBase::TutorableDialog Q_OBJECT public: - explicit SettingsDialog(PluginContainer *pluginContainer, QWidget *parent = 0); + explicit SettingsDialog( + PluginContainer *pluginContainer, Settings* settings, QWidget *parent = 0); + ~SettingsDialog(); /** @@ -53,7 +56,7 @@ public: */ QString getColoredButtonStyleSheet() const; - void setButtonColor(QPushButton *button, QColor &color); + void setButtonColor(QPushButton *button, const QColor &color); public slots: @@ -62,9 +65,6 @@ public slots: signals: void resetDialogs(); - void processApiKey(const QString &); - void closeApiConnection(QPushButton *); - void revokeApiKey(QPushButton *); void retryApiConnection(); private: @@ -94,103 +94,74 @@ public: private slots: - //void on_loginCheckBox_toggled(bool checked); - void on_categoriesBtn_clicked(); - void on_execBlacklistBtn_clicked(); - void on_bsaDateBtn_clicked(); - void on_browseDownloadDirBtn_clicked(); - void on_browseModDirBtn_clicked(); - void on_browseCacheDirBtn_clicked(); - void on_resetDialogsButton_clicked(); - void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - - void deleteBlacklistItem(); - void on_associateButton_clicked(); - void on_clearCacheButton_clicked(); - - void on_revokeNexusAuthButton_clicked(); - + void on_nexusDisconnect_clicked(); void on_browseBaseDirBtn_clicked(); - void on_browseOverwriteDirBtn_clicked(); - void on_browseProfilesDirBtn_clicked(); - void on_browseGameDirBtn_clicked(); - void on_overwritingBtn_clicked(); - void on_overwrittenBtn_clicked(); - void on_overwritingArchiveBtn_clicked(); - void on_overwrittenArchiveBtn_clicked(); - void on_containsBtn_clicked(); - void on_containedBtn_clicked(); - void on_resetColorsBtn_clicked(); - void on_baseDirEdit_editingFinished(); - void on_downloadDirEdit_editingFinished(); - void on_modDirEdit_editingFinished(); - void on_cacheDirEdit_editingFinished(); - void on_profilesDirEdit_editingFinished(); - void on_overwriteDirEdit_editingFinished(); - void on_nexusConnect_clicked(); + void on_nexusManualKey_clicked(); + void on_resetGeometryBtn_clicked(); + void deleteBlacklistItem(); void dispatchLogin(); - void loginPing(); - void authError(QAbstractSocket::SocketError error); - void receiveApiKey(const QString &apiKey); - void completeApiConnection(); - void on_resetGeometryBtn_clicked(); - private: - Ui::SettingsDialog *ui; - PluginContainer *m_PluginContainer; + Ui::SettingsDialog *ui; + Settings* m_settings; + PluginContainer *m_PluginContainer; - QColor m_OverwritingColor; - QColor m_OverwrittenColor; - QColor m_OverwritingArchiveColor; - QColor m_OverwrittenArchiveColor; - QColor m_ContainsColor; - QColor m_ContainedColor; + QColor m_OverwritingColor; + QColor m_OverwrittenColor; + QColor m_OverwritingArchiveColor; + QColor m_OverwrittenArchiveColor; + QColor m_ContainsColor; + QColor m_ContainedColor; - bool m_KeyReceived; - bool m_KeyCleared; - bool m_GeometriesReset; - QString m_UUID; - QString m_AuthToken; + bool m_KeyReceived; + bool m_KeyCleared; + bool m_GeometriesReset; + QString m_UUID; + QString m_AuthToken; - QString m_ExecutableBlacklist; - QWebSocket *m_nexusLogin; - QTimer m_loginTimer; - int m_totalPings = 0; -}; + QString m_ExecutableBlacklist; + QWebSocket *m_nexusLogin; + QTimer m_loginTimer; + int m_totalPings = 0; + bool setKey(const QString& key); + bool clearKey(); + void updateNexusButtons(); + void fetchNexusApiKey(); + void testApiKey(); +}; #endif // SETTINGSDIALOG_H diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index fa8893b9..faaf1653 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -441,7 +441,7 @@ If you use pre-releases, never contact me directly by e-mail or via private mess <item> <widget class="QLabel" name="label_23"> <property name="text"> - <string>Important: All directories have to be writeable!</string> + <string>Important: All directories have to be writable!</string> </property> </widget> </item> @@ -495,12 +495,28 @@ p, li { white-space: pre-wrap; } </spacer> </item> <item> - <widget class="QPushButton" name="revokeNexusAuthButton"> + <widget class="QPushButton" name="nexusManualKey"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Manually enter the API key and try to login</string> + </property> + <property name="text"> + <string>Enter API Key Manually</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="nexusDisconnect"> <property name="toolTip"> <string>Clear the stored Nexus API key and force reauthorization.</string> </property> <property name="text"> - <string>Revoke Nexus Authorization</string> + <string>Disconnect from Nexus</string> </property> <property name="icon"> <iconset resource="resources.qrc"> @@ -1281,7 +1297,7 @@ programs you are intentionally running.</string> <property name="whatsThis"> <string> Decides the amount of data printed to "ModOrganizer.log". - "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. + "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. </string> </property> <item> diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp index 1179110a..bde515a9 100644 --- a/src/shared/directoryentry.cpp +++ b/src/shared/directoryentry.cpp @@ -220,6 +220,11 @@ std::vector<FileEntry::Ptr> FilesOrigin::getFiles() const return result;
}
+FileEntry::Ptr FilesOrigin::findFile(FileEntry::Index index) const
+{
+ return m_FileRegister.lock()->getFile(index);
+}
+
bool FilesOrigin::containsArchive(std::wstring archiveName)
{
for (FileEntry::Index fileIdx : m_Files)
diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h index 12cef11d..785c3ff6 100644 --- a/src/shared/directoryentry.h +++ b/src/shared/directoryentry.h @@ -49,8 +49,8 @@ class FileEntry { public:
typedef unsigned int Index;
-
typedef boost::shared_ptr<FileEntry> Ptr;
+ typedef std::vector<std::pair<int, std::pair<std::wstring, int>>> AlternativesVector;
public:
@@ -72,7 +72,7 @@ public: // gets the list of alternative origins (origins with lower priority than the primary one).
// if sortOrigins has been called, it is sorted by priority (ascending)
- const std::vector<std::pair<int, std::pair<std::wstring, int>>> &getAlternatives() const { return m_Alternatives; }
+ const AlternativesVector &getAlternatives() const { return m_Alternatives; }
const std::wstring &getName() const { return m_Name; }
int getOrigin() const { return m_Origin; }
@@ -98,7 +98,7 @@ private: std::wstring m_Name;
int m_Origin = -1;
std::pair<std::wstring, int> m_Archive;
- std::vector<std::pair<int, std::pair<std::wstring, int>>> m_Alternatives;
+ AlternativesVector m_Alternatives;
DirectoryEntry *m_Parent;
mutable FILETIME m_FileTime;
@@ -135,6 +135,7 @@ public: const std::wstring &getPath() const { return m_Path; }
std::vector<FileEntry::Ptr> getFiles() const;
+ FileEntry::Ptr findFile(FileEntry::Index index) const;
void enable(bool enabled, time_t notAfter = LONG_MAX);
bool isDisabled() const { return m_Disabled; }
diff --git a/src/shared/util.cpp b/src/shared/util.cpp index ed7c434e..17df3b92 100644 --- a/src/shared/util.cpp +++ b/src/shared/util.cpp @@ -20,15 +20,31 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "util.h"
#include "windows_error.h"
#include "error_report.h"
+#include "executableslist.h"
+#include "instancemanager.h"
+#include <utility.h>
#include <sstream>
#include <locale>
#include <algorithm>
-#include <DbgHelp.h>
#include <set>
+#include <filesystem>
+
+#include <DbgHelp.h>
#include <boost/scoped_array.hpp>
#include <QApplication>
+#include <comdef.h>
+#include <Wbemidl.h>
+#include <wscapi.h>
+#include <netfw.h>
+
+#pragma comment(lib, "Wbemuuid.lib")
+
+using MOBase::formatSystemMessage;
+using MOBase::formatSystemMessageQ;
+namespace fs = std::filesystem;
+
namespace MOShared {
@@ -253,4 +269,1807 @@ MOBase::VersionInfo createVersionInfo() }
+namespace env
+{
+
+struct HandleCloser
+{
+ using pointer = HANDLE;
+
+ void operator()(HANDLE h)
+ {
+ if (h != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(h);
+ }
+ }
+};
+
+using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>;
+
+
+struct LibraryFreer
+{
+ using pointer = HINSTANCE;
+
+ void operator()(HINSTANCE h)
+ {
+ if (h != 0) {
+ ::FreeLibrary(h);
+ }
+ }
+};
+
+struct COMReleaser
+{
+ void operator()(IUnknown* p)
+ {
+ if (p) {
+ p->Release();
+ }
+ }
+};
+
+
+template <class T>
+using COMPtr = std::unique_ptr<T, COMReleaser>;
+
+
+class ShellLinkException
+{
+public:
+ ShellLinkException(QString s)
+ : m_what(std::move(s))
+ {
+ }
+
+ const QString& what() const
+ {
+ return m_what;
+ }
+
+private:
+ QString m_what;
+};
+
+// just a wrapper around IShellLink operations that throws ShellLinkException
+// on errors
+//
+class ShellLinkWrapper
+{
+public:
+ ShellLinkWrapper()
+ {
+ m_link = createShellLink();
+ m_file = createPersistFile();
+ }
+
+ void setPath(const QString& s)
+ {
+ if (s.isEmpty()) {
+ throw ShellLinkException("path cannot be empty");
+ }
+
+ const auto r = m_link->SetPath(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set target path '%1'").arg(s));
+ }
+
+ void setArguments(const QString& s)
+ {
+ const auto r = m_link->SetArguments(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set arguments '%1'").arg(s));
+ }
+
+ void setDescription(const QString& s)
+ {
+ if (s.isEmpty()) {
+ return;
+ }
+
+ const auto r = m_link->SetDescription(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set description '%1'").arg(s));
+ }
+
+ void setIcon(const QString& file, int i)
+ {
+ if (file.isEmpty()) {
+ return;
+ }
+
+ const auto r = m_link->SetIconLocation(file.toStdWString().c_str(), i);
+ throwOnFail(r, QString("failed to set icon '%1' @ %2").arg(file).arg(i));
+ }
+
+ void setWorkingDirectory(const QString& s)
+ {
+ if (s.isEmpty()) {
+ return;
+ }
+
+ const auto r = m_link->SetWorkingDirectory(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set working directory '%1'").arg(s));
+ }
+
+ void save(const QString& path)
+ {
+ const auto r = m_file->Save(path.toStdWString().c_str(), TRUE);
+ throwOnFail(r, QString("failed to save link '%1'").arg(path));
+ }
+
+private:
+ COMPtr<IShellLink> m_link;
+ COMPtr<IPersistFile> m_file;
+
+ void throwOnFail(HRESULT r, const QString& s)
+ {
+ if (FAILED(r)) {
+ throw ShellLinkException(QString("%1, %2")
+ .arg(s)
+ .arg(formatSystemMessageQ(r)));
+ }
+ }
+
+ COMPtr<IShellLink> createShellLink()
+ {
+ void* link = nullptr;
+
+ const auto r = CoCreateInstance(
+ CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLink, &link);
+
+ throwOnFail(r, "failed to create IShellLink instance");
+
+ if (!link) {
+ throw ShellLinkException("creating IShellLink worked, pointer is null");
+ }
+
+ return COMPtr<IShellLink>(static_cast<IShellLink*>(link));
+ }
+
+ COMPtr<IPersistFile> createPersistFile()
+ {
+ void* file = nullptr;
+
+ const auto r = m_link->QueryInterface(IID_IPersistFile, &file);
+ throwOnFail(r, "failed to get IPersistFile interface");
+
+ if (!file) {
+ throw ShellLinkException("querying IPersistFile worked, pointer is null");
+ }
+
+ return COMPtr<IPersistFile>(static_cast<IPersistFile*>(file));
+ }
+};
+
+
+Shortcut::Shortcut()
+ : m_iconIndex(0)
+{
+}
+
+Shortcut::Shortcut(const Executable& exe)
+ : Shortcut()
+{
+ m_name = exe.title();
+ m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath();
+
+ m_arguments = QString("\"moshortcut://%1:%2\"")
+ .arg(InstanceManager::instance().currentInstance())
+ .arg(exe.title());
+
+ m_description = QString("Run %1 with ModOrganizer").arg(exe.title());
+
+ if (exe.usesOwnIcon()) {
+ m_icon = exe.binaryInfo().absoluteFilePath();
+ }
+
+ m_workingDirectory = qApp->applicationDirPath();
+}
+
+Shortcut& Shortcut::name(const QString& s)
+{
+ m_name = s;
+ return *this;
+}
+
+Shortcut& Shortcut::target(const QString& s)
+{
+ m_target = s;
+ return *this;
+}
+
+Shortcut& Shortcut::arguments(const QString& s)
+{
+ m_arguments = s;
+ return *this;
+}
+
+Shortcut& Shortcut::description(const QString& s)
+{
+ m_description = s;
+ return *this;
+}
+
+Shortcut& Shortcut::icon(const QString& s, int index)
+{
+ m_icon = s;
+ m_iconIndex = index;
+ return *this;
+}
+
+Shortcut& Shortcut::workingDirectory(const QString& s)
+{
+ m_workingDirectory = s;
+ return *this;
+}
+
+bool Shortcut::exists(Locations loc) const
+{
+ const auto path = shortcutPath(loc);
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ return QFileInfo(path).exists();
+}
+
+bool Shortcut::toggle(Locations loc)
+{
+ if (exists(loc)) {
+ return remove(loc);
+ } else {
+ return add(loc);
+ }
+}
+
+bool Shortcut::add(Locations loc)
+{
+ debug()
+ << "adding shortcut to " << toString(loc) << ":\n"
+ << " . name: '" << m_name << "'\n"
+ << " . target: '" << m_target << "'\n"
+ << " . arguments: '" << m_arguments << "'\n"
+ << " . description: '" << m_description << "'\n"
+ << " . icon: '" << m_icon << "' @ " << m_iconIndex << "\n"
+ << " . working directory: '" << m_workingDirectory << "'";
+
+ if (m_target.isEmpty()) {
+ critical() << "target is empty";
+ return false;
+ }
+
+ const auto path = shortcutPath(loc);
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ debug() << "shorcut file will be saved at '" << path << "'";
+
+ try
+ {
+ ShellLinkWrapper link;
+
+ link.setPath(m_target);
+ link.setArguments(m_arguments);
+ link.setDescription(m_description);
+ link.setIcon(m_icon, m_iconIndex);
+ link.setWorkingDirectory(m_workingDirectory);
+
+ link.save(path);
+
+ return true;
+ }
+ catch(ShellLinkException& e)
+ {
+ critical() << e.what() << "\nshortcut file was not saved";
+ }
+
+ return false;
+}
+
+bool Shortcut::remove(Locations loc)
+{
+ debug() << "removing shortcut for '" << m_name << "' from " << toString(loc);
+
+ const auto path = shortcutPath(loc);
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ debug() << "path to shortcut file is '" << path << "'";
+
+ if (!QFile::exists(path)) {
+ critical() << "can't remove '" << path << "', file not found";
+ return false;
+ }
+
+ if (!MOBase::shellDelete({path})) {
+ const auto e = ::GetLastError();
+
+ critical()
+ << "failed to remove '" << path << "', "
+ << formatSystemMessageQ(e);
+
+ return false;
+ }
+
+ return true;
+}
+
+QString Shortcut::shortcutPath(Locations loc) const
+{
+ const auto dir = shortcutDirectory(loc);
+ if (dir.isEmpty()) {
+ return {};
+ }
+
+ const auto file = shortcutFilename();
+ if (file.isEmpty()) {
+ return {};
+ }
+
+ return dir + QDir::separator() + file;
+}
+
+QString Shortcut::shortcutDirectory(Locations loc) const
+{
+ QString dir;
+
+ try
+ {
+ switch (loc)
+ {
+ case Desktop:
+ dir = MOBase::getDesktopDirectory();
+ break;
+
+ case StartMenu:
+ dir = MOBase::getStartMenuDirectory();
+ break;
+
+ case None:
+ default:
+ critical() << "bad location " << loc;
+ break;
+ }
+ }
+ catch(std::exception&)
+ {
+ }
+
+ return QDir::toNativeSeparators(dir);
+}
+
+QString Shortcut::shortcutFilename() const
+{
+ if (m_name.isEmpty()) {
+ critical() << "name is empty";
+ return {};
+ }
+
+ return m_name + ".lnk";
+}
+
+QDebug Shortcut::debug() const
+{
+ return qDebug().noquote().nospace() << "system shortcut: ";
+}
+
+QDebug Shortcut::critical() const
+{
+ return qCritical().noquote().nospace() << "system shortcut: ";
+}
+
+
+QString toString(Shortcut::Locations loc)
+{
+ switch (loc)
+ {
+ case Shortcut::None:
+ return "none";
+
+ case Shortcut::Desktop:
+ return "desktop";
+
+ case Shortcut::StartMenu:
+ return "start menu";
+
+ default:
+ return QString("? (%1)").arg(static_cast<int>(loc));
+ }
+}
+
+
+
+class WMI
+{
+public:
+ class failed {};
+
+ WMI(const std::string& ns)
+ {
+ try
+ {
+ createLocator();
+ createService(ns);
+ setSecurity();
+ }
+ catch(failed&)
+ {
+ }
+ }
+
+ template <class F>
+ void query(const std::string& q, F&& f)
+ {
+ if (!m_locator || !m_service) {
+ return;
+ }
+
+ auto enumerator = getEnumerator(q);
+ if (!enumerator) {
+ return;
+ }
+
+ for (;;)
+ {
+ COMPtr<IWbemClassObject> object;
+
+ {
+ IWbemClassObject* rawObject = nullptr;
+ ULONG count = 0;
+ auto ret = enumerator->Next(WBEM_INFINITE, 1, &rawObject, &count);
+
+ if (count == 0 || !rawObject) {
+ break;
+ }
+
+ if (FAILED(ret)) {
+ qCritical()
+ << "enumerator->next() failed, " << formatSystemMessageQ(ret);
+ break;
+ }
+
+ object.reset(rawObject);
+ }
+
+ f(object.get());
+ }
+ }
+
+private:
+ COMPtr<IWbemLocator> m_locator;
+ COMPtr<IWbemServices> m_service;
+
+ void createLocator()
+ {
+ void* rawLocator = nullptr;
+
+ const auto ret = CoCreateInstance(
+ CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IWbemLocator, &rawLocator);
+
+ if (FAILED(ret) || !rawLocator) {
+ qCritical()
+ << "CoCreateInstance for WbemLocator failed, "
+ << formatSystemMessageQ(ret);
+
+ throw failed();
+ }
+
+ m_locator.reset(static_cast<IWbemLocator*>(rawLocator));
+ }
+
+ void createService(const std::string& ns)
+ {
+ IWbemServices* rawService = nullptr;
+
+ const auto res = m_locator->ConnectServer(
+ _bstr_t(ns.c_str()),
+ nullptr, nullptr, nullptr, 0, nullptr, nullptr,
+ &rawService);
+
+ if (FAILED(res) || !rawService) {
+ qCritical()
+ << "locator->ConnectServer() failed for namespace "
+ << "'" << QString::fromStdString(ns) << "', "
+ << formatSystemMessageQ(res);
+
+ throw failed();
+ }
+
+ m_service.reset(rawService);
+ }
+
+ void setSecurity()
+ {
+ auto ret = CoSetProxyBlanket(
+ m_service.get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
+ RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE);
+
+ if (FAILED(ret))
+ {
+ qCritical()
+ << "CoSetProxyBlanket() failed, " << formatSystemMessageQ(ret);
+
+ throw failed();
+ }
+ }
+
+ COMPtr<IEnumWbemClassObject> getEnumerator(
+ const std::string& query)
+ {
+ IEnumWbemClassObject* rawEnumerator = NULL;
+
+ auto ret = m_service->ExecQuery(
+ bstr_t("WQL"),
+ bstr_t(query.c_str()),
+ WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
+ NULL,
+ &rawEnumerator);
+
+ if (FAILED(ret) || !rawEnumerator)
+ {
+ qCritical()
+ << "query '" << QString::fromStdString(query) << "' failed, "
+ << formatSystemMessageQ(ret);
+
+ return {};
+ }
+
+ return COMPtr<IEnumWbemClassObject>(rawEnumerator);
+ }
+};
+
+
+Environment::Environment()
+{
+ m_modules = getLoadedModules();
+ m_security = getSecurityProducts();
+}
+
+const std::vector<Module>& Environment::loadedModules()
+{
+ return m_modules;
+}
+
+const WindowsInfo& Environment::windowsInfo() const
+{
+ return m_windows;
+}
+
+const std::vector<SecurityProduct>& Environment::securityProducts() const
+{
+ return m_security;
+}
+
+std::vector<Module> Environment::getLoadedModules() const
+{
+ HandlePtr snapshot(CreateToolhelp32Snapshot(
+ TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId()));
+
+ if (snapshot.get() == INVALID_HANDLE_VALUE)
+ {
+ const auto e = GetLastError();
+
+ qCritical().nospace().noquote()
+ << "CreateToolhelp32Snapshot() failed, "
+ << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ MODULEENTRY32 me = {};
+ me.dwSize = sizeof(me);
+
+ // first module, this shouldn't fail because there's at least the executable
+ if (!Module32First(snapshot.get(), &me))
+ {
+ const auto e = GetLastError();
+
+ qCritical().nospace().noquote()
+ << "Module32First() failed, " << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ std::vector<Module> v;
+
+ for (;;)
+ {
+ const auto path = QString::fromWCharArray(me.szExePath);
+ if (!path.isEmpty()) {
+ v.push_back(Module(path, me.modBaseSize));
+ }
+
+ // next module
+ if (!Module32Next(snapshot.get(), &me)) {
+ const auto e = GetLastError();
+
+ // no more modules is not an error
+ if (e != ERROR_NO_MORE_FILES) {
+ qCritical().nospace().noquote()
+ << "Module32Next() failed, " << formatSystemMessageQ(e);
+ }
+
+ break;
+ }
+ }
+
+ // sorting by display name
+ std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) {
+ return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0);
+ });
+
+ return v;
+}
+
+std::vector<SecurityProduct> Environment::getSecurityProducts() const
+{
+ std::vector<SecurityProduct> v;
+
+ {
+ auto fromWMI = getSecurityProductsFromWMI();
+ v.insert(
+ v.end(),
+ std::make_move_iterator(fromWMI.begin()),
+ std::make_move_iterator(fromWMI.end()));
+ }
+
+ if (auto p=getWindowsFirewall()) {
+ v.push_back(std::move(*p));
+ }
+
+ return v;
+}
+
+std::vector<SecurityProduct> Environment::getSecurityProductsFromWMI() const
+{
+ // some products may be present in multiple queries, such as a product marked
+ // as both antivirus and antispyware, but they'll have the same GUID, so use
+ // that to avoid duplicating entries
+ std::map<QUuid, SecurityProduct> map;
+
+ auto handleProduct = [&](auto* o) {
+ VARIANT prop;
+
+ // display name
+ auto ret = o->Get(L"displayName", 0, &prop, 0, 0);
+ if (FAILED(ret)) {
+ qCritical()
+ << "failed to get displayName, "
+ << formatSystemMessageQ(ret);
+
+ return;
+ }
+
+ if (prop.vt != VT_BSTR) {
+ qCritical() << "displayName is a " << prop.vt << ", not a bstr";
+ return;
+ }
+
+ const std::wstring name = prop.bstrVal;
+ VariantClear(&prop);
+
+ // product state
+ ret = o->Get(L"productState", 0, &prop, 0, 0);
+ if (FAILED(ret)) {
+ qCritical()
+ << "failed to get productState, "
+ << formatSystemMessageQ(ret);
+
+ return;
+ }
+
+ if (prop.vt != VT_UI4 && prop.vt != VT_I4) {
+ qCritical() << "productState is a " << prop.vt << ", is not a VT_UI4";
+ return;
+ }
+
+ DWORD state = 0;
+ if (prop.vt == VT_I4) {
+ state = prop.lVal;
+ } else {
+ state = prop.ulVal;
+ }
+
+ VariantClear(&prop);
+
+ // guid
+ ret = o->Get(L"instanceGuid", 0, &prop, 0, 0);
+ if (FAILED(ret)) {
+ qCritical()
+ << "failed to get instanceGuid, "
+ << formatSystemMessageQ(ret);
+
+ return;
+ }
+
+ if (prop.vt != VT_BSTR) {
+ qCritical() << "instanceGuid is a " << prop.vt << ", is not a bstr";
+ return;
+ }
+
+ const QUuid guid(QString::fromWCharArray(prop.bstrVal));
+ VariantClear(&prop);
+
+ const auto provider = static_cast<int>((state >> 16) & 0xff);
+ const auto scanner = (state >> 8) & 0xff;
+ const auto definitions = state & 0xff;
+
+ const bool active = ((scanner & 0x10) != 0);
+ const bool upToDate = (definitions == 0);
+
+ map.insert({
+ guid,
+ {QString::fromStdWString(name), provider, active, upToDate}});
+ };
+
+ {
+ WMI wmi("root\\SecurityCenter2");
+ wmi.query("select * from AntivirusProduct", handleProduct);
+ wmi.query("select * from FirewallProduct", handleProduct);
+ wmi.query("select * from AntiSpywareProduct", handleProduct);
+ }
+
+ {
+ WMI wmi("root\\SecurityCenter");
+ wmi.query("select * from AntivirusProduct", handleProduct);
+ wmi.query("select * from FirewallProduct", handleProduct);
+ wmi.query("select * from AntiSpywareProduct", handleProduct);
+ }
+
+ std::vector<SecurityProduct> v;
+
+ for (auto&& p : map) {
+ v.push_back(p.second);
+ }
+
+ return v;
+}
+
+std::optional<SecurityProduct> Environment::getWindowsFirewall() const
+{
+ HRESULT hr = 0;
+
+ COMPtr<INetFwPolicy2> policy;
+
+ {
+ void* rawPolicy = nullptr;
+
+ hr = CoCreateInstance(
+ __uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER,
+ __uuidof(INetFwPolicy2), &rawPolicy);
+
+ if (FAILED(hr) || !rawPolicy) {
+ qCritical()
+ << "CoCreateInstance for NetFwPolicy2 failed, "
+ << formatSystemMessageQ(hr);
+
+ return {};
+ }
+
+ policy.reset(static_cast<INetFwPolicy2*>(rawPolicy));
+ }
+
+ VARIANT_BOOL enabledVariant;
+
+ if (policy) {
+ hr = policy->get_FirewallEnabled(NET_FW_PROFILE2_PUBLIC, &enabledVariant);
+ if (FAILED(hr))
+ {
+ qCritical()
+ << "get_FirewallEnabled failed, "
+ << formatSystemMessageQ(hr);
+
+ return {};
+ }
+ }
+
+ const auto enabled = (enabledVariant != VARIANT_FALSE);
+ if (!enabled) {
+ return {};
+ }
+
+ return SecurityProduct(
+ "Windows Firewall", WSC_SECURITY_PROVIDER_FIREWALL, true, true);
+}
+
+
+Module::Module(QString path, std::size_t fileSize)
+ : m_path(std::move(path)), m_fileSize(fileSize)
+{
+ const auto fi = getFileInfo();
+
+ m_version = getVersion(fi.ffi);
+ m_timestamp = getTimestamp(fi.ffi);
+ m_versionString = fi.fileDescription;
+ m_md5 = getMD5();
+}
+
+const QString& Module::path() const
+{
+ return m_path;
+}
+
+QString Module::displayPath() const
+{
+ return QDir::fromNativeSeparators(m_path.toLower());
+}
+
+std::size_t Module::fileSize() const
+{
+ return m_fileSize;
+}
+
+const QString& Module::version() const
+{
+ return m_version;
+}
+
+const QString& Module::versionString() const
+{
+ return m_versionString;
+}
+
+const QDateTime& Module::timestamp() const
+{
+ return m_timestamp;
+}
+
+const QString& Module::md5() const
+{
+ return m_md5;
+}
+
+QString Module::timestampString() const
+{
+ if (!m_timestamp.isValid()) {
+ return "(no timestamp)";
+ }
+
+ return m_timestamp.toString(Qt::DateFormat::ISODate);
+}
+
+QString Module::toString() const
+{
+ QStringList sl;
+
+ // file size
+ sl.push_back(displayPath());
+ sl.push_back(QString("%1 B").arg(m_fileSize));
+
+ // version
+ if (m_version.isEmpty() && m_versionString.isEmpty()) {
+ sl.push_back("(no version)");
+ } else {
+ if (!m_version.isEmpty()) {
+ sl.push_back(m_version);
+ }
+
+ if (!m_versionString.isEmpty() && m_versionString != m_version) {
+ sl.push_back(versionString());
+ }
+ }
+
+ // timestamp
+ if (m_timestamp.isValid()) {
+ sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate));
+ } else {
+ sl.push_back("(no timestamp)");
+ }
+
+ // md5
+ if (!m_md5.isEmpty()) {
+ sl.push_back(m_md5);
+ }
+
+ return sl.join(", ");
+}
+
+Module::FileInfo Module::getFileInfo() const
+{
+ const auto wspath = m_path.toStdWString();
+
+ // getting version info size
+ DWORD dummy = 0;
+ const DWORD size = GetFileVersionInfoSizeW(wspath.c_str(), &dummy);
+
+ if (size == 0) {
+ const auto e = GetLastError();
+
+ if (e == ERROR_RESOURCE_TYPE_NOT_FOUND) {
+ // not an error, no version information built into that module
+ return {};
+ }
+
+ qCritical().nospace().noquote()
+ << "GetFileVersionInfoSizeW() failed on '" << m_path << "', "
+ << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ // getting version info
+ auto buffer = std::make_unique<std::byte[]>(size);
+
+ if (!GetFileVersionInfoW(wspath.c_str(), 0, size, buffer.get())) {
+ const auto e = GetLastError();
+
+ qCritical().nospace().noquote()
+ << "GetFileVersionInfoW() failed on '" << m_path << "', "
+ << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ // the version info has two major parts: a fixed version and a localizable
+ // set of strings
+
+ FileInfo fi;
+ fi.ffi = getFixedFileInfo(buffer.get());
+ fi.fileDescription = getFileDescription(buffer.get());
+
+ return fi;
+}
+
+VS_FIXEDFILEINFO Module::getFixedFileInfo(std::byte* buffer) const
+{
+ void* valuePointer = nullptr;
+ unsigned int valueSize = 0;
+
+ // the fixed version info is in the root
+ const auto ret = VerQueryValueW(buffer, L"\\", &valuePointer, &valueSize);
+
+ if (!ret || !valuePointer || valueSize == 0) {
+ // not an error, no fixed file info
+ return {};
+ }
+
+ const auto* fi = static_cast<VS_FIXEDFILEINFO*>(valuePointer);
+
+ // signature is always 0xfeef04bd
+ if (fi->dwSignature != 0xfeef04bd) {
+ qCritical().nospace().noquote()
+ << "bad file info signature 0x" << hex << fi->dwSignature << " for "
+ << "'" << m_path << "'";
+
+ return {};
+ }
+
+ return *fi;
+}
+
+QString Module::getFileDescription(std::byte* buffer) const
+{
+ struct LANGANDCODEPAGE
+ {
+ WORD wLanguage;
+ WORD wCodePage;
+ };
+
+ void* valuePointer = nullptr;
+ unsigned int valueSize = 0;
+
+ // getting list of available languages
+ auto ret = VerQueryValueW(
+ buffer, L"\\VarFileInfo\\Translation", &valuePointer, &valueSize);
+
+ if (!ret || !valuePointer || valueSize == 0) {
+ qCritical().nospace().noquote()
+ << "VerQueryValueW() for translations failed on '" << m_path << "'";
+
+ return {};
+ }
+
+ // number of languages
+ const auto count = valueSize / sizeof(LANGANDCODEPAGE);
+ if (count == 0) {
+ return {};
+ }
+
+ // using the first language in the list to get FileVersion
+ const auto* lcp = static_cast<LANGANDCODEPAGE*>(valuePointer);
+
+ const auto subBlock = QString("\\StringFileInfo\\%1%2\\FileVersion")
+ .arg(lcp->wLanguage, 4, 16, QChar('0'))
+ .arg(lcp->wCodePage, 4, 16, QChar('0'));
+
+ ret = VerQueryValueW(
+ buffer, subBlock.toStdWString().c_str(), &valuePointer, &valueSize);
+
+ if (!ret || !valuePointer || valueSize == 0) {
+ // not an error, no file version
+ return {};
+ }
+
+ // valueSize includes the null terminator
+ return QString::fromWCharArray(
+ static_cast<wchar_t*>(valuePointer), valueSize - 1);
+}
+
+QString Module::getVersion(const VS_FIXEDFILEINFO& fi) const
+{
+ if (fi.dwSignature == 0) {
+ return {};
+ }
+
+ const DWORD major = (fi.dwFileVersionMS >> 16 ) & 0xffff;
+ const DWORD minor = (fi.dwFileVersionMS >> 0 ) & 0xffff;
+ const DWORD maintenance = (fi.dwFileVersionLS >> 16 ) & 0xffff;
+ const DWORD build = (fi.dwFileVersionLS >> 0 ) & 0xffff;
+
+ if (major == 0 && minor == 0 && maintenance == 0 && build == 0) {
+ return {};
+ }
+
+ return QString("%1.%2.%3.%4")
+ .arg(major).arg(minor).arg(maintenance).arg(build);
+}
+
+QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
+{
+ FILETIME ft = {};
+
+ if (fi.dwSignature == 0 || (fi.dwFileDateMS == 0 && fi.dwFileDateLS == 0)) {
+ // if the file info is invalid or doesn't have a date, use the creation
+ // time on the file
+
+ // opening the file
+ HandlePtr h(CreateFileW(
+ m_path.toStdWString().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0));
+
+ if (h.get() == INVALID_HANDLE_VALUE) {
+ const auto e = GetLastError();
+
+ qCritical().nospace().noquote()
+ << "can't open file '" << m_path << "' for timestamp, "
+ << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ // getting the file time
+ if (!GetFileTime(h.get(), &ft, nullptr, nullptr)) {
+ const auto e = GetLastError();
+ qCritical().nospace().noquote()
+ << "can't get file time for '" << m_path << "', "
+ << formatSystemMessageQ(e);
+
+ return {};
+ }
+ } else {
+ // use the time from the file info
+ ft.dwHighDateTime = fi.dwFileDateMS;
+ ft.dwLowDateTime = fi.dwFileDateLS;
+ }
+
+
+ // converting to SYSTEMTIME
+ SYSTEMTIME utc = {};
+
+ if (!FileTimeToSystemTime(&ft, &utc)) {
+ qCritical().nospace().noquote()
+ << "FileTimeToSystemTime() failed on timestamp "
+ << "high=0x" << hex << ft.dwHighDateTime << " "
+ << "low=0x" << hex << ft.dwLowDateTime << " for "
+ << "'" << m_path << "'";
+
+ return {};
+ }
+
+ return QDateTime(
+ QDate(utc.wYear, utc.wMonth, utc.wDay),
+ QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds));
+}
+
+QString Module::getMD5() const
+{
+ if (m_path.contains("\\windows\\", Qt::CaseInsensitive)) {
+ // don't calculate md5 for system files, it's not really relevant and
+ // it takes a while
+ return {};
+ }
+
+ // opening the file
+ QFile f(m_path);
+
+ if (!f.open(QFile::ReadOnly)) {
+ qCritical().nospace().noquote()
+ << "failed to open file '" << m_path << "' for md5";
+
+ return {};
+ }
+
+ // hashing
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ if (!hash.addData(&f)) {
+ qCritical().nospace().noquote()
+ << "failed to calculate md5 for '" << m_path << "'";
+
+ return {};
+ }
+
+ return hash.result().toHex();
+}
+
+
+WindowsInfo::WindowsInfo()
+{
+ // loading ntdll.dll, the functions will be found with GetProcAddress()
+ std::unique_ptr<HINSTANCE, LibraryFreer> ntdll(LoadLibraryW(L"ntdll.dll"));
+
+ if (!ntdll) {
+ qCritical() << "failed to load ntdll.dll while getting version";
+ return;
+ } else {
+ m_reported = getReportedVersion(ntdll.get());
+ m_real = getRealVersion(ntdll.get());
+ }
+
+ m_release = getRelease();
+ m_elevated = getElevated();
+}
+
+bool WindowsInfo::compatibilityMode() const
+{
+ if (m_real == Version()) {
+ // don't know the real version, can't guess compatibility mode
+ return false;
+ }
+
+ return (m_real != m_reported);
+}
+
+const WindowsInfo::Version& WindowsInfo::reportedVersion() const
+{
+ return m_reported;
+}
+
+const WindowsInfo::Version& WindowsInfo::realVersion() const
+{
+ return m_real;
+}
+
+const WindowsInfo::Release& WindowsInfo::release() const
+{
+ return m_release;
+}
+
+std::optional<bool> WindowsInfo::isElevated() const
+{
+ return m_elevated;
+}
+
+QString WindowsInfo::toString() const
+{
+ QStringList sl;
+
+ const QString reported = m_reported.toString();
+ const QString real = m_real.toString();
+
+ // version
+ sl.push_back("version: " + reported);
+
+ // real version if different
+ if (compatibilityMode()) {
+ sl.push_back("real version: " + real);
+ }
+
+ // build.UBR, such as 17763.557
+ if (m_release.UBR != 0) {
+ DWORD build = 0;
+
+ if (compatibilityMode()) {
+ build = m_real.build;
+ } else {
+ build = m_reported.build;
+ }
+
+ sl.push_back(QString("%1.%2").arg(build).arg(m_release.UBR));
+ }
+
+ // release ID
+ if (!m_release.ID.isEmpty()) {
+ sl.push_back("release " + m_release.ID);
+ }
+
+ // buildlab string
+ if (!m_release.buildLab.isEmpty()) {
+ sl.push_back(m_release.buildLab);
+ }
+
+ // product name
+ if (!m_release.productName.isEmpty()) {
+ sl.push_back(m_release.productName);
+ }
+
+ // elevated
+ QString elevated = "?";
+ if (m_elevated.has_value()) {
+ elevated = (*m_elevated ? "yes" : "no");
+ }
+
+ sl.push_back("elevated: " + elevated);
+
+ return sl.join(", ");
+}
+
+WindowsInfo::Version WindowsInfo::getReportedVersion(HINSTANCE ntdll) const
+{
+ // windows has been deprecating pretty much all the functions having to do
+ // with getting version information because apparently, people keep misusing
+ // them for feature detection
+ //
+ // there's still RtlGetVersion() though
+
+ using RtlGetVersionType = NTSTATUS (NTAPI)(PRTL_OSVERSIONINFOW);
+
+ auto* RtlGetVersion = reinterpret_cast<RtlGetVersionType*>(
+ GetProcAddress(ntdll, "RtlGetVersion"));
+
+ if (!RtlGetVersion) {
+ qCritical() << "RtlGetVersion() not found in ntdll.dll";
+ return {};
+ }
+
+ OSVERSIONINFOEX vi = {};
+ vi.dwOSVersionInfoSize = sizeof(vi);
+
+ // this apparently never fails
+ RtlGetVersion((RTL_OSVERSIONINFOW*)&vi);
+
+ return {vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber};
+}
+
+WindowsInfo::Version WindowsInfo::getRealVersion(HINSTANCE ntdll) const
+{
+ // getting the actual windows version is more difficult because all the
+ // functions are lying when running in compatibility mode
+ //
+ // RtlGetNtVersionNumbers() is an undocumented function that seems to work
+ // fine, but it might not in the future
+
+ using RtlGetNtVersionNumbersType = void (NTAPI)(DWORD*, DWORD*, DWORD*);
+
+ auto* RtlGetNtVersionNumbers = reinterpret_cast<RtlGetNtVersionNumbersType*>(
+ GetProcAddress(ntdll, "RtlGetNtVersionNumbers"));
+
+ if (!RtlGetNtVersionNumbers) {
+ qCritical() << "RtlGetNtVersionNumbers not found in ntdll.dll";
+ return {};
+ }
+
+ DWORD major=0, minor=0, build=0;
+ RtlGetNtVersionNumbers(&major, &minor, &build);
+
+ // for whatever reason, the build number has 0xf0000000 set
+ build = 0x0fffffff & build;
+
+ return {major, minor, build};
+}
+
+WindowsInfo::Release WindowsInfo::getRelease() const
+{
+ // there are several interesting items in the registry, but most of them
+ // are undocumented, not always available, and localizable
+ //
+ // most of them are used to provide as much information as possible in case
+ // any of the other versions fail to work
+
+ QSettings settings(
+ R"(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
+ QSettings::NativeFormat);
+
+ Release r;
+
+ // buildlab seems to be an internal name from the build system
+ r.buildLab = settings.value("BuildLabEx", "").toString();
+ if (r.buildLab.isEmpty()) {
+ r.buildLab = settings.value("BuildLab", "").toString();
+ if (r.buildLab.isEmpty()) {
+ r.buildLab = settings.value("BuildBranch", "").toString();
+ }
+ }
+
+ // localized name of windows, such as "Windows 10 Pro"
+ r.productName = settings.value("ProductName", "").toString();
+
+ // release ID, such as 1803
+ r.ID = settings.value("ReleaseId", "").toString();
+
+ // some other build number, shown in winver.exe
+ r.UBR = settings.value("UBR", 0).toUInt();
+
+ return r;
+}
+
+std::optional<bool> WindowsInfo::getElevated() const
+{
+ HandlePtr token;
+
+ {
+ HANDLE rawToken = 0;
+
+ if (!OpenProcessToken(GetCurrentProcess( ), TOKEN_QUERY, &rawToken)) {
+ const auto e = GetLastError();
+
+ qCritical()
+ << "while trying to check if process is elevated, "
+ << "OpenProcessToken() failed: " << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ token.reset(rawToken);
+ }
+
+ TOKEN_ELEVATION e = {};
+ DWORD size = sizeof(TOKEN_ELEVATION);
+
+ if (!GetTokenInformation(token.get(), TokenElevation, &e, sizeof(e), &size)) {
+ const auto e = GetLastError();
+
+ qCritical()
+ << "while trying to check if process is elevated, "
+ << "GetTokenInformation() failed: " << formatSystemMessageQ(e);
+
+ return {};
+ }
+
+ return (e.TokenIsElevated != 0);
+}
+
+
+SecurityProduct::SecurityProduct(
+ QString name, int provider,
+ bool active, bool upToDate) :
+ m_name(std::move(name)), m_provider(provider),
+ m_active(active), m_upToDate(upToDate)
+{
+}
+
+const QString& SecurityProduct::name() const
+{
+ return m_name;
+}
+
+int SecurityProduct::provider() const
+{
+ return m_provider;
+}
+
+bool SecurityProduct::active() const
+{
+ return m_active;
+}
+
+bool SecurityProduct::upToDate() const
+{
+ return m_upToDate;
+}
+
+QString SecurityProduct::toString() const
+{
+ QString s;
+
+ s += m_name + " ";
+
+
+ QStringList ps;
+ if (m_provider & WSC_SECURITY_PROVIDER_FIREWALL) {
+ ps.push_back("firewall");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS) {
+ ps.push_back("autoupdate");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_ANTIVIRUS) {
+ ps.push_back("antivirus");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_ANTISPYWARE) {
+ ps.push_back("antispyware");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_INTERNET_SETTINGS) {
+ ps.push_back("settings");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL) {
+ ps.push_back("uac");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_SERVICE) {
+ ps.push_back("service");
+ }
+
+ if (ps.empty()) {
+ s += "(doesn't provide anything)";
+ } else {
+ s += "(" + ps.join("|") + ")";
+ }
+
+ if (m_active) {
+ s += ", active";
+ } else {
+ s += ", inactive";
+ }
+
+ if (!m_upToDate) {
+ s += ", definitions outdated";
+ }
+
+ return s;
+}
+
+
+struct Process
+{
+ std::wstring filename;
+ DWORD pid;
+
+ Process(std::wstring f, DWORD id)
+ : filename(std::move(f)), pid(id)
+ {
+ }
+};
+
+// returns the filename of the given process or the current one
+//
+std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
+{
+ // double the buffer size 10 times
+ const int MaxTries = 10;
+
+ DWORD bufferSize = MAX_PATH;
+
+ for (int tries=0; tries<MaxTries; ++tries)
+ {
+ auto buffer = std::make_unique<wchar_t[]>(bufferSize + 1);
+ std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0);
+
+ DWORD writtenSize = 0;
+
+ if (process == INVALID_HANDLE_VALUE) {
+ // query this process
+ writtenSize = GetModuleFileNameW(0, buffer.get(), bufferSize);
+ } else {
+ // query another process
+ writtenSize = GetModuleBaseNameW(process, 0, buffer.get(), bufferSize);
+ }
+
+ if (writtenSize == 0) {
+ // hard failure
+ const auto e = GetLastError();
+ std::wcerr << formatSystemMessage(e) << L"\n";
+ break;
+ } else if (writtenSize >= bufferSize) {
+ // buffer is too small, try again
+ bufferSize *= 2;
+ } else {
+ // if GetModuleFileName() works, `writtenSize` does not include the null
+ // terminator
+ const std::wstring s(buffer.get(), writtenSize);
+ const fs::path path(s);
+
+ return path.filename().native();
+ }
+ }
+
+ // something failed or the path is way too long to make sense
+
+ std::wstring what;
+ if (process == INVALID_HANDLE_VALUE) {
+ what = L"the current process";
+ } else {
+ what = L"pid " + std::to_wstring(reinterpret_cast<std::uintptr_t>(process));
+ }
+
+ std::wcerr << L"failed to get filename for " << what << L"\n";
+ return {};
+}
+
+std::vector<DWORD> runningProcessesIds()
+{
+ // double the buffer size 10 times
+ const int MaxTries = 10;
+
+ // initial size of 300 processes, unlikely to be more than that
+ std::size_t size = 300;
+
+ for (int tries=0; tries<MaxTries; ++tries) {
+ auto ids = std::make_unique<DWORD[]>(size);
+ std::fill(ids.get(), ids.get() + size, 0);
+
+ DWORD bytesGiven = static_cast<DWORD>(size * sizeof(ids[0]));
+ DWORD bytesWritten = 0;
+
+ if (!EnumProcesses(ids.get(), bytesGiven, &bytesWritten))
+ {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to enumerate processes, "
+ << formatSystemMessage(e) << L"\n";
+
+ return {};
+ }
+
+ if (bytesWritten == bytesGiven) {
+ // no way to distinguish between an exact fit and not enough space,
+ // just try again
+ size *= 2;
+ continue;
+ }
+
+ const auto count = bytesWritten / sizeof(ids[0]);
+ return std::vector<DWORD>(ids.get(), ids.get() + count);
+ }
+
+ std::cerr << L"too many processes to enumerate";
+ return {};
+}
+
+std::vector<Process> runningProcesses()
+{
+ const auto pids = runningProcessesIds();
+ std::vector<Process> v;
+
+ for (const auto& pid : pids) {
+ if (pid == 0) {
+ // the idle process has pid 0 and seems to be picked up by EnumProcesses()
+ continue;
+ }
+
+ HandlePtr h(OpenProcess(
+ PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
+
+ if (!h) {
+ const auto e = GetLastError();
+
+ if (e != ERROR_ACCESS_DENIED) {
+ // don't log access denied, will happen a lot for system processes, even
+ // when elevated
+ std::wcerr
+ << L"failed to open process " << pid << L", "
+ << formatSystemMessage(e) << L"\n";
+ }
+
+ continue;
+ }
+
+ auto filename = processFilename(h.get());
+ if (!filename.empty()) {
+ v.emplace_back(std::move(filename), pid);
+ }
+ }
+
+ return v;
+}
+
+DWORD findOtherPid()
+{
+ const std::wstring defaultName = L"ModOrganizer.exe";
+
+ std::wclog << L"looking for the other process...\n";
+
+ // used to skip the current process below
+ const auto thisPid = GetCurrentProcessId();
+ std::wclog << L"this process id is " << thisPid << L"\n";
+
+ // getting the filename for this process, assumes the other process has the
+ // smae one
+ auto filename = processFilename();
+ if (filename.empty()) {
+ std::wcerr
+ << L"can't get current process filename, defaulting to "
+ << defaultName << L"\n";
+
+ filename = defaultName;
+ } else {
+ std::wclog << L"this process filename is " << filename << L"\n";
+ }
+
+ // getting all running processes
+ const auto processes = runningProcesses();
+ std::wclog << L"there are " << processes.size() << L" processes running\n";
+
+ // going through processes, trying to find one with the same name and a
+ // different pid than this process has
+ for (const auto& p : processes) {
+ if (p.filename == filename) {
+ if (p.pid != thisPid) {
+ return p.pid;
+ }
+ }
+ }
+
+ std::wclog
+ << L"no process with this filename\n"
+ << L"MO may not be running, or it may be running as administrator\n"
+ << L"you can try running this again as administrator\n";
+
+ return 0;
+}
+
+std::wstring tempDir()
+{
+ const DWORD bufferSize = MAX_PATH + 1;
+ wchar_t buffer[bufferSize + 1] = {};
+
+ const auto written = GetTempPathW(bufferSize, buffer);
+ if (written == 0) {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to get temp path, " << formatSystemMessage(e) << L"\n";
+
+ return {};
+ }
+
+ // `written` does not include the null terminator
+ return std::wstring(buffer, buffer + written);
+}
+
+HandlePtr tempFile(const std::wstring dir)
+{
+ // maximum tries of incrementing the counter
+ const int MaxTries = 100;
+
+ // UTC time and date will be in the filename
+ const auto now = std::time(0);
+ const auto tm = std::gmtime(&now);
+
+ // "ModOrganizer-YYYYMMDDThhmmss.dmp", with a possible "-i" appended, where
+ // i can go until MaxTries
+ std::wostringstream oss;
+ oss
+ << L"ModOrganizer-"
+ << std::setw(4) << (1900 + tm->tm_year)
+ << std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1)
+ << std::setw(2) << std::setfill(L'0') << tm->tm_mday << "T"
+ << std::setw(2) << std::setfill(L'0') << tm->tm_hour
+ << std::setw(2) << std::setfill(L'0') << tm->tm_min
+ << std::setw(2) << std::setfill(L'0') << tm->tm_sec;
+
+ const std::wstring prefix = oss.str();
+ const std::wstring ext = L".dmp";
+
+ // first path to try, without counter in it
+ std::wstring path = dir + L"\\" + prefix + ext;
+
+ for (int i=0; i<MaxTries; ++i) {
+ std::wclog << L"trying file '" << path << L"'\n";
+
+ HandlePtr h (CreateFileW(
+ path.c_str(), GENERIC_WRITE, 0, nullptr,
+ CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
+
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ // worked
+ return h;
+ }
+
+ const auto e = GetLastError();
+
+ if (e != ERROR_FILE_EXISTS) {
+ // probably no write access
+ std::wcerr
+ << L"failed to create dump file, " << formatSystemMessage(e) << L"\n";
+
+ return {};
+ }
+
+ // try again with "-i"
+ path = dir + L"\\" + prefix + L"-" + std::to_wstring(i + 1) + ext;
+ }
+
+ std::wcerr << L"can't create dump file, ran out of filenames\n";
+ return {};
+}
+
+HandlePtr dumpFile()
+{
+ // try the current directory
+ HandlePtr h = tempFile(L".");
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ return h;
+ }
+
+ std::wclog << L"cannot write dump file in current directory\n";
+
+ // try the temp directory
+ const auto dir = tempDir();
+
+ if (!dir.empty()) {
+ h = tempFile(dir.c_str());
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ return h;
+ }
+ }
+
+ return {};
+}
+
+bool createMiniDump(HANDLE process, CoreDumpTypes type)
+{
+ const DWORD pid = GetProcessId(process);
+
+ const HandlePtr file = dumpFile();
+ if (!file) {
+ std::wcerr << L"nowhere to write the dump file\n";
+ return false;
+ }
+
+ auto flags = _MINIDUMP_TYPE(
+ MiniDumpNormal |
+ MiniDumpWithHandleData |
+ MiniDumpWithUnloadedModules |
+ MiniDumpWithProcessThreadData);
+
+ if (type == CoreDumpTypes::Data) {
+ std::wclog << L"writing minidump with data\n";
+ flags = _MINIDUMP_TYPE(flags | MiniDumpWithDataSegs);
+ } else if (type == CoreDumpTypes::Full) {
+ std::wclog << L"writing full minidump\n";
+ flags = _MINIDUMP_TYPE(flags | MiniDumpWithFullMemory);
+ } else {
+ std::wclog << L"writing mini minidump\n";
+ }
+
+ const auto ret = MiniDumpWriteDump(
+ process, pid, file.get(), flags, nullptr, nullptr, nullptr);
+
+ if (!ret) {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to write mini dump, " << formatSystemMessage(e) << L"\n";
+
+ return false;
+ }
+
+ std::wclog << L"minidump written correctly\n";
+ return true;
+}
+
+
+bool coredump(CoreDumpTypes type)
+{
+ std::wclog << L"creating minidump for the current process\n";
+ return createMiniDump(GetCurrentProcess(), type);
+}
+
+bool coredumpOther(CoreDumpTypes type)
+{
+ std::wclog << L"creating minidump for an running process\n";
+
+ const auto pid = findOtherPid();
+ if (pid == 0) {
+ std::wcerr << L"no other process found\n";
+ return false;
+ }
+
+ std::wclog << L"found other process with pid " << pid << L"\n";
+
+ HandlePtr handle(OpenProcess(
+ PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
+
+ if (!handle) {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to open process " << pid << L", "
+ << formatSystemMessage(e) << L"\n";
+
+ return false;
+ }
+
+ return createMiniDump(handle.get(), type);
+}
+
+} // namespace env
+
} // namespace MOShared
diff --git a/src/shared/util.h b/src/shared/util.h index 1fdfb089..c4a2ed7d 100644 --- a/src/shared/util.h +++ b/src/shared/util.h @@ -22,11 +22,15 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <string>
+#include <optional>
+
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <versioninfo.h>
+class Executable;
+
namespace MOShared {
/// Test if a file (or directory) by the specified name exists
@@ -46,8 +50,394 @@ std::wstring ToLower(const std::wstring &text); bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs);
-VS_FIXEDFILEINFO GetFileVersion(const std::wstring &fileName);
-std::wstring GetFileVersionString(const std::wstring &fileName);
+
+namespace env
+{
+
+// an application shortcut that can be either on the desktop or the start menu
+//
+class Shortcut
+{
+public:
+ // location of a shortcut
+ //
+ enum Locations
+ {
+ None = 0,
+
+ // on the desktop
+ Desktop,
+
+ // in the start menu
+ StartMenu
+ };
+
+
+ // empty shortcut
+ //
+ Shortcut();
+
+ // shortcut from an executable
+ //
+ explicit Shortcut(const Executable& exe);
+
+ // sets the name of the shortcut, shown on icons and start menu entries
+ //
+ Shortcut& name(const QString& s);
+
+ // the program to start
+ //
+ Shortcut& target(const QString& s);
+
+ // arguments to pass
+ //
+ Shortcut& arguments(const QString& s);
+
+ // shows in the status bar of explorer, for example
+ //
+ Shortcut& description(const QString& s);
+
+ // path to a binary that contains the icon and its index
+ //
+ Shortcut& icon(const QString& s, int index=0);
+
+ // "start in" option for this shortcut
+ //
+ Shortcut& workingDirectory(const QString& s);
+
+
+ // returns whether this shortcut already exists at the given location; this
+ // does not check whether the shortcut parameters are different, it merely if
+ // the .lnk file exists
+ //
+ bool exists(Locations loc) const;
+
+ // calls remove() if exists(), or add()
+ //
+ bool toggle(Locations loc);
+
+ // adds the shortcut to the given location
+ //
+ bool add(Locations loc);
+
+ // removes the shortcut from the given location
+ //
+ bool remove(Locations loc);
+
+private:
+ QString m_name;
+ QString m_target;
+ QString m_arguments;
+ QString m_description;
+ QString m_icon;
+ int m_iconIndex;
+ QString m_workingDirectory;
+
+ // returns a qCritical() logger with a prefix already logged
+ //
+ QDebug critical() const;
+
+ // returns a qDebug() logger with a prefix already logged
+ //
+ QDebug debug() const;
+
+
+ // returns the path where the shortcut file should be saved
+ //
+ QString shortcutPath(Locations loc) const;
+
+ // returns the directory where the shortcut file should be saved
+ //
+ QString shortcutDirectory(Locations loc) const;
+
+ // returns the filename of the shortcut file that should be used when saving
+ //
+ QString shortcutFilename() const;
+};
+
+
+// returns a string representation of the given location
+//
+QString toString(Shortcut::Locations loc);
+
+
+// represents one module
+//
+class Module
+{
+public:
+ explicit Module(QString path, std::size_t fileSize);
+
+ // returns the module's path
+ //
+ const QString& path() const;
+
+ // returns the module's path in lowercase and using forward slashes
+ //
+ QString displayPath() const;
+
+ // returns the size in bytes, may be 0
+ //
+ std::size_t fileSize() const;
+
+ // returns the x.x.x.x version embedded from the version info, may be empty
+ //
+ const QString& version() const;
+
+ // returns the FileVersion entry from the resource file, returns
+ // "(no version)" if not available
+ //
+ const QString& versionString() const;
+
+ // returns the build date from the version info, or the creation time of the
+ // file on the filesystem, may be empty
+ //
+ const QDateTime& timestamp() const;
+
+ // returns the md5 of the file, may be empty for system files
+ //
+ const QString& md5() const;
+
+ // converts timestamp() to a string for display, returns "(no timestamp)" if
+ // not available
+ //
+ QString timestampString() const;
+
+ // returns a string with all the above information on one line
+ //
+ QString toString() const;
+
+private:
+ // contains the information from the version resource
+ //
+ struct FileInfo
+ {
+ VS_FIXEDFILEINFO ffi;
+ QString fileDescription;
+ };
+
+ QString m_path;
+ std::size_t m_fileSize;
+ QString m_version;
+ QDateTime m_timestamp;
+ QString m_versionString;
+ QString m_md5;
+
+ // returns information from the version resource
+ //
+ FileInfo getFileInfo() const;
+
+ // uses VS_FIXEDFILEINFO to build the version string
+ //
+ QString getVersion(const VS_FIXEDFILEINFO& fi) const;
+
+ // uses the file date from VS_FIXEDFILEINFO if available, or gets the
+ // creation date on the file
+ //
+ QDateTime getTimestamp(const VS_FIXEDFILEINFO& fi) const;
+
+ // returns the md5 hash unless the path contains "\windows\"
+ //
+ QString getMD5() const;
+
+ // gets VS_FIXEDFILEINFO from the file version info buffer
+ //
+ VS_FIXEDFILEINFO getFixedFileInfo(std::byte* buffer) const;
+
+ // gets FileVersion from the file version info buffer
+ //
+ QString getFileDescription(std::byte* buffer) const;
+};
+
+
+// a variety of information on windows
+//
+class WindowsInfo
+{
+public:
+ struct Version
+ {
+ DWORD major=0, minor=0, build=0;
+
+ QString toString() const
+ {
+ return QString("%1.%2.%3").arg(major).arg(minor).arg(build);
+ }
+
+ friend bool operator==(const Version& a, const Version& b)
+ {
+ return
+ a.major == b.major &&
+ a.minor == b.minor &&
+ a.build == b.build;
+ }
+
+ friend bool operator!=(const Version& a, const Version& b)
+ {
+ return !(a == b);
+ }
+ };
+
+ struct Release
+ {
+ // the BuildLab entry from the registry, may be empty
+ QString buildLab;
+
+ // product name such as "Windows 10 Pro", may not be in English, may be
+ // empty
+ QString productName;
+
+ // release ID such as 1809, may be mepty
+ QString ID;
+
+ // some sub-build number, undocumented, may be empty
+ DWORD UBR;
+
+ Release()
+ : UBR(0)
+ {
+ }
+ };
+
+
+ WindowsInfo();
+
+ // tries to guess whether this process is running in compatibility mode
+ //
+ bool compatibilityMode() const;
+
+ // returns the Windows version, may not correspond to the actual version
+ // if the process is running in compatibility mode
+ //
+ const Version& reportedVersion() const;
+
+ // tries to guess the real Windows version that's running, can be empty
+ //
+ const Version& realVersion() const;
+
+ // various information about the current release
+ //
+ const Release& release() const;
+
+ // whether this process is running as administrator, may be empty if the
+ // information is not available
+ std::optional<bool> isElevated() const;
+
+ // returns a string with all the above information on one line
+ //
+ QString toString() const;
+
+private:
+ Version m_reported, m_real;
+ Release m_release;
+ std::optional<bool> m_elevated;
+
+ // uses RtlGetVersion() to get the version number as reported by Windows
+ //
+ Version getReportedVersion(HINSTANCE ntdll) const;
+
+ // uses RtlGetNtVersionNumbers() to get the real version number
+ //
+ Version getRealVersion(HINSTANCE ntdll) const;
+
+ // gets various information from the registry
+ //
+ Release getRelease() const;
+
+ // gets whether the process is elevated
+ //
+ std::optional<bool> getElevated() const;
+};
+
+
+// represents a security product, such as an antivirus or a firewall
+//
+class SecurityProduct
+{
+public:
+ SecurityProduct(
+ QString name, int provider,
+ bool active, bool upToDate);
+
+ // display name of the product
+ //
+ const QString& name() const;
+
+ // a bunch of _WSC_SECURITY_PROVIDER flags
+ //
+ int provider() const;
+
+ // whether the product is active
+ //
+ bool active() const;
+
+ // whether its definitions are up-to-date
+ //
+ bool upToDate() const;
+
+ // string representation of the above
+ //
+ QString toString() const;
+
+private:
+ QString m_name;
+ int m_provider;
+ bool m_active;
+ bool m_upToDate;
+};
+
+
+// represents the process's environment
+//
+class Environment
+{
+public:
+ Environment();
+
+ // list of loaded modules in the current process
+ //
+ const std::vector<Module>& loadedModules();
+
+ // information about the operating system
+ //
+ const WindowsInfo& windowsInfo() const;
+
+ // information about the installed security products
+ //
+ const std::vector<SecurityProduct>& securityProducts() const;
+
+private:
+ std::vector<Module> m_modules;
+ WindowsInfo m_windows;
+ std::vector<SecurityProduct> m_security;
+
+ std::vector<Module> getLoadedModules() const;
+ std::vector<SecurityProduct> getSecurityProducts() const;
+
+ std::vector<SecurityProduct> getSecurityProductsFromWMI() const;
+ std::optional<SecurityProduct> getWindowsFirewall() const;
+};
+
+
+enum class CoreDumpTypes
+{
+ Mini = 1,
+ Data,
+ Full
+};
+
+// creates a minidump file for the given process
+//
+bool coredump(CoreDumpTypes type);
+
+// finds another process with the same name as this one and creates a minidump
+// file for it
+//
+bool coredumpOther(CoreDumpTypes type);
+
+} // namespace env
+
+
MOBase::VersionInfo createVersionInfo();
} // namespace MOShared
diff --git a/src/simpleinstalldialog.ui b/src/simpleinstalldialog.ui deleted file mode 100644 index a9fdc1d5..00000000 --- a/src/simpleinstalldialog.ui +++ /dev/null @@ -1,82 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>SimpleInstallDialog</class>
- <widget class="QDialog" name="SimpleInstallDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>400</width>
- <height>71</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Quick Install</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Name</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="nameEdit"/>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="manualBtn">
- <property name="toolTip">
- <string>Opens a Dialog that allows custom modifications.</string>
- </property>
- <property name="whatsThis">
- <string>Opens a Dialog that allows custom modifications.</string>
- </property>
- <property name="text">
- <string>Manual</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="okBtn">
- <property name="text">
- <string>OK</string>
- </property>
- <property name="default">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="cancelBtn">
- <property name="text">
- <string>Cancel</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/spawn.cpp b/src/spawn.cpp index e1de5c0f..f77da35f 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -94,7 +94,6 @@ static bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory, PROCESS_INFORMATION pi;
BOOL success = FALSE;
if (hooked) {
- qDebug() << "Creating process hooked: <" << QString::fromWCharArray(commandLine, static_cast<int>(length)) <<">";
success = ::CreateProcessHooked(nullptr,
commandLine,
nullptr, nullptr, // no special process or thread attributes
@@ -151,7 +150,7 @@ HANDLE startBinary(const QFileInfo &binary, if (e.getErrorCode() == ERROR_ELEVATION_REQUIRED) {
if (QMessageBox::question(QApplication::activeModalWidget(), QObject::tr("Elevation required"),
QObject::tr("This process requires elevation to run.\n"
- "This is a potential security risk so I highly advice you to investigate if\n"
+ "This is a potential security risk so I highly advise you to investigate if\n"
"\"%1\"\n"
"can be installed to work without elevation.\n\n"
"Restart Mod Organizer as an elevated process?\n"
diff --git a/src/statusbar.cpp b/src/statusbar.cpp new file mode 100644 index 00000000..e9a6e658 --- /dev/null +++ b/src/statusbar.cpp @@ -0,0 +1,168 @@ +#include "statusbar.h" +#include "nexusinterface.h" +#include "settings.h" +#include "ui_mainwindow.h" + +StatusBar::StatusBar(QStatusBar* bar, Ui::MainWindow* ui) : + m_bar(bar), m_progress(new QProgressBar), + m_notifications(new StatusBarAction(ui->actionNotifications)), + m_update(new StatusBarAction(ui->actionUpdate)), + m_api(new QLabel) +{ + QWidget* spacer1 = new QWidget; + spacer1->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + spacer1->setHidden(true); + spacer1->setVisible(true); + m_bar->addPermanentWidget(spacer1, 0); + m_bar->addPermanentWidget(m_progress); + QWidget* spacer2 = new QWidget; + spacer2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + spacer2->setHidden(true); + spacer2->setVisible(true); + m_bar->addPermanentWidget(spacer2,0); + m_bar->addPermanentWidget(m_notifications); + m_bar->addPermanentWidget(m_update); + m_bar->addPermanentWidget(m_api); + + + m_progress->setTextVisible(true); + m_progress->setRange(0, 100); + m_progress->setMaximumWidth(300); + m_progress->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + m_update->set(false); + m_notifications->set(false); + + m_api->setObjectName("apistats"); + m_api->setToolTip(QObject::tr( + "This tracks the number of queued Nexus API requests, as well as the " + "remaining daily and hourly requests. The Nexus API limits you to a pool " + "of requests per day and requests per hour. It is dynamically updated " + "every time a request is completed. If you run out of requests, you will " + "be unable to queue downloads, check updates, parse mod info, or even log " + "in. Both pools must be consumed before this happens.")); + + m_bar->clearMessage(); + setProgress(-1); + setAPI({}, {}); +} + +void StatusBar::setProgress(int percent) +{ + if (percent < 0 || percent >= 100) { + m_bar->clearMessage(); + m_progress->setVisible(false); + } else { + m_bar->showMessage(QObject::tr("Loading...")); + m_progress->setVisible(true); + m_progress->setValue(percent); + } +} + +void StatusBar::setNotifications(bool hasNotifications) +{ + m_notifications->set(hasNotifications); +} + +void StatusBar::setAPI(const APIStats& stats, const APIUserAccount& user) +{ + QString text; + QString textColor; + QString backgroundColor; + + if (user.type() == APIUserAccountTypes::None) { + text = "API: not logged in"; + textColor = ""; + backgroundColor = ""; + } else { + text = QString("API: Queued: %1 | Daily: %2 | Hourly: %3") + .arg(stats.requestsQueued) + .arg(user.limits().remainingDailyRequests) + .arg(user.limits().remainingHourlyRequests); + + if (user.remainingRequests() > 500) { + textColor = "white"; + backgroundColor = "darkgreen"; + } else if (user.remainingRequests() > 200) { + textColor = "black"; + backgroundColor = "rgb(226, 192, 0)"; // yellow + } else { + textColor = "white"; + backgroundColor = "darkred"; + } + } + + m_api->setText(text); + + QString ss(R"( + QLabel + { + padding-left: 0.1em; + padding-right: 0.1em; + padding-top: 0; + padding-bottom: 0;)"); + + if (!textColor.isEmpty()) { + ss += QString("\ncolor: %1;").arg(textColor); + } + + if (!backgroundColor.isEmpty()) { + ss += QString("\nbackground-color: %2;").arg(backgroundColor); + } + + ss += "\n}"; + + m_api->setStyleSheet(ss); + m_api->setAutoFillBackground(true); +} + +void StatusBar::setUpdateAvailable(bool b) +{ + m_update->set(b); +} + +void StatusBar::checkSettings(const Settings& settings) +{ + m_api->setVisible(!settings.hideAPICounter()); +} + + +StatusBarAction::StatusBarAction(QAction* action) + : m_action(action), m_icon(new QLabel), m_text(new QLabel) +{ + setLayout(new QHBoxLayout); + layout()->setContentsMargins(0, 0, 0, 0); + layout()->addWidget(m_icon); + layout()->addWidget(m_text); +} + +void StatusBarAction::set(bool visible) +{ + if (visible) { + m_icon->setPixmap(m_action->icon().pixmap(16, 16)); + m_text->setText(cleanupActionText(m_action->text())); + } + + setVisible(visible); +} + +void StatusBarAction::mouseDoubleClickEvent(QMouseEvent* e) +{ + if (m_action->isEnabled()) { + m_action->trigger(); + } +} + +QString StatusBarAction::cleanupActionText(const QString& original) const +{ + QString s = original; + + s.replace(QRegExp("\\&([^&])"), "\\1"); // &Item -> Item + s.replace("&&", "&"); // &&Item -> &Item + + if (s.endsWith("...")) { + s = s.left(s.size() - 3); + } + + return s; +} diff --git a/src/statusbar.h b/src/statusbar.h new file mode 100644 index 00000000..2baf12ee --- /dev/null +++ b/src/statusbar.h @@ -0,0 +1,51 @@ +#ifndef MO_STATUSBAR_H +#define MO_STATUSBAR_H + +#include <QStatusBar> +#include <QProgressBar> + +struct APIStats; +class APIUserAccount; +class Settings; +namespace Ui { class MainWindow; } + + +class StatusBarAction : public QWidget +{ +public: + StatusBarAction(QAction* action); + + void set(bool visible); + +protected: + void mouseDoubleClickEvent(QMouseEvent* e) override; + +private: + QAction* m_action; + QLabel* m_icon; + QLabel* m_text; + + QString cleanupActionText(const QString& s) const; +}; + + +class StatusBar +{ +public: + StatusBar(QStatusBar* bar, Ui::MainWindow* ui); + + void setProgress(int percent); + void setNotifications(bool hasNotifications); + void setAPI(const APIStats& stats, const APIUserAccount& user); + void setUpdateAvailable(bool b); + void checkSettings(const Settings& settings); + +private: + QStatusBar* m_bar; + QProgressBar* m_progress; + StatusBarAction* m_notifications; + StatusBarAction* m_update; + QLabel* m_api; +}; + +#endif // MO_STATUSBAR_H diff --git a/src/stylesheets/Night Eyes.qss b/src/stylesheets/Night Eyes.qss index 6c6b0629..142acb6a 100644 --- a/src/stylesheets/Night Eyes.qss +++ b/src/stylesheets/Night Eyes.qss @@ -106,15 +106,15 @@ QTreeView::item:selected color: #FFCC88; } -QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:has-children:!has-siblings:closed, QTreeView::branch:closed:has-children:has-siblings { image: url(:/stylesheet/branch-closed.png); border: 0px; } -QTreeView::branch:open:has-children:!has-siblings, -QTreeView::branch:open:has-children:has-siblings +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings { image: url(:/stylesheet/branch-open.png); border: 0px; @@ -342,41 +342,41 @@ QScrollBar::sub-line:vertical /* Combined */ -QScrollBar::handle:horizontal:hover, +QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:hover, -QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover { background: #242424; } -QScrollBar::handle:horizontal:pressed, +QScrollBar::handle:horizontal:pressed, QScrollBar::handle:vertical:pressed, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { background: #181818; } -QScrollBar::add-page:horizontal, -QScrollBar::sub-page:horizontal, -QScrollBar::add-page:vertical, +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal, +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; } -QScrollBar::up-arrow:vertical, +QScrollBar::up-arrow:vertical, QScrollBar::right-arrow:horizontal, -QScrollBar::down-arrow:vertical, +QScrollBar::down-arrow:vertical, QScrollBar::left-arrow:horizontal { height: 1px; - width: 1px; + width: 1px; border: 1px solid #181818; } @@ -426,6 +426,16 @@ QHeaderView::down-arrow /* Context Menus, Toolbar Dropdowns, & Tooltips ------------------------------- */ +QMenuBar { + background: #181818; + border: 1px solid #181818; +} + +QMenuBar::item:selected { + background: #242424; + color: #FFCC88; +} + QMenu { background: #141414; @@ -471,6 +481,7 @@ QToolTip border: 0px; } +QStatusBar::item {border: None;} /* Progress Bars (Downloads) -------------------------------------------------- */ @@ -615,8 +626,8 @@ QWidget#downloadTab QAbstractScrollArea background: #242424; } -DownloadListWidget QFrame, -DownloadListWidgetCompact, +DownloadListWidget QFrame, +DownloadListWidgetCompact, DownloadListWidgetCompact QLabel { /* an entry on the Downloads tab */ @@ -641,7 +652,7 @@ DownloadListWidget QFrame:clicked /* compact downloads view */ -DownloadListWidgetCompact, +DownloadListWidgetCompact, DownloadListWidgetCompact QLabel { /* an entry on the Downloads tab */ diff --git a/src/stylesheets/Paper Automata.qss b/src/stylesheets/Paper Automata.qss index d72aa7e2..7c0a604e 100644 --- a/src/stylesheets/Paper Automata.qss +++ b/src/stylesheets/Paper Automata.qss @@ -593,6 +593,16 @@ QHeaderView::section:last { /* Right Click Menus, Toolbar Dropdown Menus, & Tooltips */ +QMenuBar { + background: #DAD4BB; + border: 2px solid #CDC8B0; +} + +QMenuBar::item:selected { + background: #B4AF9A; + border: none; +} + QMenu { /* right click menu */ background: #DAD4BB; @@ -671,6 +681,8 @@ QToolTip { border: 2px solid #CDC8B0; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) */ QProgressBar { diff --git a/src/stylesheets/Paper Dark by 6788.qss b/src/stylesheets/Paper Dark by 6788.qss index 9abd8086..77086c15 100644 --- a/src/stylesheets/Paper Dark by 6788.qss +++ b/src/stylesheets/Paper Dark by 6788.qss @@ -588,6 +588,18 @@ QHeaderView::down-arrow { /* Right Click Menus, Toolbar Dropdown Menus, & Tooltips */ +QMenuBar { + background: #242424; + border: 1px solid #242424; +} + +QMenuBar::item:selected { + background: #006868; + color: #FFFFFF; + border: 0px; + border-radius: 4px; +} + QMenu { /* right click menu */ background: #141414; @@ -671,6 +683,8 @@ QToolTip { border-radius: 6px; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) */ QProgressBar { diff --git a/src/stylesheets/Paper Light by 6788.qss b/src/stylesheets/Paper Light by 6788.qss index 4aad55a8..2b08bcd1 100644 --- a/src/stylesheets/Paper Light by 6788.qss +++ b/src/stylesheets/Paper Light by 6788.qss @@ -590,6 +590,18 @@ QHeaderView::down-arrow { /* Right Click Menus, Toolbar Dropdown Menus, & Tooltips */ +QMenuBar { + background: #EBEBEB; + border: 1px solid #EBEBEB; +} + +QMenuBar::item:selected { + background: #008484; + color: #FFFFFF; + border: 0px; + border-radius: 4px; +} + QMenu { /* right click menu */ background: #FFFFFF; @@ -673,6 +685,8 @@ QToolTip { border-radius: 6px; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) */ QProgressBar { diff --git a/src/stylesheets/Parchment v1.1 by Bob.qss b/src/stylesheets/Parchment v1.1 by Bob.qss index da3546d1..61c2f84d 100644 --- a/src/stylesheets/Parchment v1.1 by Bob.qss +++ b/src/stylesheets/Parchment v1.1 by Bob.qss @@ -95,7 +95,7 @@ QTreeView { } QTreeView::branch:hover { - background: #008F8F; + background: #008F8F; color: #F7F6CF; } @@ -109,13 +109,13 @@ QTreeView::item:selected { color: #F7F6CF; } -QTreeView::branch:has-children:!has-siblings:closed, -QTreeView::branch:closed:has-children:has-siblings { +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings { image: url(:/stylesheet/branch-closed.png); border: 0px; } -QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings { image: url(:/stylesheet/branch-open.png); border: 0px; @@ -168,7 +168,7 @@ QCheckBox::indicator { background-color: transparent; border: none; width: 14px; - height: 14px; + height: 14px; } QGroupBox::indicator:checked, QGroupBox::indicator:indeterminate, @@ -177,7 +177,7 @@ QTreeView::indicator:indeterminate, QCheckBox::indicator:checked, QCheckBox::indicator:indeterminate { - image: url(./Parchment/checkbox-checked.png); + image: url(./Parchment/checkbox-checked.png); } QGroupBox::indicator:checked:hover, QGroupBox::indicator:indeterminate:hover, @@ -186,7 +186,7 @@ QTreeView::indicator:indeterminate:hover, QCheckBox::indicator:checked:hover, QCheckBox::indicator:indeterminate:hover { - image: url(./Parchment/checkbox-checked-hover.png); + image: url(./Parchment/checkbox-checked-hover.png); } QGroupBox::indicator:checked:disabled, QGroupBox::indicator:indeterminate:disabled, @@ -195,28 +195,28 @@ QCheckBox::indicator:indeterminate:hover { QCheckBox::indicator:checked:disabled, QCheckBox::indicator:indeterminate:disabled { - image: url(./Parchment/checkbox-checked-disabled.png); + image: url(./Parchment/checkbox-checked-disabled.png); } QGroupBox::indicator:unchecked, QTreeView::indicator:unchecked, QCheckBox::indicator:unchecked { - image: url(./Parchment/checkbox.png); + image: url(./Parchment/checkbox.png); } QGroupBox::indicator:unchecked:hover, QTreeView::indicator:unchecked:hover, QCheckBox::indicator:unchecked:hover { - image: url(./Parchment/checkbox-hover.png); + image: url(./Parchment/checkbox-hover.png); } QGroupBox::indicator:unchecked:disabled, QTreeView::indicator:unchecked:disabled, QCheckBox::indicator:unchecked:disabled { - image: url(./Parchment/checkbox-disabled.png); + image: url(./Parchment/checkbox-disabled.png); } /* Search Boxes */ @@ -323,7 +323,7 @@ QScrollBar::add-line:horizontal { margin: 0px -2px -2px 0px; } -QScrollBar::sub-line:horizontal { +QScrollBar::sub-line:horizontal { background: #F7F6CF; width: 23px; subcontrol-position: left; @@ -359,7 +359,7 @@ QScrollBar::add-line:vertical { margin: 0px -2px -2px 0px; } -QScrollBar::sub-line:vertical { +QScrollBar::sub-line:vertical { background: #F7F6CF; height: 23px; subcontrol-position: top; @@ -371,37 +371,37 @@ QScrollBar::sub-line:vertical { /* Combined */ -QScrollBar::handle:horizontal:hover, +QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:hover, -QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover { background: #008F8F; } -QScrollBar::handle:horizontal:pressed, +QScrollBar::handle:horizontal:pressed, QScrollBar::handle:vertical:pressed, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { background: #0CA6FF; } -QScrollBar::add-page:horizontal, -QScrollBar::sub-page:horizontal, -QScrollBar::add-page:vertical, +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal, +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; } -QScrollBar::up-arrow:vertical, +QScrollBar::up-arrow:vertical, QScrollBar::right-arrow:horizontal, -QScrollBar::down-arrow:vertical, +QScrollBar::down-arrow:vertical, QScrollBar::left-arrow:horizontal { height: 1px; - width: 1px; + width: 1px; border: 1px solid #DBD399; } @@ -443,6 +443,16 @@ QHeaderView::down-arrow { /* Context Menus, Toolbar Dropdowns, & Tooltips */ +QMenuBar { + background: #DBD399; + border: 1px solid #DBD399; +} + +QMenuBar::item:selected { + background: #008F8F; + color: #F7F6CF; +} + QMenu { background: #F7F6CF; selection-color: #F7F6CF; @@ -465,7 +475,7 @@ QMenu::item:disabled { color: #444444; } -QMenu::separator { +QMenu::separator { background: #DBD399; height: 2px; } @@ -481,9 +491,11 @@ QToolTip { border: 0px; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) */ -QProgressBar { +QProgressBar { background: #F7F6CF; text-align: center; border: 0px; diff --git a/src/stylesheets/Transparent-Style-101-Green.qss b/src/stylesheets/Transparent-Style-101-Green.qss index f7bf16b6..a4ed2623 100644 --- a/src/stylesheets/Transparent-Style-101-Green.qss +++ b/src/stylesheets/Transparent-Style-101-Green.qss @@ -1 +1,492 @@ -#centralWidget { border-image: url(./Transparent-Style/Green/vault-101.png) 0 0 0 0 stretch stretch; } QDialog,Qmenu { border: none; } QWidget { color: #fff; background-color: rgb(0, 204, 0, 5); alternate-background-color: rgb(0, 204, 0, 10); } QAbstractItemView { color: #fff; background-color: rgb(0, 204, 0, 5); alternate-background-color: rgb(0, 204, 0, 10); border: 1px solid #001a00; border-style:outset; border-radius: 3px; selection-color: #001a00; outline: none; } #ProblemsDialog,#TransferSavesDialog,#SettingsDialog,#ProblemsDialog, #ModInfoDialog,#TextViewer,#PyCfgDialog,#InstallDialog { border-image:url(./Transparent-Style/Green/vault-tec-full.png) 0 0 0 0 stretch stretch; } #EditExecutablesDialog { border-image: url(./Transparent-Style/Green/vault-tec.png) 0 0 0 0 stretch stretch; } #SimpleInstallDialog,#LockedDialog,QMenu { border-image: url(./Transparent-Style/Green/vault-tec-small.png) 0 0 0 0 stretch stretch; } #nameLabel { background-color: rgb(0, 0, 0, 20); border-top:1px solid black; border-bottom:1px solid black; } #ProfilesDialog,#FomodInstallerDialog { border-image: url(./Transparent-Style/Green/vault-tec-profiles.png) 0 0 0 0 stretch stretch; } #filesView { border-image: url(./Transparent-Style/Green/vault-tec-overwrite.png) 0 0 0 0 stretch stretch; } /* ******************************************** */ /* Main Navigation Button Bar at top of window */ /* ******************************************** */ QToolBar { border: none; } QToolBar::separator { image: url(./Transparent-Style/Green/Vault-101-vault-boy.png); } QToolButton { border:2px ridge None; border-radius: 6px; margin: 3px; padding-left: 8px; padding-right: 8px; padding-top: 0px; padding-bottom: 2px; } QToolButton:hover { border: 2px ridge #fff; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); } QToolButton:pressed { background: black; } QTabWidget::pane { background-color: transparent; border:transparent; } QTabWidget::tab-bar { alignment: center; } /* **************************************** */ /* Tabs on top of Treeview */ /* **************************************** */ QTabBar { text-transform: uppercase; max-height: 22px; padding-bottom: 5px; } QTabBar::tab { border: none; border-bottom-style: none; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); padding-left: 15px; padding-right: 15px; padding-top: 3px; padding-bottom: 3px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } QTabBar::tab:selected { color: #fff; background-color: #00cc00; } QTabBar::tab:!selected { color: #00cc00; margin-top: 0px; } QTabBar::tab:disabled { color: #bbbbbb; border-bottom-style: none; margin-top: 0px; background-color: #000; } #deactivateESP,#activateESP { border:px #00cc00; background:QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); } QToolButton::menu-indicator:pressed,QToolButton::menu-indicator:open { border:2px ridge #00cc00; image: url(./Transparent-Style/Green/arrow-right.png); } QTabBar QToolButton { border:2px ridge #00cc00; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); padding: 1px; margin: 0; } QTabBar QToolButton::right-arrow { image: url(./Transparent-Style/Green/arrow-right.png); } QTabBar QToolButton::left-arrow { image: url(./Transparent-Style/Green/arrow-left.png); } /* **************************************** */ /* Column names of TreeView */ /* **************************************** */ QTableView { color:#fff; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); } QHeaderView::section { color: #00cc00; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); border-radius: 6px; padding: 4px; padding-left: 5px; padding-right: 0px; } QHeaderView::section:hover { background: #121212; border: 2px solid #ff0000; padding: 0px; } QHeaderView::up-arrow,QHeaderView::down-arrow { subcontrol-origin: content; subcontrol-position: center right; width: 7px; height: 7px; margin-right: 7px; } QHeaderView::up-arrow { image: url(./Transparent-Style/Green/arrow-up.png); } QHeaderView::down-arrow { image: url(./Transparent-Style/Green/arrow-down.png); } /* **************************************** */ /* Hover */ /* **************************************** */ QMenu:item:hover,QTreeView#espList::item:hover,QTreeWidget#categoriesList::item:hover,QListView::item:hover,QTreeWidget::item:hover { color:#fff; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:#000; } QAbstractItemView::item:hover { border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:#000; padding: 3px; } #deactivateESP:hover,#activateESP:hover,QPushButton:hover,QTableView:hover,QHeaderView::selected:hover,QTabBar::tab:!selected:hover,QComboBox:hover { border: 1px solid #ff0000; padding: 1px; text-align: center; } /* **************************************** */ /* Context menus, toolbar dropdowns #QMenu */ /* **************************************** */ QMenu { border-width: 3px; border-style: solid; } QMenu::item { padding: 6px 20px; } QMenu::item:selected { color:#fff; background-color: #001a00; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; } QMenu::right-arrow { image: url(./Transparent-Style/Green/arrow-right.png); subcontrol-origin: padding; subcontrol-position: center right; padding-right: 0.5em; } /* ********************* */ /* LCD Counter */ /* ********************* */ QLCDNumber { Background:#000; border-color: #00cc00; border-style: solid; border-width: 1px; border-radius: 5px; } /* **************************************** */ /* Launch application Drop down Box */ /* **************************************** */ QComboBox { background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); border:1px outset #00cc00; height: 20px; width: 15px; padding: 3px; padding-left:3px; border-radius: 5px; } QComboBox::drop-down{ subcontrol-origin: padding; subcontrol-position: top right; width: 15px; padding:4px; border-radius: 5px; } QComboBox::down-arrow{ image:url(./Transparent-Style/Green/arrow-down.png); } QComboBox::item{ border:3px;padding: 3px; } /* **************************************************************** */ /* Action Buttons */ /* */ /* Open list, Rextore Backup, Create Backup, RUN, Shortcut Button */ /* Sort, Load order backup, Load Order Restore */ /* **************************************************************** */ QPushButton { color: #00cc00; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); border-width: 1px; border-color: 1px outset #00cc00; border-style: solid; border-radius: 4px; padding:5px; padding-right: 10px; padding-left: 5px; } QDialog QPushButton { min-width: 3em; min-height: 1em; padding-left: 1em; padding-right: 1em; } QPushButton::menu-indicator { image: url(./Transparent-Style/Green/arrow-down.png); background: transparent; subcontrol-origin: padding; subcontrol-position: center right; padding-right: 5%; } QPushButton:open { background-color: rgb(0, 204, 0, 100); } QPushButton:!enabled { border: 1px grey; color: grey; background: black; } QPushButton#displayCategoriesBtn { min-width: 20px; } /* **************************************** */ /* Scroll bar */ /* **************************************** */ QScrollBar { height:20px; width:20px; background-color:transparent; } QScrollBar::handle { border: 1px solid #aaa; border-radius: 4px; } QScrollBar::handle:vertical { margin: 1px 4px; min-height:20px; min-width: 10px; background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #33ff33, stop:1 #000000); } QScrollBar::handle:vertical:hover { background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #000000, stop:1 #33ff33); } QScrollBar::handle:horizontal { min-height:10px;min-width:20px;margin:4px 1px; background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #33ff33, stop:1 #000000); } QScrollBar::handle:horizontal:hover { background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #000000, stop:1 #33ff33); } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background-color: transparent; } QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal { height:0; width:0; } /* ******************************************************** */ /* Main Tree View Inherated from QAbstractItemView */ /* ******************************************************** */ QTreeView { } QTreeView:item { padding:4px; } QTreeView:item:selected { color: #001a00; border: none;outline: none; border-radius: 5px; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color: rgb(0, 204, 0, 100); } QTreeView::branch:has-siblings:!adjoins-item { background:url(Vault-101/Vault-101-item-line-v.png) center center no-repeat; } QTreeView::branch:has-siblings { background:url(Vault-101/Vault-101-itemB-line.png) center center no-repeat; } QTreeView::branch:!has-children:!has-siblings:adjoins-item { background:url(Vault-101/Vault-101-itemB-end.png) center center no-repeat; } QTreeView::branch:closed:has-children:has-siblings { background:url(Vault-101/Vault-101-itemB-close.png) center center no-repeat; } QTreeView::branch:closed:has-children:!has-siblings { background:url(Vault-101/Vault-101-itemB-close-last.png) center center no-repeat; } QTreeView::branch:open:has-children:has-siblings { background:url(Vault-101/Vault-101-itemB-open.png) center center no-repeat; } QTreeView::branch:open:has-children:!has-siblings { background:url(Vault-101/Vault-101-itemB-open-last.png) center center no-repeat; } /* **************************************************************** */ /* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */ /* common */ /* **************************************************************** */ QGroupBox::indicator,QTreeView::indicator,QCheckBox::indicator { outline: none; border: none; width: 20px; height: 20px; } QGroupBox::indicator:checked,QGroupBox::indicator:indeterminate,QTreeView::indicator:checked,QTreeView::indicator:indeterminate,QCheckBox::indicator:checked,QCheckBox::indicator:indeterminate { image: url(./Transparent-Style/Green/checkbox-checked.png); } QGroupBox::indicator:checked:hover,QGroupBox::indicator:indeterminate:hover,QTreeView::indicator:checked:hover,QTreeView::indicator:indeterminate:hover,QCheckBox::indicator:checked:hover,QCheckBox::indicator:indeterminate:hover { image: url(./Transparent-Style/Green/checkbox-hover.png); } QGroupBox::indicator:checked:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:checked:disabled,QTreeView::indicator:indeterminate:disabled,QCheckBox::indicator:checked:disabled,QCheckBox::indicator:indeterminate:disabled { image: url(./Transparent-Style/Green/checkbox-checked-disabled.png); } QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QCheckBox::indicator:unchecked { image: url(./Transparent-Style/Green/checkbox.png); } QGroupBox::indicator:unchecked:hover,QTreeView::indicator:unchecked:hover,QCheckBox::indicator:unchecked:hover { image: url(./Transparent-Style/Green/checkbox-checked-hover.png); } QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QCheckBox::indicator:unchecked:disabled { image: url(./Transparent-Style/Green/checkbox-checked-disabled.png); } QTreeWidget#categoriesList::item:has-children { background-image: url(./Transparent-Style/Green/arrow-right.png); } QTreeWidget#categoriesList::item:has-children:open { background-image: url(./Transparent-Style/Green/branch-opened.png); } /* ******************************** */ /* Special styles */ /* increase categories tab width */ /* ******************************** */ QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item { padding: .3em 0; } QTreeWidget#categoriesGroup,QTreeWidget#categoriesList { min-width: 200px; } QTreeWidget#categoriesList::item { background-position: center left; background-repeat: no-repeat; padding: 0.35em 4px; } /* ********************* */ /* Display Radio Button */ /* ********************* */ QRadioButton::indicator { width: 16px; height: 16px; } QRadioButton::indicator::checked { image: url(./Transparent-Style/Green/radio-checked.png); } QRadioButton::indicator::unchecked { image: url(./Transparent-Style/Green/radio.png); } QRadioButton::indicator::unchecked:hover { image: url(./Transparent-Style/Green/radio-hover.png); } /* **************************************** */ /* Spinners #QSpinBox, #QDoubleSpinBox */ /* **************************************** */ QAbstractSpinBox { min-height: 24px; } QAbstractSpinBox::up-button,QAbstractSpinBox::down-button { border-style: solid; border-width: 1px; subcontrol-origin: padding; } QAbstractSpinBox::up-button:hover,QAbstractSpinBox::down-button:hover { background-color: #141414; } QAbstractSpinBox::up-button { subcontrol-position: top right; } QAbstractSpinBox::down-button { subcontrol-position: bottom right; } QAbstractSpinBox::up-arrow { image: url(./Transparent-Style/Green/arrow-up.png); } QAbstractSpinBox::down-arrow { image: url(./Transparent-Style/Green/arrow-down.png); } /* **************************************** */ /* Tooltips #QToolTip, #SaveGameInfoWidget */ /* **************************************** */ QToolTip { background-color:transparent; color:#C0C0C0; padding:0px; border-width:7px; border-style:solid; border-color:transparent; border-image:url(./Transparent-Style/Green/border-image.png) 27 repeat repeat; } SaveGameInfoWidget { background-color:#121212; color:#C0C0C0; } /* **************************** */ /* Handles Web, Nexus info tab */ /* **************************** */ QWebView { background-color: Black; } QLineEdit { font-family: Source Sans Pro; background:#000; border-width: 1px; border-radius:5px; margin: 0px; padding-left:2px; selection-background-color: rgb(0, 204, 0, 10); } /* Font size */ QLabel, QMenu, QPushButton, QTabBar, QTextEdit, QLineEdit, QWebView, QComboBox, QComboBox:editable, QAbstractSpinBox, QGroupBox, QCheckBox, QRadioButton { color: #cccccc; font-family: Source Sans Pro; font-size: 14px; } QAbstractItemView { color: #cccccc; font-family: Source Sans Pro; font-size: 14px; }
\ No newline at end of file +#centralWidget { + border-image: url(./Transparent-Style/Green/vault-101.png) 0 0 0 0 stretch stretch; +} + QDialog,Qmenu { + border: none; +} + QWidget { + color: #fff; + background-color: rgb(0, 204, 0, 5); + alternate-background-color: rgb(0, 204, 0, 10); +} + QAbstractItemView { + color: #fff; + background-color: rgb(0, 204, 0, 5); + alternate-background-color: rgb(0, 204, 0, 10); + border: 1px solid #001a00; + border-style:outset; + border-radius: 3px; + selection-color: #001a00; + outline: none; +} + #ProblemsDialog,#TransferSavesDialog,#SettingsDialog,#ProblemsDialog, #ModInfoDialog,#TextViewer,#PyCfgDialog,#InstallDialog { + border-image:url(./Transparent-Style/Green/vault-tec-full.png) 0 0 0 0 stretch stretch; +} + #EditExecutablesDialog { + border-image: url(./Transparent-Style/Green/vault-tec.png) 0 0 0 0 stretch stretch; +} + #SimpleInstallDialog,#LockedDialog,QMenu { + border-image: url(./Transparent-Style/Green/vault-tec-small.png) 0 0 0 0 stretch stretch; +} + #nameLabel { + background-color: rgb(0, 0, 0, 20); + border-top:1px solid black; + border-bottom:1px solid black; +} + #ProfilesDialog,#FomodInstallerDialog { + border-image: url(./Transparent-Style/Green/vault-tec-profiles.png) 0 0 0 0 stretch stretch; +} + #filesView { + border-image: url(./Transparent-Style/Green/vault-tec-overwrite.png) 0 0 0 0 stretch stretch; +} +/* ******************************************** */ +/* Main Navigation Button Bar at top of window */ +/* ******************************************** */ + QToolBar { + border: none; +} + QToolBar::separator { + image: url(./Transparent-Style/Green/Vault-101-vault-boy.png); +} + QToolButton { + border:2px ridge None; + border-radius: 6px; + margin: 3px; + padding-left: 8px; + padding-right: 8px; + padding-top: 0px; + padding-bottom: 2px; +} + QToolButton:hover { + border: 2px ridge #fff; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); +} + QToolButton:pressed { + background: black; +} + QTabWidget::pane { + background-color: transparent; + border:transparent; +} + QTabWidget::tab-bar { + alignment: center; +} +/* **************************************** */ +/* Tabs on top of Treeview */ +/* **************************************** */ + QTabBar { + text-transform: uppercase; + max-height: 22px; + padding-bottom: 5px; +} + QTabBar::tab { + border: none; + border-bottom-style: none; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + padding-left: 15px; + padding-right: 15px; + padding-top: 3px; + padding-bottom: 3px; + margin-right: 2px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + QTabBar::tab:selected { + color: #fff; + background-color: #00cc00; +} + QTabBar::tab:!selected { + color: #00cc00; + margin-top: 0px; +} + QTabBar::tab:disabled { + color: #bbbbbb; + border-bottom-style: none; + margin-top: 0px; + background-color: #000; +} + #deactivateESP,#activateESP { + border:px #00cc00; + background:QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); +} + QToolButton::menu-indicator:pressed,QToolButton::menu-indicator:open { + border:2px ridge #00cc00; + image: url(./Transparent-Style/Green/arrow-right.png); +} + QTabBar QToolButton { + border:2px ridge #00cc00; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + padding: 1px; + margin: 0; +} + QTabBar QToolButton::right-arrow { + image: url(./Transparent-Style/Green/arrow-right.png); +} + QTabBar QToolButton::left-arrow { + image: url(./Transparent-Style/Green/arrow-left.png); +} +/* **************************************** */ +/* Column names of TreeView */ +/* **************************************** */ + QTableView { + color:#fff; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); +} + QHeaderView::section { + color: #00cc00; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + border-radius: 6px; + padding: 4px; + padding-left: 5px; + padding-right: 0px; +} + QHeaderView::section:hover { + background: #121212; + border: 2px solid #ff0000; + padding: 0px; +} + QHeaderView::up-arrow,QHeaderView::down-arrow { + subcontrol-origin: content; + subcontrol-position: center right; + width: 7px; + height: 7px; + margin-right: 7px; +} + QHeaderView::up-arrow { + image: url(./Transparent-Style/Green/arrow-up.png); +} + QHeaderView::down-arrow { + image: url(./Transparent-Style/Green/arrow-down.png); +} +/* **************************************** */ +/* Hover */ +/* **************************************** */ + QMenu:item:hover,QMenuBar:item:selected,QTreeView#espList::item:hover,QTreeWidget#categoriesList::item:hover,QListView::item:hover,QTreeWidget::item:hover { + color:#fff; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:#000; +} + QAbstractItemView::item:hover { + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:#000; + padding: 3px; +} + #deactivateESP:hover,#activateESP:hover,QPushButton:hover,QTableView:hover,QHeaderView::selected:hover,QTabBar::tab:!selected:hover,QComboBox:hover { + border: 1px solid #ff0000; + padding: 1px; + text-align: center; +} +/* **************************************** */ +/* Context menus, toolbar dropdowns #QMenu */ +/* **************************************** */ + QMenu { + border-width: 3px; + border-style: solid; +} + QMenu::item { + padding: 6px 20px; +} + QMenu::item:selected { + color:#fff; + background-color: #001a00; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; +} + QMenu::right-arrow { + image: url(./Transparent-Style/Green/arrow-right.png); + subcontrol-origin: padding; + subcontrol-position: center right; + padding-right: 0.5em; +} +/* ********************* */ +/* LCD Counter */ +/* ********************* */ + QLCDNumber { + Background:#000; + border-color: #00cc00; + border-style: solid; + border-width: 1px; + border-radius: 5px; +} +/* **************************************** */ +/* Launch application Drop down Box */ +/* **************************************** */ + QComboBox { + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + border:1px outset #00cc00; + height: 20px; + width: 15px; + padding: 3px; + padding-left:3px; + border-radius: 5px; +} + QComboBox::drop-down{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + padding:4px; + border-radius: 5px; +} + QComboBox::down-arrow{ + image:url(./Transparent-Style/Green/arrow-down.png); +} + QComboBox::item{ + border:3px; + padding: 3px; +} +/* **************************************************************** */ +/* Action Buttons */ +/* */ +/* Open list, Rextore Backup, Create Backup, RUN, Shortcut Button */ +/* Sort, Load order backup, Load Order Restore */ +/* **************************************************************** */ + QPushButton { + color: #00cc00; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + border-width: 1px; + border-color: 1px outset #00cc00; + border-style: solid; + border-radius: 4px; + padding:5px; + padding-right: 10px; + padding-left: 5px; +} + QDialog QPushButton { + min-width: 3em; + min-height: 1em; + padding-left: 1em; + padding-right: 1em; +} + QPushButton::menu-indicator { + image: url(./Transparent-Style/Green/arrow-down.png); + background: transparent; + subcontrol-origin: padding; + subcontrol-position: center right; + padding-right: 5%; +} + QPushButton:open { + background-color: rgb(0, 204, 0, 100); +} + QPushButton:!enabled { + border: 1px grey; + color: grey; + background: black; +} + QPushButton#displayCategoriesBtn { + min-width: 20px; +} +/* **************************************** */ +/* Scroll bar */ +/* **************************************** */ + QScrollBar { + height:20px; + width:20px; + background-color:transparent; +} + QScrollBar::handle { + border: 1px solid #aaa; + border-radius: 4px; +} + QScrollBar::handle:vertical { + margin: 1px 4px; + min-height:20px; + min-width: 10px; + background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #33ff33, stop:1 #000000); +} + QScrollBar::handle:vertical:hover { + background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #000000, stop:1 #33ff33); +} + QScrollBar::handle:horizontal { + min-height:10px; + min-width:20px; + margin:4px 1px; + background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #33ff33, stop:1 #000000); +} + QScrollBar::handle:horizontal:hover { + background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #000000, stop:1 #33ff33); +} + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background-color: transparent; +} + QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal { + height:0; + width:0; +} +/* ******************************************************** */ +/* Main Tree View Inherated from QAbstractItemView */ +/* ******************************************************** */ + QTreeView { +} + QTreeView:item { + padding:4px; +} + QTreeView:item:selected { + color: #001a00; + border: none; + outline: none; + border-radius: 5px; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color: rgb(0, 204, 0, 100); +} + QTreeView::branch:has-siblings:!adjoins-item { + background:url(Vault-101/Vault-101-item-line-v.png) center center no-repeat; +} + QTreeView::branch:has-siblings { + background:url(Vault-101/Vault-101-itemB-line.png) center center no-repeat; +} + QTreeView::branch:!has-children:!has-siblings:adjoins-item { + background:url(Vault-101/Vault-101-itemB-end.png) center center no-repeat; +} + QTreeView::branch:closed:has-children:has-siblings { + background:url(Vault-101/Vault-101-itemB-close.png) center center no-repeat; +} + QTreeView::branch:closed:has-children:!has-siblings { + background:url(Vault-101/Vault-101-itemB-close-last.png) center center no-repeat; +} + QTreeView::branch:open:has-children:has-siblings { + background:url(Vault-101/Vault-101-itemB-open.png) center center no-repeat; +} + QTreeView::branch:open:has-children:!has-siblings { + background:url(Vault-101/Vault-101-itemB-open-last.png) center center no-repeat; +} +/* **************************************************************** */ +/* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */ +/* common */ +/* **************************************************************** */ + QGroupBox::indicator,QTreeView::indicator,QCheckBox::indicator { + outline: none; + border: none; + width: 20px; + height: 20px; +} + QGroupBox::indicator:checked,QGroupBox::indicator:indeterminate,QTreeView::indicator:checked,QTreeView::indicator:indeterminate,QCheckBox::indicator:checked,QCheckBox::indicator:indeterminate { + image: url(./Transparent-Style/Green/checkbox-checked.png); +} + QGroupBox::indicator:checked:hover,QGroupBox::indicator:indeterminate:hover,QTreeView::indicator:checked:hover,QTreeView::indicator:indeterminate:hover,QCheckBox::indicator:checked:hover,QCheckBox::indicator:indeterminate:hover { + image: url(./Transparent-Style/Green/checkbox-hover.png); +} + QGroupBox::indicator:checked:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:checked:disabled,QTreeView::indicator:indeterminate:disabled,QCheckBox::indicator:checked:disabled,QCheckBox::indicator:indeterminate:disabled { + image: url(./Transparent-Style/Green/checkbox-checked-disabled.png); +} + QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QCheckBox::indicator:unchecked { + image: url(./Transparent-Style/Green/checkbox.png); +} + QGroupBox::indicator:unchecked:hover,QTreeView::indicator:unchecked:hover,QCheckBox::indicator:unchecked:hover { + image: url(./Transparent-Style/Green/checkbox-checked-hover.png); +} + QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QCheckBox::indicator:unchecked:disabled { + image: url(./Transparent-Style/Green/checkbox-checked-disabled.png); +} + QTreeWidget#categoriesList::item:has-children { + background-image: url(./Transparent-Style/Green/arrow-right.png); +} + QTreeWidget#categoriesList::item:has-children:open { + background-image: url(./Transparent-Style/Green/branch-opened.png); +} +/* ******************************** */ +/* Special styles */ +/* increase categories tab width */ +/* ******************************** */ + QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item { + padding: .3em 0; +} + QTreeWidget#categoriesGroup,QTreeWidget#categoriesList { + min-width: 200px; +} + QTreeWidget#categoriesList::item { + background-position: center left; + background-repeat: no-repeat; + padding: 0.35em 4px; +} +/* ********************* */ +/* Display Radio Button */ +/* ********************* */ + QRadioButton::indicator { + width: 16px; + height: 16px; +} + QRadioButton::indicator::checked { + image: url(./Transparent-Style/Green/radio-checked.png); +} + QRadioButton::indicator::unchecked { + image: url(./Transparent-Style/Green/radio.png); +} + QRadioButton::indicator::unchecked:hover { + image: url(./Transparent-Style/Green/radio-hover.png); +} +/* **************************************** */ +/* Spinners #QSpinBox, #QDoubleSpinBox */ +/* **************************************** */ + QAbstractSpinBox { + min-height: 24px; +} + QAbstractSpinBox::up-button,QAbstractSpinBox::down-button { + border-style: solid; + border-width: 1px; + subcontrol-origin: padding; +} + QAbstractSpinBox::up-button:hover,QAbstractSpinBox::down-button:hover { + background-color: #141414; +} + QAbstractSpinBox::up-button { + subcontrol-position: top right; +} + QAbstractSpinBox::down-button { + subcontrol-position: bottom right; +} + QAbstractSpinBox::up-arrow { + image: url(./Transparent-Style/Green/arrow-up.png); +} + QAbstractSpinBox::down-arrow { + image: url(./Transparent-Style/Green/arrow-down.png); +} +/* **************************************** */ +/* Tooltips #QToolTip, #SaveGameInfoWidget */ +/* **************************************** */ + QToolTip { + background-color:transparent; + color:#C0C0C0; + padding:0px; + border-width:7px; + border-style:solid; + border-color:transparent; + border-image:url(./Transparent-Style/Green/border-image.png) 27 repeat repeat; +} + SaveGameInfoWidget { + background-color:#121212; + color:#C0C0C0; +} + +QStatusBar::item {border: None;} + +/* **************************** */ +/* Handles Web, Nexus info tab */ +/* **************************** */ + QWebView { + background-color: Black; +} + QLineEdit { + font-family: Source Sans Pro; + background:#000; + border-width: 1px; + border-radius:5px; + margin: 0px; + padding-left:2px; + selection-background-color: rgb(0, 204, 0, 10); +} +/* Font size */ + QLabel, QMenu, QPushButton, QTabBar, QTextEdit, QLineEdit, QWebView, QComboBox, QComboBox:editable, QAbstractSpinBox, QGroupBox, QCheckBox, QRadioButton { + color: #cccccc; + font-family: Source Sans Pro; + font-size: 14px; +} + QAbstractItemView { + color: #cccccc; + font-family: Source Sans Pro; + font-size: 14px; +} diff --git a/src/stylesheets/Transparent-Style-BOS.qss b/src/stylesheets/Transparent-Style-BOS.qss index cda40e9f..efad0859 100644 --- a/src/stylesheets/Transparent-Style-BOS.qss +++ b/src/stylesheets/Transparent-Style-BOS.qss @@ -1 +1,745 @@ -#centralWidget { border-image: url(./Transparent-Style/Fallout-BOS.png) 0 0 0 0 stretch stretch; } QWidget#TextViewer{ } QWidget{ color: #fff; background-color: rgba(45,45,48,30); alternate-background-color: rgba(38,38,38,30); } QWidget:disabled{ } QAbstractItemView{ color: #dcdcdc; background-color: rgb(45,45,48,30); alternate-background-color: rgb(38,38,38,50); border: 1px solid #3F3F46; /* Grey - Blue*/ border-style:outset; border-radius: 3px; selection-color: #3F3F46; /* Grey - Blue*/ outline: none; padding-left:0; } #problemsWidget,#TransferSavesDialog,#SettingsDialog,#ProblemsDialog,#ModInfoDialog,#TextViewer,#PyCfgDialog,#InstallDialog { border-image:url(./Transparent-Style/Vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #toolBarArea{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #EditExecutablesDialog{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #SimpleInstallDialog,#LockedDialog,QMenu{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #nameLabel{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:rgba(154,154,0,0.3); color:#fff; } #ProfilesDialog,#FomodInstallerDialog{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #filesView{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } /* ******************************************** */ /* Main Navigation Button Bar at top of window */ /* ******************************************** */ QToolBar{ /*border: none;*/ } QToolBar::separator{ /*image: url(./Transparent-Style/Vault-101-vault-logo.png);*/ } QToolButton{ border:2px solid None; border-radius: 6px; margin: 3px; padding-left: 8px; padding-right: 8px; padding-top: 0px; padding-bottom: 2px; } QToolButton:hover{ border-radius: 3px; border: 0px; padding: 0px; background: trANSPARENT; border: 0px ridge #9A9A00; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #9A9A00, stop: 1 #484F53); } QToolButton:pressed{ background: black; } /* **************************************** */ /* Tabs on top of Treeview */ /* **************************************** */ QTabWidget::pane{ border-color:#3F3F46; /* Grey - Blue*/ /* border-top-color:#9A9A00; */ top: 0px; border-style:hidden; border-width:1px; } QTabWidget::tab-bar{ alignment: center; } QTabBar{ color: #cccccc; font-family: Segoe UI; font-size: 16px; text-transform: none; min-height:auto; padding-bottom: 5px; } QTabBar::tab{ border: none; border-bottom-style: none; background-color: rgba(154,154,0,0.3); padding-left: 9px; padding-right: 9px; padding-top: 3px; padding-bottom: 3px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } QTabBar::tab:disabled{ color: #404040; background-color: #000; margin-top: 0px; } QTabBar::tab:selected{ color: #dcdcdc; background-color: #9A9A00; /*Yellow opaque*/ } QTabBar::tab:!selected:hover{ border-radius: 3px; border-top:3px double #9A9A00; border-bottom:3px double #9A9A00; padding: 1px; text-align: center; background-color: #9A9A00; color:#fff; Height: 20%; } /* QTabBar::tab:!selected{ color: #808080; background-color: #404040; margin-top: 0px; } */ #deactivateESP,#activateESP{ border:#b30000; background:#333337; } /* **************************************** */ /* <More View> on top of Treeview */ /* **************************************** */ QTabBar QToolButton{ border-radius: 0px; border: 0px; padding: 0px; background: transparent; } QToolButton::menu-indicator:pressed,QToolButton::menu-indicator:open{ image: url(./Transparent-Style/arrow-up.png); } QToolButton::menu-indicator:!pressed,QToolButton::menu-indicator:!open{ image: url(./Transparent-Style/arrow-down.png); } QTabBar QToolButton::right-arrow{ image: url(./Transparent-Style/narrow-arrow-right.png); } QTabBar QToolButton::left-arrow{ image: url(./Transparent-Style/narrow-arrow-left.png); } /* **************************************** */ /* Column names of TreeView */ /* **************************************** */ QTableView{ gridline-color:#3F3F46; selection-background-color:#9A9A00; selection-color:#F1F1F1; /* White */ text-align: center; min-height:24px; } QHeaderView{ min-height:24px; /*Top Bar on menus*/ padding: 3px; } QHeaderView::section{ /* color: #cc3333; */ background-color: #252526; border-radius: 0px; padding: 0px; padding-left: 5px; } QHeaderView::section:hover{ background: rgba(154,154,0,0.3); border: 1px solid #9a9a00; padding: 0px; padding-left: 5px; } QHeaderView::up-arrow,QHeaderView::down-arrow{ min-height:Auto; min-width:Auto; } QHeaderView::up-arrow{ image: url(./Transparent-Style/arrow-up.png); min-height:25px; min-width:15px; padding-right: 0px; }QHeaderView::down-arrow{ image: url(./Transparent-Style/arrow-down.png); } /* **************************************** */ /* Hover */ /* **************************************** */ QMenu:item:hover,QTreeView#espList::item:hover,QTreeWidget#categoriesList::item:hover,QListView::item:hover,QTreeWidget::item:hover{ color:#fff; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:rgba(154,154,0,0.3); } QAbstractItemView::item:hover{ border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:rgba(154,154,0,0.3); padding: 3px; } /*#DownloadListWidget{ }*/ #DownloadListWidget{ outline: 1px solid #e8e8e8; border: 0px solid #9a9a00; } #deactivateESP:hover,#activateESP:hover,QTableView:hover,QHeaderView::selected:hover{ border: 1px solid #9a9a00; padding: 1px; text-align: center; color:#fff; } /* **************************************** */ /* Context menus, toolbar dropdowns #QMenu */ /* **************************************** */ QMenu{ border-width: 3px; border-style: solid; }QMenu::item{ padding: 6px 20px; }QMenu::item:selected{ color:#fff; background-color: #3F3F46; /* Grey - Blue*/ border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QMenu::right-arrow{ image: url(./Transparent-Style/arrow-right.png); /* right click main menu top option*/ subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; border:0px outset #3F3F46; /* Grey - Blue*/ } /* ********************* */ /* LCD Counter */ /* ********************* */ QLCDNumber{ color: #9A9A00; Background: #000; border-color: #3F3F46; /* Grey - Blue*/ border-style: solid; border-width: 1px; border-radius: 5px; } /* **************************************** */ /* Launch application Drop down Box */ /* **************************************** */ QComboBox{ background-color: #333337; padding: 3px 5px 5px 5px; height: 26px; border-radius: 5px; border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QComboBox:on{ padding: 3px 5px 5px 5px; color: white; background-color: #3F3F46; /* Grey - Blue*/ } QComboBox:hover{ border-radius: 3px; border-top:3px double #9A9A00; border-bottom:3px double #9A9A00; /* padding: 0px; */ } QComboBox::drop-down{ /* Down arrow in combo box */ outline: none; border: none; border-width: 1px; } QComboBox::menu-indicator{ image: url(./Transparent-Style/arrow-down.png); subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; border:1px outset #3F3F46; /* Grey - Blue*/ } /* ************************* */ /* QComboBox::down-arrow:on, */ /* ************************* */ QComboBox::down-arrow:hover,QComboBox::down-arrow:focus { image: url(./Transparent-Style/arrow-down-hover.png); } QComboBox::up-arrow:hover,QComboBox::up-arrow:focus { image: url(./Transparent-Style/arrow-up-hover.png); } QComboBox::down-arrow{ image:url(./Transparent-Style/arrow-down.png); subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; } QComboBox::down-arrow:on{ image:url(./Transparent-Style/arrow-up.png); } QComboBox:editable { background: black; /* color: pink; */ } QComboBox::item{ } QComboBox QAbstractItemView{ selection-color:#fff; outline: 1px solid #9a9a00; selection-background-color:#3F3F46; /* Grey - Blue*/ border-top:1px solid #9a9a00; border-bottom:1px solid #9a9a00; padding: 5px; } /* **************************************************************** */ /* Action Buttons */ /* */ /* Open list, Restore Backup, Create Backup, RUN, Shortcut Button */ /* Sort, Load order backup, Load Order Restore */ /* **************************************************************** */ QPushButton{ background-color: #333337; min-height:26px; padding:1px 5%; text-align: center; border-radius: 5px; border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QPushButton::menu-indicator{ image: url(./Transparent-Style/arrow-down.png); subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; border:1px hidden #3F3F46; /* Grey - Blue*/ } QPushButton::menu-indicator:Hover{ image: url(./Transparent-Style/arrow-down-hover.png); } QPushButton:focus{ } QPushButton:on,QPushButton:checked,QPushButton:pressed,QPushButton:checked:hover{ background: #3F3F46; /* Grey - Blue*/ border-radius: 1px; border: 1px solid #9a9a00; } QPushButton:hover{ border-radius: 3px; border-top:3px double #9A9A00; border-bottom:3px double #9A9A00; } QDialog QPushButton,QSlider::handle:horizontal,QSlider::handle:vertical{ color:#fff; border-color:#707070; border-style:solid; border-width:1px; } QPushButton:disabled{ } QPushButton:open{ } QPushButton:!enabled{ } /* **************************************** */ /* Filter Button */ /* **************************************** */ QPushButton#displayCategoriesBtn{ min-width: 20px; background:transparent; color: transparent; border: 1px hidden #9a9a00; } QPushButton#displayCategoriesBtn:checked{ image: url(./Transparent-Style/arrow-left.png); } QPushButton#displayCategoriesBtn:!checked{ image: url(./Transparent-Style/arrow-right.png); } QPushButton#displayCategoriesBtn:checked:hover{ image: url(./Transparent-Style/arrow-left-hover.png); } QPushButton#displayCategoriesBtn:!checked:hover{ image: url(./Transparent-Style/arrow-right-hover.png); } /* **************************************** */ /* Scroll bar */ /* **************************************** */ QScrollBar{ height:20px; width:20px; background-color:transparent; } QScrollBar::handle{ border: 1px solid #aaa; border-radius: 4px; } QScrollBar::handle:vertical{ margin: 1px 4px; min-height:20px; min-width: 10px; background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #9A9A00, stop:1 #000000); } QScrollBar::handle:vertical:hover{ background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #000000, stop:1 #9A9A00); } QScrollBar::handle:horizontal{ min-height:10px;min-width:20px;margin:4px 1px; background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #9A9A00, stop:1 #000000); } QScrollBar::handle:horizontal:hover{ background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #000000, stop:1 #9A9A00); } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal{ background-color: transparent; } QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal{ height:0; width:0; } /* ******************************************************** */ /* Main Tree View Inherited from QAbstractItemView */ /* ******************************************************** */ QTreeView { } QTreeView:item{ padding: 3px; font-size: 16px; height: 18px; /*Bottom text Box*/ } QTreeView:item:selected{ color:#fff; padding: 0px; background-color: #3F3F46; /* Grey - Blue*/ border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QTreeView:item QLineEdit{ outline: none; border: none; padding: 0px; margin: 0px; } QTreeView::branch:has-siblings:!adjoins-item{ background:url(Transparent-Style/vault-101-item-line-v.png) center center no-repeat; } QTreeView::branch:has-siblings{ background:url(Transparent-Style/vault-101-itemB-line.png) center center no-repeat; } QTreeView::branch:!has-children:!has-siblings:adjoins-item{ background:url(Transparent-Style/vault-101-itemB-end.png) center center no-repeat; } QTreeView::branch:closed:has-children:has-siblings{ background:url(Transparent-Style/vault-101-itemB-close.png) center center no-repeat; } QTreeView::branch:closed:has-children:!has-siblings{ background:url(Transparent-Style/vault-101-itemB-close-last.png) center center no-repeat; } QTreeView::branch:open:has-children:has-siblings{ background:url(Transparent-Style/vault-101-itemB-open.png) center center no-repeat; } QTreeView::branch:open:has-children:!has-siblings{ background:url(Transparent-Style/vault-101-itemB-open-last.png) center center no-repeat; } /* **************************************************************** */ /* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */ /* common */ /* **************************************************************** */ QTreeView::indicator,QCheckBox::indicator{ outline: none; border: none; padding: 0px; width: 24px; height: 24px; } QGroupBox::indicator:checked,QGroupBox::indicator:indeterminate,QTreeView::indicator:checked,QTreeView::indicator:indeterminate,QCheckBox::indicator:checked,QCheckBox::indicator:indeterminate{ image: url(./Transparent-Style/checkbox-checked.png); } QGroupBox::indicator:checked:hover,QGroupBox::indicator:indeterminate:hover,QTreeView::indicator:checked:hover,QTreeView::indicator:indeterminate:hover,QCheckBox::indicator:checked:hover,QCheckBox::indicator:indeterminate:hover{ image: url(./Transparent-Style/checkbox-hover.png); } QGroupBox::indicator:checked:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:checked:disabled,QTreeView::indicator:indeterminate:disabled,QCheckBox::indicator:checked:disabled,QCheckBox::indicator:indeterminate:disabled{ image: url(./Transparent-Style/checkbox-checked-disabled.png); } QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QCheckBox::indicator:unchecked{ image: url(./Transparent-Style/checkbox.png); } QGroupBox::indicator:unchecked:hover,QTreeView::indicator:unchecked:hover,QCheckBox::indicator:unchecked:hover{ image: url(./Transparent-Style/checkbox-checked-hover.png); } QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QCheckBox::indicator:unchecked:disabled{ image: url(./Transparent-Style/checkbox-checked-disabled.png); } QTreeWidget#categoriesList::item:has-children{ background-image: url(./Transparent-Style/arrow-right-small.png); } QTreeWidget#categoriesList::item:has-children:open{ background-image: url(./Transparent-Style/branch-opened.png); } /* ******************************** */ /* Special styles */ /* increase categories tab width */ /* ******************************** */ QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ padding: .3em 0em; } QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ padding: .3em 0em; } QTreeWidget#categoriesGroup,QTreeWidget#categoriesList{ min-width: 200px; } QTreeWidget#categoriesList::item { background-position: center left; background-repeat: no-repeat; padding: 0.35em 4px; } /* ********************* */ /* Display Radio Button */ /* ********************* */ QRadioButton::indicator{ width: 16px; height: 16px; } QRadioButton::indicator::checked{ image: url(./Transparent-Style/radio-BOS-checked.png); } QRadioButton::indicator::unchecked{ image: url(./Transparent-Style/radio-BOS.png); } QRadioButton::indicator::unchecked:hover{ image: url(./Transparent-Style/radio-BOS-hover.png); } /* **************************************** */ /* Spinners #QSpinBox, #QDoubleSpinBox */ /* **************************************** */ QAbstractSpinBox{ padding: 5px; background-color: transparent; color: #eff0f1; border-radius: 2px; min-width: Auto; } QAbstractSpinBox::up-button,QAbstractSpinBox::down-button{ border-style: solid; border-width: 1px; subcontrol-origin: border; } QAbstractSpinBox::up-button:hover,QAbstractSpinBox::down-button:hover{ background-color: transparent; } QAbstractSpinBox::up-button{ background-color: transparent; subcontrol-origin: border; subcontrol-position: center right; padding-right: 5px; /* subcontrol-position: top right; */ } QAbstractSpinBox::down-button{ background-color: transparent; subcontrol-origin: border; subcontrol-position: center left; padding-left: 5px; /* subcontrol-position: bottom right; */ } QAbstractSpinBox::up-arrow{ border-image: url(./Transparent-Style/narrow-arrow-right.png); }QAbstractSpinBox::down-arrow{ border-image: url(./Transparent-Style/narrow-arrow-left.png); }/* QTextEdit,QWebView,QLineEdit,QComboBox{ border-color:#3F3F46; /* Grey - Blue*/ /*}*/ /* **************************************** */ /* Tooltips #QToolTip, #SaveGameInfoWidget */ /* **************************************** */ QToolTip{ background-color:#121212; color:#C0C0C0; padding:0px; border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } SaveGameInfoWidget{ font-size: 16px; background-color:#121212; color:#C0C0C0; padding:0px; border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } /* **************************** */ /* Handles Web, Nexus info tab */ /* **************************** */ QWebView{ background-color: Black; } QLineEdit{ font-family: Segoe UI; min-height: 20px; font-size: 16px; background-color:#3F3F46; selection-background-color: rgba(154,154,0,0.6);border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } QTextEdit{ color: #cccccc; font-family: Segoe UI; font-size: 16px; /* background-color:#000; */ selection-background-color: rgba(154,154,0,0.6); border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } /* Font size */ QLabel,QMenu,QPushButton,QAbstractSpinBox,QGroupBox,QCheckBox,QRadioButton{ color: #cccccc; font-family: Segoe UI; font-size: 16px; /* was 14px Profile font size */ } QAbstractItemView{ color: #cccccc; font-family: Segoe UI; /* font-family: Source Sans Pro; */ font-size: 16px; /* Body font size*/ } QRadioButton{ color: #cccccc; font-family: Segoe UI; /* font-family: Source Sans Pro; */ font-size: 16px; } QWebView,QComboBox,QComboBox:editable{ font-family:Segoe UI; font-style: normal; font-weight: normal; font-size: 16px; background-color:#000; /* border-color:#3F3F46; /* Grey - Blue*/ color: #cccccc; font-family: Arial; font-size: 16px; /* Filter Name*/ } QHeaderView::section{ /* font-family: Source Sans Pro; */ font-family: Segoe UI; font-size: 16px; /*Top Bar on menus font*/ } QListView::item{ color:#F1F1F1; /* White */ } QGroupBox { } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; } QGroupBox::indicator { subcontrol-origin: margin; subcontrol-position: top center; } QLabel { color: #cccccc; /* color: #F1F1F1; /* White */ /* border: none;outline: none; border-radius: 5px; border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00;*/ }
\ No newline at end of file +#centralWidget { + border-image: url(./Transparent-Style/Fallout-BOS.png) 0 0 0 0 stretch stretch; +} + QWidget#TextViewer{ +} + QWidget{ + color: #fff; + background-color: rgba(45,45,48,30); + alternate-background-color: rgba(38,38,38,30); +} + QWidget:disabled{ +} + QAbstractItemView{ + color: #dcdcdc; + background-color: rgb(45,45,48,30); + alternate-background-color: rgb(38,38,38,50); + border: 1px solid #3F3F46; + /* Grey - Blue*/ + border-style:outset; + border-radius: 3px; + selection-color: #3F3F46; + /* Grey - Blue*/ + outline: none; + padding-left:0; +} + #problemsWidget,#TransferSavesDialog,#SettingsDialog,#ProblemsDialog,#ModInfoDialog,#TextViewer,#PyCfgDialog,#InstallDialog { + border-image:url(./Transparent-Style/Vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #toolBarArea{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #EditExecutablesDialog{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #SimpleInstallDialog,#LockedDialog,QMenu{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #nameLabel{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:rgba(154,154,0,0.3); + color:#fff; +} + #ProfilesDialog,#FomodInstallerDialog{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #filesView{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} +/* ******************************************** */ +/* Main Navigation Button Bar at top of window */ +/* ******************************************** */ + QToolBar{ + /*border: none; + */ +} + QToolBar::separator{ + /*image: url(./Transparent-Style/Vault-101-vault-logo.png); + */ +} + QToolButton{ + border:2px solid None; + border-radius: 6px; + margin: 3px; + padding-left: 8px; + padding-right: 8px; + padding-top: 0px; + padding-bottom: 2px; +} + QToolButton:hover{ + border-radius: 3px; + border: 0px; + padding: 0px; + background: trANSPARENT; + border: 0px ridge #9A9A00; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #9A9A00, stop: 1 #484F53); +} + QToolButton:pressed{ + background: black; +} +/* **************************************** */ +/* Tabs on top of Treeview */ +/* **************************************** */ + QTabWidget::pane{ + border-color:#3F3F46; + /* Grey - Blue*/ + /* border-top-color:#9A9A00; + */ + top: 0px; + border-style:hidden; + border-width:1px; +} + QTabWidget::tab-bar{ + alignment: center; +} + QTabBar{ + color: #cccccc; + font-family: Segoe UI; + font-size: 16px; + text-transform: none; + min-height:auto; + padding-bottom: 5px; +} + QTabBar::tab{ + border: none; + border-bottom-style: none; + background-color: rgba(154,154,0,0.3); + padding-left: 9px; + padding-right: 9px; + padding-top: 3px; + padding-bottom: 3px; + margin-right: 2px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + QTabBar::tab:disabled{ + color: #404040; + background-color: #000; + margin-top: 0px; +} + QTabBar::tab:selected{ + color: #dcdcdc; + background-color: #9A9A00; + /*Yellow opaque*/ +} + QTabBar::tab:!selected:hover{ + border-radius: 3px; + border-top:3px double #9A9A00; + border-bottom:3px double #9A9A00; + padding: 1px; + text-align: center; + background-color: #9A9A00; + color:#fff; + Height: 20%; +} +/* QTabBar::tab:!selected{ + color: #808080; + background-color: #404040; + margin-top: 0px; +} + */ + #deactivateESP,#activateESP{ + border:#b30000; + background:#333337; +} +/* **************************************** */ +/* <More View> on top of Treeview */ +/* **************************************** */ + QTabBar QToolButton{ + border-radius: 0px; + border: 0px; + padding: 0px; + background: transparent; +} + QToolButton::menu-indicator:pressed,QToolButton::menu-indicator:open{ + image: url(./Transparent-Style/arrow-up.png); +} + QToolButton::menu-indicator:!pressed,QToolButton::menu-indicator:!open{ + image: url(./Transparent-Style/arrow-down.png); +} + QTabBar QToolButton::right-arrow{ + image: url(./Transparent-Style/narrow-arrow-right.png); +} + QTabBar QToolButton::left-arrow{ + image: url(./Transparent-Style/narrow-arrow-left.png); +} +/* **************************************** */ +/* Column names of TreeView */ +/* **************************************** */ + QTableView{ + gridline-color:#3F3F46; + selection-background-color:#9A9A00; + selection-color:#F1F1F1; + /* White */ + text-align: center; + min-height:24px; +} + QHeaderView{ + min-height:24px; + /*Top Bar on menus*/ + padding: 3px; +} + QHeaderView::section{ + /* color: #cc3333; + */ + background-color: #252526; + border-radius: 0px; + padding: 0px; + padding-left: 5px; +} + QHeaderView::section:hover{ + background: rgba(154,154,0,0.3); + border: 1px solid #9a9a00; + padding: 0px; + padding-left: 5px; +} + QHeaderView::up-arrow,QHeaderView::down-arrow{ + min-height:Auto; + min-width:Auto; +} + QHeaderView::up-arrow{ + image: url(./Transparent-Style/arrow-up.png); + min-height:25px; + min-width:15px; + padding-right: 0px; +} +QHeaderView::down-arrow{ + image: url(./Transparent-Style/arrow-down.png); +} +/* **************************************** */ +/* Hover */ +/* **************************************** */ + QMenu:item:hover,QMenuBar::item:selected,QTreeView#espList::item:hover,QTreeWidget#categoriesList::item:hover,QListView::item:hover,QTreeWidget::item:hover{ + color:#fff; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:rgba(154,154,0,0.3); +} + QAbstractItemView::item:hover{ + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:rgba(154,154,0,0.3); + padding: 3px; +} +/*#DownloadListWidget{ +} +*/ + #DownloadListWidget{ + outline: 1px solid #e8e8e8; + border: 0px solid #9a9a00; +} + #deactivateESP:hover,#activateESP:hover,QTableView:hover,QHeaderView::selected:hover{ + border: 1px solid #9a9a00; + padding: 1px; + text-align: center; + color:#fff; +} +/* **************************************** */ +/* Context menus, toolbar dropdowns #QMenu */ +/* **************************************** */ + QMenu{ + border-width: 3px; + border-style: solid; +} +QMenu::item{ + padding: 6px 20px; +} +QMenu::item:selected{ + color:#fff; + background-color: #3F3F46; + /* Grey - Blue*/ + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QMenu::right-arrow{ + image: url(./Transparent-Style/arrow-right.png); + /* right click main menu top option*/ + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; + border:0px outset #3F3F46; + /* Grey - Blue*/ +} +/* ********************* */ +/* LCD Counter */ +/* ********************* */ + QLCDNumber{ + color: #9A9A00; + Background: #000; + border-color: #3F3F46; + /* Grey - Blue*/ + border-style: solid; + border-width: 1px; + border-radius: 5px; +} +/* **************************************** */ +/* Launch application Drop down Box */ +/* **************************************** */ + QComboBox{ + background-color: #333337; + padding: 3px 5px 5px 5px; + height: 26px; + border-radius: 5px; + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QComboBox:on{ + padding: 3px 5px 5px 5px; + color: white; + background-color: #3F3F46; + /* Grey - Blue*/ +} + QComboBox:hover{ + border-radius: 3px; + border-top:3px double #9A9A00; + border-bottom:3px double #9A9A00; + /* padding: 0px; + */ +} + QComboBox::drop-down{ + /* Down arrow in combo box */ + outline: none; + border: none; + border-width: 1px; +} + QComboBox::menu-indicator{ + image: url(./Transparent-Style/arrow-down.png); + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; + border:1px outset #3F3F46; + /* Grey - Blue*/ +} +/* ************************* */ +/* QComboBox::down-arrow:on, */ +/* ************************* */ + QComboBox::down-arrow:hover,QComboBox::down-arrow:focus { + image: url(./Transparent-Style/arrow-down-hover.png); +} + QComboBox::up-arrow:hover,QComboBox::up-arrow:focus { + image: url(./Transparent-Style/arrow-up-hover.png); +} + QComboBox::down-arrow{ + image:url(./Transparent-Style/arrow-down.png); + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; +} + QComboBox::down-arrow:on{ + image:url(./Transparent-Style/arrow-up.png); +} + QComboBox:editable { + background: black; + /* color: pink; + */ +} + QComboBox::item{ +} + QComboBox QAbstractItemView{ + selection-color:#fff; + outline: 1px solid #9a9a00; + selection-background-color:#3F3F46; + /* Grey - Blue*/ + border-top:1px solid #9a9a00; + border-bottom:1px solid #9a9a00; + padding: 5px; +} +/* **************************************************************** */ +/* Action Buttons */ +/* */ +/* Open list, Restore Backup, Create Backup, RUN, Shortcut Button */ +/* Sort, Load order backup, Load Order Restore */ +/* **************************************************************** */ + QPushButton{ + background-color: #333337; + min-height:26px; + padding:1px 5%; + text-align: center; + border-radius: 5px; + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QPushButton::menu-indicator{ + image: url(./Transparent-Style/arrow-down.png); + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; + border:1px hidden #3F3F46; + /* Grey - Blue*/ +} + QPushButton::menu-indicator:Hover{ + image: url(./Transparent-Style/arrow-down-hover.png); +} + QPushButton:focus{ +} + QPushButton:on,QPushButton:checked,QPushButton:pressed,QPushButton:checked:hover{ + background: #3F3F46; + /* Grey - Blue*/ + border-radius: 1px; + border: 1px solid #9a9a00; +} + QPushButton:hover{ + border-radius: 3px; + border-top:3px double #9A9A00; + border-bottom:3px double #9A9A00; +} + QDialog QPushButton,QSlider::handle:horizontal,QSlider::handle:vertical{ + color:#fff; + border-color:#707070; + border-style:solid; + border-width:1px; +} + QPushButton:disabled{ +} + QPushButton:open{ +} + QPushButton:!enabled{ +} +/* **************************************** */ +/* Filter Button */ +/* **************************************** */ + QPushButton#displayCategoriesBtn{ + min-width: 20px; + background:transparent; + color: transparent; + border: 1px hidden #9a9a00; +} + QPushButton#displayCategoriesBtn:checked{ + image: url(./Transparent-Style/arrow-left.png); +} + QPushButton#displayCategoriesBtn:!checked{ + image: url(./Transparent-Style/arrow-right.png); +} + QPushButton#displayCategoriesBtn:checked:hover{ + image: url(./Transparent-Style/arrow-left-hover.png); +} + QPushButton#displayCategoriesBtn:!checked:hover{ + image: url(./Transparent-Style/arrow-right-hover.png); +} +/* **************************************** */ +/* Scroll bar */ +/* **************************************** */ + QScrollBar{ + height:20px; + width:20px; + background-color:transparent; +} + QScrollBar::handle{ + border: 1px solid #aaa; + border-radius: 4px; +} + QScrollBar::handle:vertical{ + margin: 1px 4px; + min-height:20px; + min-width: 10px; + background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #9A9A00, stop:1 #000000); +} + QScrollBar::handle:vertical:hover{ + background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #000000, stop:1 #9A9A00); +} + QScrollBar::handle:horizontal{ + min-height:10px; + min-width:20px; + margin:4px 1px; + background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #9A9A00, stop:1 #000000); +} + QScrollBar::handle:horizontal:hover{ + background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #000000, stop:1 #9A9A00); +} + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal{ + background-color: transparent; +} + QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal{ + height:0; + width:0; +} +/* ******************************************************** */ +/* Main Tree View Inherited from QAbstractItemView */ +/* ******************************************************** */ + QTreeView { +} + QTreeView:item{ + padding: 3px; + font-size: 16px; + height: 18px; + /*Bottom text Box*/ +} + QTreeView:item:selected{ + color:#fff; + padding: 0px; + background-color: #3F3F46; + /* Grey - Blue*/ + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QTreeView:item QLineEdit{ + outline: none; + border: none; + padding: 0px; + margin: 0px; +} + QTreeView::branch:has-siblings:!adjoins-item{ + background:url(Transparent-Style/vault-101-item-line-v.png) center center no-repeat; +} + QTreeView::branch:has-siblings{ + background:url(Transparent-Style/vault-101-itemB-line.png) center center no-repeat; +} + QTreeView::branch:!has-children:!has-siblings:adjoins-item{ + background:url(Transparent-Style/vault-101-itemB-end.png) center center no-repeat; +} + QTreeView::branch:closed:has-children:has-siblings{ + background:url(Transparent-Style/vault-101-itemB-close.png) center center no-repeat; +} + QTreeView::branch:closed:has-children:!has-siblings{ + background:url(Transparent-Style/vault-101-itemB-close-last.png) center center no-repeat; +} + QTreeView::branch:open:has-children:has-siblings{ + background:url(Transparent-Style/vault-101-itemB-open.png) center center no-repeat; +} + QTreeView::branch:open:has-children:!has-siblings{ + background:url(Transparent-Style/vault-101-itemB-open-last.png) center center no-repeat; +} +/* **************************************************************** */ +/* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */ +/* common */ +/* **************************************************************** */ + QTreeView::indicator,QCheckBox::indicator{ + outline: none; + border: none; + padding: 0px; + width: 24px; + height: 24px; +} + QGroupBox::indicator:checked,QGroupBox::indicator:indeterminate,QTreeView::indicator:checked,QTreeView::indicator:indeterminate,QCheckBox::indicator:checked,QCheckBox::indicator:indeterminate{ + image: url(./Transparent-Style/checkbox-checked.png); +} + QGroupBox::indicator:checked:hover,QGroupBox::indicator:indeterminate:hover,QTreeView::indicator:checked:hover,QTreeView::indicator:indeterminate:hover,QCheckBox::indicator:checked:hover,QCheckBox::indicator:indeterminate:hover{ + image: url(./Transparent-Style/checkbox-hover.png); +} + QGroupBox::indicator:checked:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:checked:disabled,QTreeView::indicator:indeterminate:disabled,QCheckBox::indicator:checked:disabled,QCheckBox::indicator:indeterminate:disabled{ + image: url(./Transparent-Style/checkbox-checked-disabled.png); +} + QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QCheckBox::indicator:unchecked{ + image: url(./Transparent-Style/checkbox.png); +} + QGroupBox::indicator:unchecked:hover,QTreeView::indicator:unchecked:hover,QCheckBox::indicator:unchecked:hover{ + image: url(./Transparent-Style/checkbox-checked-hover.png); +} + QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QCheckBox::indicator:unchecked:disabled{ + image: url(./Transparent-Style/checkbox-checked-disabled.png); +} + QTreeWidget#categoriesList::item:has-children{ + background-image: url(./Transparent-Style/arrow-right-small.png); +} + QTreeWidget#categoriesList::item:has-children:open{ + background-image: url(./Transparent-Style/branch-opened.png); +} +/* ******************************** */ +/* Special styles */ +/* increase categories tab width */ +/* ******************************** */ + QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ + padding: .3em 0em; +} + QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ + padding: .3em 0em; +} + QTreeWidget#categoriesGroup,QTreeWidget#categoriesList{ + min-width: 200px; +} + QTreeWidget#categoriesList::item { + background-position: center left; + background-repeat: no-repeat; + padding: 0.35em 4px; +} +/* ********************* */ +/* Display Radio Button */ +/* ********************* */ + QRadioButton::indicator{ + width: 16px; + height: 16px; +} + QRadioButton::indicator::checked{ + image: url(./Transparent-Style/radio-BOS-checked.png); +} + QRadioButton::indicator::unchecked{ + image: url(./Transparent-Style/radio-BOS.png); +} + QRadioButton::indicator::unchecked:hover{ + image: url(./Transparent-Style/radio-BOS-hover.png); +} +/* **************************************** */ +/* Spinners #QSpinBox, #QDoubleSpinBox */ +/* **************************************** */ + QAbstractSpinBox{ + padding: 5px; + background-color: transparent; + color: #eff0f1; + border-radius: 2px; + min-width: Auto; +} + QAbstractSpinBox::up-button,QAbstractSpinBox::down-button{ + border-style: solid; + border-width: 1px; + subcontrol-origin: border; +} + QAbstractSpinBox::up-button:hover,QAbstractSpinBox::down-button:hover{ + background-color: transparent; +} + QAbstractSpinBox::up-button{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; + padding-right: 5px; + /* subcontrol-position: top right; + */ +} + QAbstractSpinBox::down-button{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; + padding-left: 5px; + /* subcontrol-position: bottom right; + */ +} + QAbstractSpinBox::up-arrow{ + border-image: url(./Transparent-Style/narrow-arrow-right.png); +} +QAbstractSpinBox::down-arrow{ + border-image: url(./Transparent-Style/narrow-arrow-left.png); +} +/* QTextEdit,QWebView,QLineEdit,QComboBox{ + border-color:#3F3F46; + /* Grey - Blue*/ + /* +} +*/ +/* **************************************** */ +/* Tooltips #QToolTip, #SaveGameInfoWidget */ +/* **************************************** */ + QToolTip{ + background-color:#121212; + color:#C0C0C0; + padding:0px; + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} + SaveGameInfoWidget{ + font-size: 16px; + background-color:#121212; + color:#C0C0C0; + padding:0px; + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} +/* **************************** */ +/* Handles Web, Nexus info tab */ +/* **************************** */ + QWebView{ + background-color: Black; +} + QLineEdit{ + font-family: Segoe UI; + min-height: 20px; + font-size: 16px; + background-color:#3F3F46; + selection-background-color: rgba(154,154,0,0.6); + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} + QTextEdit{ + color: #cccccc; + font-family: Segoe UI; + font-size: 16px; + /* background-color:#000; + */ + selection-background-color: rgba(154,154,0,0.6); + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} + +QStatusBar::item {border: None;} + +/* Font size */ + QLabel,QMenu,QPushButton,QAbstractSpinBox,QGroupBox,QCheckBox,QRadioButton{ + color: #cccccc; + font-family: Segoe UI; + font-size: 16px; + /* was 14px Profile font size */ +} + QAbstractItemView{ + color: #cccccc; + font-family: Segoe UI; + /* font-family: Source Sans Pro; + */ + font-size: 16px; + /* Body font size*/ +} + QRadioButton{ + color: #cccccc; + font-family: Segoe UI; + /* font-family: Source Sans Pro; + */ + font-size: 16px; +} + QWebView,QComboBox,QComboBox:editable{ + font-family:Segoe UI; + font-style: normal; + font-weight: normal; + font-size: 16px; + background-color:#000; + /* border-color:#3F3F46; + /* Grey - Blue*/ + color: #cccccc; + font-family: Arial; + font-size: 16px; + /* Filter Name*/ +} + QHeaderView::section{ + /* font-family: Source Sans Pro; + */ + font-family: Segoe UI; + font-size: 16px; + /*Top Bar on menus font*/ +} + QListView::item{ + color:#F1F1F1; + /* White */ +} + QGroupBox { +} + QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top center; +} + QGroupBox::indicator { + subcontrol-origin: margin; + subcontrol-position: top center; +} + QLabel { + color: #cccccc; + /* color: #F1F1F1; + /* White */ + /* border: none; + outline: none; + border-radius: 5px; + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; + */ +} diff --git a/src/stylesheets/Transparent-Style-Skyrim.qss b/src/stylesheets/Transparent-Style-Skyrim.qss index b5fd7473..89e36c74 100644 --- a/src/stylesheets/Transparent-Style-Skyrim.qss +++ b/src/stylesheets/Transparent-Style-Skyrim.qss @@ -1 +1,745 @@ -#centralWidget { border-image: url(./Transparent-Style/Background-skyrim.png) 0 0 0 0 stretch stretch; } QWidget#TextViewer{ } QWidget{ color: #fff; background-color: rgba(45,45,48,30); alternate-background-color: rgba(38,38,38,30); } QWidget:disabled{ } QAbstractItemView{ color: #dcdcdc; background-color: rgb(45,45,48,30); alternate-background-color: rgb(38,38,38,50); border: 1px solid #3F3F46; /* Grey - Blue*/ border-style:outset; border-radius: 3px; selection-color: #3F3F46; /* Grey - Blue*/ outline: none; padding-left:0; } #problemsWidget,#TransferSavesDialog,#SettingsDialog,#ProblemsDialog,#ModInfoDialog,#TextViewer,#PyCfgDialog,#InstallDialog { border-image:url(./Transparent-Style/Vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #toolBarArea{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #EditExecutablesDialog{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #SimpleInstallDialog,#LockedDialog,QMenu{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #nameLabel{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:rgba(154,154,0,0.3); color:#fff; } #ProfilesDialog,#FomodInstallerDialog{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } #filesView{ border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; } /* ******************************************** */ /* Main Navigation Button Bar at top of window */ /* ******************************************** */ QToolBar{ /*border: none;*/ } QToolBar::separator{ /*image: url(./Transparent-Style/Vault-101-vault-logo.png);*/ } QToolButton{ border:2px solid None; border-radius: 6px; margin: 3px; padding-left: 8px; padding-right: 8px; padding-top: 0px; padding-bottom: 2px; } QToolButton:hover{ border-radius: 3px; border: 0px; padding: 0px; background: trANSPARENT; border: 0px ridge #9A9A00; background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #9A9A00, stop: 1 #484F53); } QToolButton:pressed{ background: black; } /* **************************************** */ /* Tabs on top of Treeview */ /* **************************************** */ QTabWidget::pane{ border-color:#3F3F46; /* Grey - Blue*/ /* border-top-color:#9A9A00; */ top: 0px; border-style:hidden; border-width:1px; } QTabWidget::tab-bar{ alignment: center; } QTabBar{ color: #cccccc; font-family: Segoe UI; font-size: 16px; text-transform: none; min-height:auto; padding-bottom: 5px; } QTabBar::tab{ border: none; border-bottom-style: none; background-color: rgba(154,154,0,0.3); padding-left: 9px; padding-right: 9px; padding-top: 3px; padding-bottom: 3px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } QTabBar::tab:disabled{ color: #404040; background-color: #000; margin-top: 0px; } QTabBar::tab:selected{ color: #dcdcdc; background-color: #9A9A00; /*Yellow opaque*/ } QTabBar::tab:!selected:hover{ border-radius: 3px; border-top:3px double #9A9A00; border-bottom:3px double #9A9A00; padding: 1px; text-align: center; background-color: #9A9A00; color:#fff; Height: 20%; } /* QTabBar::tab:!selected{ color: #808080; background-color: #404040; margin-top: 0px; } */ #deactivateESP,#activateESP{ border:#b30000; background:#333337; } /* **************************************** */ /* <More View> on top of Treeview */ /* **************************************** */ QTabBar QToolButton{ border-radius: 0px; border: 0px; padding: 0px; background: transparent; } QToolButton::menu-indicator:pressed,QToolButton::menu-indicator:open{ image: url(./Transparent-Style/arrow-up.png); } QToolButton::menu-indicator:!pressed,QToolButton::menu-indicator:!open{ image: url(./Transparent-Style/arrow-down.png); } QTabBar QToolButton::right-arrow{ image: url(./Transparent-Style/narrow-arrow-right.png); } QTabBar QToolButton::left-arrow{ image: url(./Transparent-Style/narrow-arrow-left.png); } /* **************************************** */ /* Column names of TreeView */ /* **************************************** */ QTableView{ gridline-color:#3F3F46; selection-background-color:#9A9A00; selection-color:#F1F1F1; /* White */ text-align: center; min-height:24px; } QHeaderView{ min-height:24px; /*Top Bar on menus*/ padding: 3px; } QHeaderView::section{ /* color: #cc3333; */ background-color: #252526; border-radius: 0px; padding: 0px; padding-left: 5px; } QHeaderView::section:hover{ background: rgba(154,154,0,0.3); border: 1px solid #9a9a00; padding: 0px; padding-left: 5px; } QHeaderView::up-arrow,QHeaderView::down-arrow{ min-height:Auto; min-width:Auto; } QHeaderView::up-arrow{ image: url(./Transparent-Style/arrow-up.png); min-height:25px; min-width:15px; padding-right: 0px; }QHeaderView::down-arrow{ image: url(./Transparent-Style/arrow-down.png); } /* **************************************** */ /* Hover */ /* **************************************** */ QMenu:item:hover,QTreeView#espList::item:hover,QTreeWidget#categoriesList::item:hover,QListView::item:hover,QTreeWidget::item:hover{ color:#fff; border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:rgba(154,154,0,0.3); } QAbstractItemView::item:hover{ border-top:1px solid #b9d7fc; border-bottom:1px solid #b9d7fc; background-color:rgba(154,154,0,0.3); padding: 3px; } /*#DownloadListWidget{ }*/ #DownloadListWidget{ outline: 1px solid #e8e8e8; border: 0px solid #9a9a00; } #deactivateESP:hover,#activateESP:hover,QTableView:hover,QHeaderView::selected:hover{ border: 1px solid #9a9a00; padding: 1px; text-align: center; color:#fff; } /* **************************************** */ /* Context menus, toolbar dropdowns #QMenu */ /* **************************************** */ QMenu{ border-width: 3px; border-style: solid; }QMenu::item{ padding: 6px 20px; }QMenu::item:selected{ color:#fff; background-color: #3F3F46; /* Grey - Blue*/ border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QMenu::right-arrow{ image: url(./Transparent-Style/arrow-right.png); /* right click main menu top option*/ subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; border:0px outset #3F3F46; /* Grey - Blue*/ } /* ********************* */ /* LCD Counter */ /* ********************* */ QLCDNumber{ color: #9A9A00; Background: #000; border-color: #3F3F46; /* Grey - Blue*/ border-style: solid; border-width: 1px; border-radius: 5px; } /* **************************************** */ /* Launch application Drop down Box */ /* **************************************** */ QComboBox{ background-color: #333337; padding: 3px 5px 5px 5px; height: 26px; border-radius: 5px; border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QComboBox:on{ padding: 3px 5px 5px 5px; color: white; background-color: #3F3F46; /* Grey - Blue*/ } QComboBox:hover{ border-radius: 3px; border-top:3px double #9A9A00; border-bottom:3px double #9A9A00; /* padding: 0px; */ } QComboBox::drop-down{ /* Down arrow in combo box */ outline: none; border: none; border-width: 1px; } QComboBox::menu-indicator{ image: url(./Transparent-Style/arrow-down.png); subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; border:1px outset #3F3F46; /* Grey - Blue*/ } /* ************************* */ /* QComboBox::down-arrow:on, */ /* ************************* */ QComboBox::down-arrow:hover,QComboBox::down-arrow:focus { image: url(./Transparent-Style/arrow-down-hover.png); } QComboBox::up-arrow:hover,QComboBox::up-arrow:focus { image: url(./Transparent-Style/arrow-up-hover.png); } QComboBox::down-arrow{ image:url(./Transparent-Style/arrow-down.png); subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; } QComboBox::down-arrow:on{ image:url(./Transparent-Style/arrow-up.png); } QComboBox:editable { background: black; /* color: pink; */ } QComboBox::item{ } QComboBox QAbstractItemView{ selection-color:#fff; outline: 1px solid #9a9a00; selection-background-color:#3F3F46; /* Grey - Blue*/ border-top:1px solid #9a9a00; border-bottom:1px solid #9a9a00; padding: 5px; } /* **************************************************************** */ /* Action Buttons */ /* */ /* Open list, Restore Backup, Create Backup, RUN, Shortcut Button */ /* Sort, Load order backup, Load Order Restore */ /* **************************************************************** */ QPushButton{ background-color: #333337; min-height:26px; padding:1px 5%; text-align: center; border-radius: 5px; border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QPushButton::menu-indicator{ image: url(./Transparent-Style/arrow-down.png); subcontrol-origin: padding; subcontrol-position: center right; width: 18px; height: 18px; border:1px hidden #3F3F46; /* Grey - Blue*/ } QPushButton::menu-indicator:Hover{ image: url(./Transparent-Style/arrow-down-hover.png); } QPushButton:focus{ } QPushButton:on,QPushButton:checked,QPushButton:pressed,QPushButton:checked:hover{ background: #3F3F46; /* Grey - Blue*/ border-radius: 1px; border: 1px solid #9a9a00; } QPushButton:hover{ border-radius: 3px; border-top:3px double #9A9A00; border-bottom:3px double #9A9A00; } QDialog QPushButton,QSlider::handle:horizontal,QSlider::handle:vertical{ color:#fff; border-color:#707070; border-style:solid; border-width:1px; } QPushButton:disabled{ } QPushButton:open{ } QPushButton:!enabled{ } /* **************************************** */ /* Filter Button */ /* **************************************** */ QPushButton#displayCategoriesBtn{ min-width: 20px; background:transparent; color: transparent; border: 1px hidden #9a9a00; } QPushButton#displayCategoriesBtn:checked{ image: url(./Transparent-Style/arrow-left.png); } QPushButton#displayCategoriesBtn:!checked{ image: url(./Transparent-Style/arrow-right.png); } QPushButton#displayCategoriesBtn:checked:hover{ image: url(./Transparent-Style/arrow-left-hover.png); } QPushButton#displayCategoriesBtn:!checked:hover{ image: url(./Transparent-Style/arrow-right-hover.png); } /* **************************************** */ /* Scroll bar */ /* **************************************** */ QScrollBar{ height:20px; width:20px; background-color:transparent; } QScrollBar::handle{ border: 1px solid #aaa; border-radius: 4px; } QScrollBar::handle:vertical{ margin: 1px 4px; min-height:20px; min-width: 10px; background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #9A9A00, stop:1 #000000); } QScrollBar::handle:vertical:hover{ background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #000000, stop:1 #9A9A00); } QScrollBar::handle:horizontal{ min-height:10px;min-width:20px;margin:4px 1px; background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #9A9A00, stop:1 #000000); } QScrollBar::handle:horizontal:hover{ background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #000000, stop:1 #9A9A00); } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal{ background-color: transparent; } QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal{ height:0; width:0; } /* ******************************************************** */ /* Main Tree View Inherited from QAbstractItemView */ /* ******************************************************** */ QTreeView { } QTreeView:item{ padding: 3px; font-size: 16px; height: 18px; /*Bottom text Box*/ } QTreeView:item:selected{ color:#fff; padding: 0px; background-color: #3F3F46; /* Grey - Blue*/ border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00; } QTreeView:item QLineEdit{ outline: none; border: none; padding: 0px; margin: 0px; } QTreeView::branch:has-siblings:!adjoins-item{ background:url(Transparent-Style/vault-101-item-line-v.png) center center no-repeat; } QTreeView::branch:has-siblings{ background:url(Transparent-Style/vault-101-itemB-line.png) center center no-repeat; } QTreeView::branch:!has-children:!has-siblings:adjoins-item{ background:url(Transparent-Style/vault-101-itemB-end.png) center center no-repeat; } QTreeView::branch:closed:has-children:has-siblings{ background:url(Transparent-Style/vault-101-itemB-close.png) center center no-repeat; } QTreeView::branch:closed:has-children:!has-siblings{ background:url(Transparent-Style/vault-101-itemB-close-last.png) center center no-repeat; } QTreeView::branch:open:has-children:has-siblings{ background:url(Transparent-Style/vault-101-itemB-open.png) center center no-repeat; } QTreeView::branch:open:has-children:!has-siblings{ background:url(Transparent-Style/vault-101-itemB-open-last.png) center center no-repeat; } /* **************************************************************** */ /* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */ /* common */ /* **************************************************************** */ QTreeView::indicator,QCheckBox::indicator{ outline: none; border: none; padding: 0px; width: 24px; height: 24px; } QGroupBox::indicator:checked,QGroupBox::indicator:indeterminate,QTreeView::indicator:checked,QTreeView::indicator:indeterminate,QCheckBox::indicator:checked,QCheckBox::indicator:indeterminate{ image: url(./Transparent-Style/checkbox-checked.png); } QGroupBox::indicator:checked:hover,QGroupBox::indicator:indeterminate:hover,QTreeView::indicator:checked:hover,QTreeView::indicator:indeterminate:hover,QCheckBox::indicator:checked:hover,QCheckBox::indicator:indeterminate:hover{ image: url(./Transparent-Style/checkbox-hover.png); } QGroupBox::indicator:checked:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:checked:disabled,QTreeView::indicator:indeterminate:disabled,QCheckBox::indicator:checked:disabled,QCheckBox::indicator:indeterminate:disabled{ image: url(./Transparent-Style/checkbox-checked-disabled.png); } QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QCheckBox::indicator:unchecked{ image: url(./Transparent-Style/checkbox.png); } QGroupBox::indicator:unchecked:hover,QTreeView::indicator:unchecked:hover,QCheckBox::indicator:unchecked:hover{ image: url(./Transparent-Style/checkbox-checked-hover.png); } QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QCheckBox::indicator:unchecked:disabled{ image: url(./Transparent-Style/checkbox-checked-disabled.png); } QTreeWidget#categoriesList::item:has-children{ background-image: url(./Transparent-Style/arrow-right-small.png); } QTreeWidget#categoriesList::item:has-children:open{ background-image: url(./Transparent-Style/branch-opened.png); } /* ******************************** */ /* Special styles */ /* increase categories tab width */ /* ******************************** */ QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ padding: .3em 0em; } QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ padding: .3em 0em; } QTreeWidget#categoriesGroup,QTreeWidget#categoriesList{ min-width: 200px; } QTreeWidget#categoriesList::item { background-position: center left; background-repeat: no-repeat; padding: 0.35em 4px; } /* ********************* */ /* Display Radio Button */ /* ********************* */ QRadioButton::indicator{ width: 16px; height: 16px; } QRadioButton::indicator::checked{ image: url(./Transparent-Style/radio-BOS-checked.png); } QRadioButton::indicator::unchecked{ image: url(./Transparent-Style/radio-BOS.png); } QRadioButton::indicator::unchecked:hover{ image: url(./Transparent-Style/radio-BOS-hover.png); } /* **************************************** */ /* Spinners #QSpinBox, #QDoubleSpinBox */ /* **************************************** */ QAbstractSpinBox{ padding: 5px; background-color: transparent; color: #eff0f1; border-radius: 2px; min-width: Auto; } QAbstractSpinBox::up-button,QAbstractSpinBox::down-button{ border-style: solid; border-width: 1px; subcontrol-origin: border; } QAbstractSpinBox::up-button:hover,QAbstractSpinBox::down-button:hover{ background-color: transparent; } QAbstractSpinBox::up-button{ background-color: transparent; subcontrol-origin: border; subcontrol-position: center right; padding-right: 5px; /* subcontrol-position: top right; */ } QAbstractSpinBox::down-button{ background-color: transparent; subcontrol-origin: border; subcontrol-position: center left; padding-left: 5px; /* subcontrol-position: bottom right; */ } QAbstractSpinBox::up-arrow{ border-image: url(./Transparent-Style/narrow-arrow-right.png); }QAbstractSpinBox::down-arrow{ border-image: url(./Transparent-Style/narrow-arrow-left.png); }/* QTextEdit,QWebView,QLineEdit,QComboBox{ border-color:#3F3F46; /* Grey - Blue*/ /*}*/ /* **************************************** */ /* Tooltips #QToolTip, #SaveGameInfoWidget */ /* **************************************** */ QToolTip{ background-color:#121212; color:#C0C0C0; padding:0px; border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } SaveGameInfoWidget{ font-size: 16px; background-color:#121212; color:#C0C0C0; padding:0px; border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } /* **************************** */ /* Handles Web, Nexus info tab */ /* **************************** */ QWebView{ background-color: Black; } QLineEdit{ font-family: Segoe UI; min-height: 20px; font-size: 16px; background-color:#3F3F46; selection-background-color: rgba(154,154,0,0.6);border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } QTextEdit{ color: #cccccc; font-family: Segoe UI; font-size: 16px; /* background-color:#000; */ selection-background-color: rgba(154,154,0,0.6); border-color:#9A9A00; border-radius: 3px; border-width:1px; border-style:solid; } /* Font size */ QLabel,QMenu,QPushButton,QAbstractSpinBox,QGroupBox,QCheckBox,QRadioButton{ color: #cccccc; font-family: Segoe UI; font-size: 16px; /* was 14px Profile font size */ } QAbstractItemView{ color: #cccccc; font-family: Segoe UI; /* font-family: Source Sans Pro; */ font-size: 16px; /* Body font size*/ } QRadioButton{ color: #cccccc; font-family: Segoe UI; /* font-family: Source Sans Pro; */ font-size: 16px; } QWebView,QComboBox,QComboBox:editable{ font-family:Segoe UI; font-style: normal; font-weight: normal; font-size: 16px; background-color:#000; /* border-color:#3F3F46; /* Grey - Blue*/ color: #cccccc; font-family: Arial; font-size: 16px; /* Filter Name*/ } QHeaderView::section{ /* font-family: Source Sans Pro; */ font-family: Segoe UI; font-size: 16px; /*Top Bar on menus font*/ } QListView::item{ color:#F1F1F1; /* White */ } QGroupBox { } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; } QGroupBox::indicator { subcontrol-origin: margin; subcontrol-position: top center; } QLabel { color: #cccccc; /* color: #F1F1F1; /* White */ /* border: none;outline: none; border-radius: 5px; border-top:1px solid #9A9A00; border-bottom:1px solid #9A9A00;*/ }
\ No newline at end of file +#centralWidget { + border-image: url(./Transparent-Style/Background-skyrim.png) 0 0 0 0 stretch stretch; +} + QWidget#TextViewer{ +} + QWidget{ + color: #fff; + background-color: rgba(45,45,48,30); + alternate-background-color: rgba(38,38,38,30); +} + QWidget:disabled{ +} + QAbstractItemView{ + color: #dcdcdc; + background-color: rgb(45,45,48,30); + alternate-background-color: rgb(38,38,38,50); + border: 1px solid #3F3F46; + /* Grey - Blue*/ + border-style:outset; + border-radius: 3px; + selection-color: #3F3F46; + /* Grey - Blue*/ + outline: none; + padding-left:0; +} + #problemsWidget,#TransferSavesDialog,#SettingsDialog,#ProblemsDialog,#ModInfoDialog,#TextViewer,#PyCfgDialog,#InstallDialog { + border-image:url(./Transparent-Style/Vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #toolBarArea{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #EditExecutablesDialog{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #SimpleInstallDialog,#LockedDialog,QMenu{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #nameLabel{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:rgba(154,154,0,0.3); + color:#fff; +} + #ProfilesDialog,#FomodInstallerDialog{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} + #filesView{ + border-image: url(./Transparent-Style/vault-BOS-Main.png) 0 0 0 0 stretch stretch; +} +/* ******************************************** */ +/* Main Navigation Button Bar at top of window */ +/* ******************************************** */ + QToolBar{ + /*border: none; + */ +} + QToolBar::separator{ + /*image: url(./Transparent-Style/Vault-101-vault-logo.png); + */ +} + QToolButton{ + border:2px solid None; + border-radius: 6px; + margin: 3px; + padding-left: 8px; + padding-right: 8px; + padding-top: 0px; + padding-bottom: 2px; +} + QToolButton:hover{ + border-radius: 3px; + border: 0px; + padding: 0px; + background: trANSPARENT; + border: 0px ridge #9A9A00; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #9A9A00, stop: 1 #484F53); +} + QToolButton:pressed{ + background: black; +} +/* **************************************** */ +/* Tabs on top of Treeview */ +/* **************************************** */ + QTabWidget::pane{ + border-color:#3F3F46; + /* Grey - Blue*/ + /* border-top-color:#9A9A00; + */ + top: 0px; + border-style:hidden; + border-width:1px; +} + QTabWidget::tab-bar{ + alignment: center; +} + QTabBar{ + color: #cccccc; + font-family: Segoe UI; + font-size: 16px; + text-transform: none; + min-height:auto; + padding-bottom: 5px; +} + QTabBar::tab{ + border: none; + border-bottom-style: none; + background-color: rgba(154,154,0,0.3); + padding-left: 9px; + padding-right: 9px; + padding-top: 3px; + padding-bottom: 3px; + margin-right: 2px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + QTabBar::tab:disabled{ + color: #404040; + background-color: #000; + margin-top: 0px; +} + QTabBar::tab:selected{ + color: #dcdcdc; + background-color: #9A9A00; + /*Yellow opaque*/ +} + QTabBar::tab:!selected:hover{ + border-radius: 3px; + border-top:3px double #9A9A00; + border-bottom:3px double #9A9A00; + padding: 1px; + text-align: center; + background-color: #9A9A00; + color:#fff; + Height: 20%; +} +/* QTabBar::tab:!selected{ + color: #808080; + background-color: #404040; + margin-top: 0px; +} + */ + #deactivateESP,#activateESP{ + border:#b30000; + background:#333337; +} +/* **************************************** */ +/* <More View> on top of Treeview */ +/* **************************************** */ + QTabBar QToolButton{ + border-radius: 0px; + border: 0px; + padding: 0px; + background: transparent; +} + QToolButton::menu-indicator:pressed,QToolButton::menu-indicator:open{ + image: url(./Transparent-Style/arrow-up.png); +} + QToolButton::menu-indicator:!pressed,QToolButton::menu-indicator:!open{ + image: url(./Transparent-Style/arrow-down.png); +} + QTabBar QToolButton::right-arrow{ + image: url(./Transparent-Style/narrow-arrow-right.png); +} + QTabBar QToolButton::left-arrow{ + image: url(./Transparent-Style/narrow-arrow-left.png); +} +/* **************************************** */ +/* Column names of TreeView */ +/* **************************************** */ + QTableView{ + gridline-color:#3F3F46; + selection-background-color:#9A9A00; + selection-color:#F1F1F1; + /* White */ + text-align: center; + min-height:24px; +} + QHeaderView{ + min-height:24px; + /*Top Bar on menus*/ + padding: 3px; +} + QHeaderView::section{ + /* color: #cc3333; + */ + background-color: #252526; + border-radius: 0px; + padding: 0px; + padding-left: 5px; +} + QHeaderView::section:hover{ + background: rgba(154,154,0,0.3); + border: 1px solid #9a9a00; + padding: 0px; + padding-left: 5px; +} + QHeaderView::up-arrow,QHeaderView::down-arrow{ + min-height:Auto; + min-width:Auto; +} + QHeaderView::up-arrow{ + image: url(./Transparent-Style/arrow-up.png); + min-height:25px; + min-width:15px; + padding-right: 0px; +} +QHeaderView::down-arrow{ + image: url(./Transparent-Style/arrow-down.png); +} +/* **************************************** */ +/* Hover */ +/* **************************************** */ + QMenu:item:hover,QMenuBar::item:selected,QTreeView#espList::item:hover,QTreeWidget#categoriesList::item:hover,QListView::item:hover,QTreeWidget::item:hover{ + color:#fff; + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:rgba(154,154,0,0.3); +} + QAbstractItemView::item:hover{ + border-top:1px solid #b9d7fc; + border-bottom:1px solid #b9d7fc; + background-color:rgba(154,154,0,0.3); + padding: 3px; +} +/*#DownloadListWidget{ +} +*/ + #DownloadListWidget{ + outline: 1px solid #e8e8e8; + border: 0px solid #9a9a00; +} + #deactivateESP:hover,#activateESP:hover,QTableView:hover,QHeaderView::selected:hover{ + border: 1px solid #9a9a00; + padding: 1px; + text-align: center; + color:#fff; +} +/* **************************************** */ +/* Context menus, toolbar dropdowns #QMenu */ +/* **************************************** */ + QMenu{ + border-width: 3px; + border-style: solid; +} +QMenu::item{ + padding: 6px 20px; +} +QMenu::item:selected{ + color:#fff; + background-color: #3F3F46; + /* Grey - Blue*/ + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QMenu::right-arrow{ + image: url(./Transparent-Style/arrow-right.png); + /* right click main menu top option*/ + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; + border:0px outset #3F3F46; + /* Grey - Blue*/ +} +/* ********************* */ +/* LCD Counter */ +/* ********************* */ + QLCDNumber{ + color: #9A9A00; + Background: #000; + border-color: #3F3F46; + /* Grey - Blue*/ + border-style: solid; + border-width: 1px; + border-radius: 5px; +} +/* **************************************** */ +/* Launch application Drop down Box */ +/* **************************************** */ + QComboBox{ + background-color: #333337; + padding: 3px 5px 5px 5px; + height: 26px; + border-radius: 5px; + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QComboBox:on{ + padding: 3px 5px 5px 5px; + color: white; + background-color: #3F3F46; + /* Grey - Blue*/ +} + QComboBox:hover{ + border-radius: 3px; + border-top:3px double #9A9A00; + border-bottom:3px double #9A9A00; + /* padding: 0px; + */ +} + QComboBox::drop-down{ + /* Down arrow in combo box */ + outline: none; + border: none; + border-width: 1px; +} + QComboBox::menu-indicator{ + image: url(./Transparent-Style/arrow-down.png); + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; + border:1px outset #3F3F46; + /* Grey - Blue*/ +} +/* ************************* */ +/* QComboBox::down-arrow:on, */ +/* ************************* */ + QComboBox::down-arrow:hover,QComboBox::down-arrow:focus { + image: url(./Transparent-Style/arrow-down-hover.png); +} + QComboBox::up-arrow:hover,QComboBox::up-arrow:focus { + image: url(./Transparent-Style/arrow-up-hover.png); +} + QComboBox::down-arrow{ + image:url(./Transparent-Style/arrow-down.png); + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; +} + QComboBox::down-arrow:on{ + image:url(./Transparent-Style/arrow-up.png); +} + QComboBox:editable { + background: black; + /* color: pink; + */ +} + QComboBox::item{ +} + QComboBox QAbstractItemView{ + selection-color:#fff; + outline: 1px solid #9a9a00; + selection-background-color:#3F3F46; + /* Grey - Blue*/ + border-top:1px solid #9a9a00; + border-bottom:1px solid #9a9a00; + padding: 5px; +} +/* **************************************************************** */ +/* Action Buttons */ +/* */ +/* Open list, Restore Backup, Create Backup, RUN, Shortcut Button */ +/* Sort, Load order backup, Load Order Restore */ +/* **************************************************************** */ + QPushButton{ + background-color: #333337; + min-height:26px; + padding:1px 5%; + text-align: center; + border-radius: 5px; + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QPushButton::menu-indicator{ + image: url(./Transparent-Style/arrow-down.png); + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + height: 18px; + border:1px hidden #3F3F46; + /* Grey - Blue*/ +} + QPushButton::menu-indicator:Hover{ + image: url(./Transparent-Style/arrow-down-hover.png); +} + QPushButton:focus{ +} + QPushButton:on,QPushButton:checked,QPushButton:pressed,QPushButton:checked:hover{ + background: #3F3F46; + /* Grey - Blue*/ + border-radius: 1px; + border: 1px solid #9a9a00; +} + QPushButton:hover{ + border-radius: 3px; + border-top:3px double #9A9A00; + border-bottom:3px double #9A9A00; +} + QDialog QPushButton,QSlider::handle:horizontal,QSlider::handle:vertical{ + color:#fff; + border-color:#707070; + border-style:solid; + border-width:1px; +} + QPushButton:disabled{ +} + QPushButton:open{ +} + QPushButton:!enabled{ +} +/* **************************************** */ +/* Filter Button */ +/* **************************************** */ + QPushButton#displayCategoriesBtn{ + min-width: 20px; + background:transparent; + color: transparent; + border: 1px hidden #9a9a00; +} + QPushButton#displayCategoriesBtn:checked{ + image: url(./Transparent-Style/arrow-left.png); +} + QPushButton#displayCategoriesBtn:!checked{ + image: url(./Transparent-Style/arrow-right.png); +} + QPushButton#displayCategoriesBtn:checked:hover{ + image: url(./Transparent-Style/arrow-left-hover.png); +} + QPushButton#displayCategoriesBtn:!checked:hover{ + image: url(./Transparent-Style/arrow-right-hover.png); +} +/* **************************************** */ +/* Scroll bar */ +/* **************************************** */ + QScrollBar{ + height:20px; + width:20px; + background-color:transparent; +} + QScrollBar::handle{ + border: 1px solid #aaa; + border-radius: 4px; +} + QScrollBar::handle:vertical{ + margin: 1px 4px; + min-height:20px; + min-width: 10px; + background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #9A9A00, stop:1 #000000); +} + QScrollBar::handle:vertical:hover{ + background-color:qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #000000, stop:1 #9A9A00); +} + QScrollBar::handle:horizontal{ + min-height:10px; + min-width:20px; + margin:4px 1px; + background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #9A9A00, stop:1 #000000); +} + QScrollBar::handle:horizontal:hover{ + background-color:qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #000000, stop:1 #9A9A00); +} + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal{ + background-color: transparent; +} + QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal{ + height:0; + width:0; +} +/* ******************************************************** */ +/* Main Tree View Inherited from QAbstractItemView */ +/* ******************************************************** */ + QTreeView { +} + QTreeView:item{ + padding: 3px; + font-size: 16px; + height: 18px; + /*Bottom text Box*/ +} + QTreeView:item:selected{ + color:#fff; + padding: 0px; + background-color: #3F3F46; + /* Grey - Blue*/ + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; +} + QTreeView:item QLineEdit{ + outline: none; + border: none; + padding: 0px; + margin: 0px; +} + QTreeView::branch:has-siblings:!adjoins-item{ + background:url(Transparent-Style/vault-101-item-line-v.png) center center no-repeat; +} + QTreeView::branch:has-siblings{ + background:url(Transparent-Style/vault-101-itemB-line.png) center center no-repeat; +} + QTreeView::branch:!has-children:!has-siblings:adjoins-item{ + background:url(Transparent-Style/vault-101-itemB-end.png) center center no-repeat; +} + QTreeView::branch:closed:has-children:has-siblings{ + background:url(Transparent-Style/vault-101-itemB-close.png) center center no-repeat; +} + QTreeView::branch:closed:has-children:!has-siblings{ + background:url(Transparent-Style/vault-101-itemB-close-last.png) center center no-repeat; +} + QTreeView::branch:open:has-children:has-siblings{ + background:url(Transparent-Style/vault-101-itemB-open.png) center center no-repeat; +} + QTreeView::branch:open:has-children:!has-siblings{ + background:url(Transparent-Style/vault-101-itemB-open-last.png) center center no-repeat; +} +/* **************************************************************** */ +/* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */ +/* common */ +/* **************************************************************** */ + QTreeView::indicator,QCheckBox::indicator{ + outline: none; + border: none; + padding: 0px; + width: 24px; + height: 24px; +} + QGroupBox::indicator:checked,QGroupBox::indicator:indeterminate,QTreeView::indicator:checked,QTreeView::indicator:indeterminate,QCheckBox::indicator:checked,QCheckBox::indicator:indeterminate{ + image: url(./Transparent-Style/checkbox-checked.png); +} + QGroupBox::indicator:checked:hover,QGroupBox::indicator:indeterminate:hover,QTreeView::indicator:checked:hover,QTreeView::indicator:indeterminate:hover,QCheckBox::indicator:checked:hover,QCheckBox::indicator:indeterminate:hover{ + image: url(./Transparent-Style/checkbox-hover.png); +} + QGroupBox::indicator:checked:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeView::indicator:checked:disabled,QTreeView::indicator:indeterminate:disabled,QCheckBox::indicator:checked:disabled,QCheckBox::indicator:indeterminate:disabled{ + image: url(./Transparent-Style/checkbox-checked-disabled.png); +} + QGroupBox::indicator:unchecked,QTreeView::indicator:unchecked,QCheckBox::indicator:unchecked{ + image: url(./Transparent-Style/checkbox.png); +} + QGroupBox::indicator:unchecked:hover,QTreeView::indicator:unchecked:hover,QCheckBox::indicator:unchecked:hover{ + image: url(./Transparent-Style/checkbox-checked-hover.png); +} + QGroupBox::indicator:unchecked:disabled,QTreeView::indicator:unchecked:disabled,QCheckBox::indicator:unchecked:disabled{ + image: url(./Transparent-Style/checkbox-checked-disabled.png); +} + QTreeWidget#categoriesList::item:has-children{ + background-image: url(./Transparent-Style/arrow-right-small.png); +} + QTreeWidget#categoriesList::item:has-children:open{ + background-image: url(./Transparent-Style/branch-opened.png); +} +/* ******************************** */ +/* Special styles */ +/* increase categories tab width */ +/* ******************************** */ + QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ + padding: .3em 0em; +} + QTreeView#modList::item,QTreeWidget#categoriesTree::item,#tabConflicts QTreeWidget::item,QListView::item,QTreeView#espList::item,QTreeView#treeContent::item{ + padding: .3em 0em; +} + QTreeWidget#categoriesGroup,QTreeWidget#categoriesList{ + min-width: 200px; +} + QTreeWidget#categoriesList::item { + background-position: center left; + background-repeat: no-repeat; + padding: 0.35em 4px; +} +/* ********************* */ +/* Display Radio Button */ +/* ********************* */ + QRadioButton::indicator{ + width: 16px; + height: 16px; +} + QRadioButton::indicator::checked{ + image: url(./Transparent-Style/radio-BOS-checked.png); +} + QRadioButton::indicator::unchecked{ + image: url(./Transparent-Style/radio-BOS.png); +} + QRadioButton::indicator::unchecked:hover{ + image: url(./Transparent-Style/radio-BOS-hover.png); +} +/* **************************************** */ +/* Spinners #QSpinBox, #QDoubleSpinBox */ +/* **************************************** */ + QAbstractSpinBox{ + padding: 5px; + background-color: transparent; + color: #eff0f1; + border-radius: 2px; + min-width: Auto; +} + QAbstractSpinBox::up-button,QAbstractSpinBox::down-button{ + border-style: solid; + border-width: 1px; + subcontrol-origin: border; +} + QAbstractSpinBox::up-button:hover,QAbstractSpinBox::down-button:hover{ + background-color: transparent; +} + QAbstractSpinBox::up-button{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; + padding-right: 5px; + /* subcontrol-position: top right; + */ +} + QAbstractSpinBox::down-button{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; + padding-left: 5px; + /* subcontrol-position: bottom right; + */ +} + QAbstractSpinBox::up-arrow{ + border-image: url(./Transparent-Style/narrow-arrow-right.png); +} +QAbstractSpinBox::down-arrow{ + border-image: url(./Transparent-Style/narrow-arrow-left.png); +} +/* QTextEdit,QWebView,QLineEdit,QComboBox{ + border-color:#3F3F46; + /* Grey - Blue*/ + /* +} +*/ +/* **************************************** */ +/* Tooltips #QToolTip, #SaveGameInfoWidget */ +/* **************************************** */ + QToolTip{ + background-color:#121212; + color:#C0C0C0; + padding:0px; + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} + SaveGameInfoWidget{ + font-size: 16px; + background-color:#121212; + color:#C0C0C0; + padding:0px; + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} + +QStatusBar::item {border: None;} + +/* **************************** */ +/* Handles Web, Nexus info tab */ +/* **************************** */ + QWebView{ + background-color: Black; +} + QLineEdit{ + font-family: Segoe UI; + min-height: 20px; + font-size: 16px; + background-color:#3F3F46; + selection-background-color: rgba(154,154,0,0.6); + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} + QTextEdit{ + color: #cccccc; + font-family: Segoe UI; + font-size: 16px; + /* background-color:#000; + */ + selection-background-color: rgba(154,154,0,0.6); + border-color:#9A9A00; + border-radius: 3px; + border-width:1px; + border-style:solid; +} +/* Font size */ + QLabel,QMenu,QPushButton,QAbstractSpinBox,QGroupBox,QCheckBox,QRadioButton{ + color: #cccccc; + font-family: Segoe UI; + font-size: 16px; + /* was 14px Profile font size */ +} + QAbstractItemView{ + color: #cccccc; + font-family: Segoe UI; + /* font-family: Source Sans Pro; + */ + font-size: 16px; + /* Body font size*/ +} + QRadioButton{ + color: #cccccc; + font-family: Segoe UI; + /* font-family: Source Sans Pro; + */ + font-size: 16px; +} + QWebView,QComboBox,QComboBox:editable{ + font-family:Segoe UI; + font-style: normal; + font-weight: normal; + font-size: 16px; + background-color:#000; + /* border-color:#3F3F46; + /* Grey - Blue*/ + color: #cccccc; + font-family: Arial; + font-size: 16px; + /* Filter Name*/ +} + QHeaderView::section{ + /* font-family: Source Sans Pro; + */ + font-family: Segoe UI; + font-size: 16px; + /*Top Bar on menus font*/ +} + QListView::item{ + color:#F1F1F1; + /* White */ +} + QGroupBox { +} + QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top center; +} + QGroupBox::indicator { + subcontrol-origin: margin; + subcontrol-position: top center; +} + QLabel { + color: #cccccc; + /* color: #F1F1F1; + /* White */ + /* border: none; + outline: none; + border-radius: 5px; + border-top:1px solid #9A9A00; + border-bottom:1px solid #9A9A00; + */ +} diff --git a/src/stylesheets/dark.qss b/src/stylesheets/dark.qss index 5bbb0f0c..22cd598c 100644 --- a/src/stylesheets/dark.qss +++ b/src/stylesheets/dark.qss @@ -247,6 +247,13 @@ QMenu::item:selected border-color: #3EA0CA;
}
+QMenuBar::item:selected {
+ background-color: #3c4b54;
+ border-color: #3EA0CA;
+}
+
+QStatusBar::item {border: None;}
+
QProgressBar
{
border: 2px solid grey;
diff --git a/src/stylesheets/dracula.qss b/src/stylesheets/dracula.qss index 385fca16..2a7fbf9e 100644 --- a/src/stylesheets/dracula.qss +++ b/src/stylesheets/dracula.qss @@ -280,12 +280,18 @@ QMenu::separator { /* * Scroll bar */ -QScrollBar { +QScrollBar:vertical { background-color: transparent; margin: 0; height: 1px; width: 12px; } +QScrollBar:horizontal { + background-color: transparent; + margin: 0; + height: 12px; + width: 1px; +} QScrollBar::handle { border: 1px solid #555555; border-radius: 4px; @@ -412,6 +418,8 @@ DownloadListWidget::item:selected { padding: 0px; } +QStatusBar::item {border: None;} + QProgressBar { border: 2px solid grey; diff --git a/src/stylesheets/skyrim.qss b/src/stylesheets/skyrim.qss index d36eac57..2da5154d 100644 --- a/src/stylesheets/skyrim.qss +++ b/src/stylesheets/skyrim.qss @@ -488,6 +488,14 @@ QHeaderView { image: url(./skyrim/arrow-down.png); } /* Context menus, toolbar drop-downs #QMenu */ +QMenuBar { + background-color: #000; +} + +QMenuBar::item:selected { + background-color: #121212; +} + QMenu { background-color: transparent; } QMenu::item, @@ -529,6 +537,8 @@ SaveGameInfoWidget { background-color: #121212; color: #C0C0C0; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: transparent; diff --git a/src/stylesheets/vs15 Dark-Green.qss b/src/stylesheets/vs15 Dark-Green.qss index 10f923cb..9cf26a31 100644 --- a/src/stylesheets/vs15 Dark-Green.qss +++ b/src/stylesheets/vs15 Dark-Green.qss @@ -49,7 +49,9 @@ QTreeWidget#categoriesTree::item, QListView::item, QTreeView#espList::item { - padding: 0.3em; } + padding-top: 0.3em; + padding-bottom: 0.3em; +} /* to enable border color */ QTreeView, @@ -201,8 +203,7 @@ QToolBar::separator { width: 0; } QToolButton { - margin: 0 4px 0 4px; - padding: 5px; } + padding: 4px; } QToolButton:hover, QToolButton:focus { background-color: #3E3E40; } QToolButton:pressed { @@ -435,13 +436,13 @@ QScrollBar::sub-line { background-color: #3E3E42; border: none; } -/*QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +/*QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { }*/ QScrollBar::handle:hover { background: #9E9E9E; } @@ -551,7 +552,7 @@ QMenu::item { background: transparent; padding: 4px 20px; } -QMenu::item:selected { +QMenu::item:selected, QMenuBar::item:selected { background-color: #333334; } QMenu::item:disabled { @@ -591,6 +592,8 @@ SaveGameInfoWidget { border-width: 1px; padding: 2px; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: #E6E6E6; diff --git a/src/stylesheets/vs15 Dark-Orange.qss b/src/stylesheets/vs15 Dark-Orange.qss index bbde1f82..041f1b00 100644 --- a/src/stylesheets/vs15 Dark-Orange.qss +++ b/src/stylesheets/vs15 Dark-Orange.qss @@ -50,7 +50,9 @@ QTreeWidget#categoriesTree::item, QListView::item, QTreeView#espList::item { - padding: 0.3em; } + padding-top: 0.3em; + padding-bottom: 0.3em; +} /* to enable border color */ QTreeView, @@ -202,8 +204,7 @@ QToolBar::separator { width: 0; } QToolButton { - margin: 0 4px 0 4px; - padding: 5px; } + padding: 4px; } QToolButton:hover, QToolButton:focus { background-color: #3E3E40; } QToolButton:pressed { @@ -436,13 +437,13 @@ QScrollBar::sub-line { background-color: #3E3E42; border: none; } -/*QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +/*QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { }*/ QScrollBar::handle:hover { background: #9E9E9E; } @@ -552,7 +553,7 @@ QMenu::item { background: transparent; padding: 4px 20px; } -QMenu::item:selected { +QMenu::item:selected, QMenuBar::item:selected { background-color: #333334; } QMenu::item:disabled { @@ -592,6 +593,8 @@ SaveGameInfoWidget { border-width: 1px; padding: 2px; } + QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: #E6E6E6; diff --git a/src/stylesheets/vs15 Dark-Purple.qss b/src/stylesheets/vs15 Dark-Purple.qss index faad7297..bc8bbcde 100644 --- a/src/stylesheets/vs15 Dark-Purple.qss +++ b/src/stylesheets/vs15 Dark-Purple.qss @@ -50,7 +50,9 @@ QTreeWidget#categoriesTree::item, QListView::item, QTreeView#espList::item { - padding: 0.3em; } + padding-top: 0.3em; + padding-bottom: 0.3em; +} /* to enable border color */ QTreeView, @@ -202,8 +204,7 @@ QToolBar::separator { width: 0; } QToolButton { - margin: 0 4px 0 4px; - padding: 5px; } + padding: 4px; } QToolButton:hover, QToolButton:focus { background-color: #3E3E40; } QToolButton:pressed { @@ -436,13 +437,13 @@ QScrollBar::sub-line { background-color: #3E3E42; border: none; } -/*QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +/*QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { }*/ QScrollBar::handle:hover { background: #9E9E9E; } @@ -552,7 +553,7 @@ QMenu::item { background: transparent; padding: 4px 20px; } -QMenu::item:selected { +QMenu::item:selected, QMenuBar::item:selected { background-color: #333334; } QMenu::item:disabled { @@ -592,6 +593,8 @@ SaveGameInfoWidget { border-width: 1px; padding: 2px; } + QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: #E6E6E6; diff --git a/src/stylesheets/vs15 Dark-Red.qss b/src/stylesheets/vs15 Dark-Red.qss index 2ffcff68..0c9143cd 100644 --- a/src/stylesheets/vs15 Dark-Red.qss +++ b/src/stylesheets/vs15 Dark-Red.qss @@ -50,7 +50,9 @@ QTreeWidget#categoriesTree::item, QListView::item, QTreeView#espList::item { - padding: 0.3em; } + padding-top: 0.3em; + padding-bottom: 0.3em; +} /* to enable border color */ QTreeView, @@ -202,8 +204,7 @@ QToolBar::separator { width: 0; } QToolButton { - margin: 0 4px 0 4px; - padding: 5px; } + padding: 4px; } QToolButton:hover, QToolButton:focus { background-color: #3E3E40; } QToolButton:pressed { @@ -436,13 +437,13 @@ QScrollBar::sub-line { background-color: #3E3E42; border: none; } -/*QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +/*QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { }*/ QScrollBar::handle:hover { background: #9E9E9E; } @@ -552,7 +553,7 @@ QMenu::item { background: transparent; padding: 4px 20px; } -QMenu::item:selected { +QMenu::item:selected, QMenuBar::item:selected { background-color: #333334; } QMenu::item:disabled { @@ -592,6 +593,8 @@ SaveGameInfoWidget { border-width: 1px; padding: 2px; } + QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: #E6E6E6; diff --git a/src/stylesheets/vs15 Dark-Yellow.qss b/src/stylesheets/vs15 Dark-Yellow.qss index 24afe005..2eb42534 100644 --- a/src/stylesheets/vs15 Dark-Yellow.qss +++ b/src/stylesheets/vs15 Dark-Yellow.qss @@ -50,7 +50,9 @@ QTreeWidget#categoriesTree::item, QListView::item, QTreeView#espList::item { - padding: 0.3em; } + padding-top: 0.3em; + padding-bottom: 0.3em; +} /* to enable border color */ QTreeView, @@ -202,8 +204,7 @@ QToolBar::separator { width: 0; } QToolButton { - margin: 0 4px 0 4px; - padding: 5px; } + padding: 4px; } QToolButton:hover, QToolButton:focus { background-color: #3E3E40; } QToolButton:pressed { @@ -436,13 +437,13 @@ QScrollBar::sub-line { background-color: #3E3E42; border: none; } -/*QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +/*QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { }*/ QScrollBar::handle:hover { background: #9E9E9E; } @@ -552,7 +553,7 @@ QMenu::item { background: transparent; padding: 4px 20px; } -QMenu::item:selected { +QMenu::item:selected, QMenuBar::item:selected { background-color: #333334; } QMenu::item:disabled { @@ -592,6 +593,8 @@ SaveGameInfoWidget { border-width: 1px; padding: 2px; } + QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: #E6E6E6; diff --git a/src/stylesheets/vs15 Dark.qss b/src/stylesheets/vs15 Dark.qss index 6a551775..d67cce35 100644 --- a/src/stylesheets/vs15 Dark.qss +++ b/src/stylesheets/vs15 Dark.qss @@ -49,7 +49,9 @@ QTreeWidget#categoriesTree::item, QListView::item, QTreeView#espList::item { - padding: 0.3em; } + padding-top: 0.3em; + padding-bottom: 0.3em; +} /* to enable border color */ QTreeView, @@ -201,8 +203,7 @@ QToolBar::separator { width: 0; } QToolButton { - margin: 0 4px 0 4px; - padding: 5px; } + padding: 4px; } QToolButton:hover, QToolButton:focus { background-color: #3E3E40; } QToolButton:pressed { @@ -435,13 +436,13 @@ QScrollBar::sub-line { background-color: #3E3E42; border: none; } -/*QScrollBar::add-line:horizontal:hover, -QScrollBar::sub-line:horizontal:hover, -QScrollBar::add-line:vertical:hover, +/*QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, QScrollBar::sub-line:vertical:hover, -QScrollBar::add-line:horizontal:pressed, -QScrollBar::sub-line:horizontal:pressed, -QScrollBar::add-line:vertical:pressed, +QScrollBar::add-line:horizontal:pressed, +QScrollBar::sub-line:horizontal:pressed, +QScrollBar::add-line:vertical:pressed, QScrollBar::sub-line:vertical:pressed { }*/ QScrollBar::handle:hover { background: #9E9E9E; } @@ -551,7 +552,7 @@ QMenu::item { background: transparent; padding: 4px 20px; } -QMenu::item:selected { +QMenu::item:selected, QMenuBar::item:selected { background-color: #333334; } QMenu::item:disabled { @@ -591,6 +592,8 @@ SaveGameInfoWidget { border-width: 1px; padding: 2px; } +QStatusBar::item {border: None;} + /* Progress Bars (Downloads) #QProgressBar */ QProgressBar { background-color: #E6E6E6; diff --git a/src/texteditor.cpp b/src/texteditor.cpp new file mode 100644 index 00000000..130cd76f --- /dev/null +++ b/src/texteditor.cpp @@ -0,0 +1,533 @@ +#include "texteditor.h" +#include "utility.h" +#include <QSplitter> + +TextEditor::TextEditor(QWidget* parent) : + QPlainTextEdit(parent), + m_toolbar(nullptr), m_lineNumbers(nullptr), m_highlighter(nullptr), + m_dirty(false), m_loading(false) +{ + m_toolbar = new TextEditorToolbar(*this); + m_lineNumbers = new TextEditorLineNumbers(*this); + m_highlighter = new TextEditorHighlighter(document()); + + setDefaultStyle(); + wordWrap(true); + + emit modified(false); + + connect( + document(), &QTextDocument::modificationChanged, + [&](bool b){ onModified(b); }); + + connect( + this, &QPlainTextEdit::cursorPositionChanged, + [&]{ highlightCurrentLine(); }); +} + +void TextEditor::setDefaultStyle() +{ + const auto font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + + setFont(font); + m_lineNumbers->setFont(font); + + QColor textColor(Qt::black); + QColor altTextColor(Qt::darkGray); + QColor backgroundColor(Qt::white); + + { + auto w = std::make_unique<QWidget>(); + + if (auto* s=style()) { + s->polish(w.get()); + } + + textColor = w->palette().color(QPalette::WindowText); + altTextColor = w->palette().color(QPalette::Disabled, QPalette::WindowText); + backgroundColor = w->palette().color(QPalette::Window); + } + + setTextColor(textColor); + m_lineNumbers->setTextColor(altTextColor); + + setBackgroundColor(backgroundColor); + m_lineNumbers->setBackgroundColor(backgroundColor); + + setHighlightBackgroundColor(backgroundColor); +} + +void TextEditor::clear() +{ + QScopedValueRollback loading(m_loading, true); + + m_filename.clear(); + m_encoding.clear(); + setPlainText(""); + dirty(false); + document()->setModified(false); + + emit loaded(""); +} + +bool TextEditor::load(const QString& filename) +{ + clear(); + + QScopedValueRollback loading(m_loading, true); + + m_filename = filename; + + const QString s = MOBase::readFileText(filename, &m_encoding); + + setPlainText(s); + document()->setModified(false); + + if (s.isEmpty()) { + // the modificationChanged even is not fired by the setModified() call + // above when the text being set is empty + onModified(false); + } + + emit loaded(m_filename); + + return true; +} + +bool TextEditor::save() +{ + if (m_filename.isEmpty() || m_encoding.isEmpty()) { + return false; + } + + QFile file(m_filename); + file.open(QIODevice::WriteOnly); + file.resize(0); + + QTextCodec* codec = QTextCodec::codecForName(m_encoding.toUtf8()); + QString data = toPlainText().replace("\n", "\r\n"); + + file.write(codec->fromUnicode(data)); + document()->setModified(false); + + return true; +} + +const QString& TextEditor::filename() const +{ + return m_filename; +} + +void TextEditor::wordWrap(bool b) +{ + if (b) { + setLineWrapMode(QPlainTextEdit::WidgetWidth); + } else { + setLineWrapMode(QPlainTextEdit::NoWrap); + } + + emit wordWrapChanged(b); +} + +void TextEditor::toggleWordWrap() +{ + wordWrap(!wordWrap()); +} + +bool TextEditor::wordWrap() const +{ + return (lineWrapMode() == QPlainTextEdit::WidgetWidth); +} + +void TextEditor::dirty(bool b) +{ + m_dirty = b; +} + +bool TextEditor::dirty() const +{ + return m_dirty; +} + +QColor TextEditor::backgroundColor() const +{ + return m_highlighter->backgroundColor(); +} + +void TextEditor::setBackgroundColor(const QColor& c) +{ + if (m_highlighter->backgroundColor() == c) { + return; + } + + m_highlighter->setBackgroundColor(c); + + setStyleSheet(QString("QPlainTextEdit{ background-color: rgba(%1, %2, %3, %4); }") + .arg(c.redF() * 255) + .arg(c.greenF() * 255) + .arg(c.blueF() * 255) + .arg(c.alphaF())); +} + +QColor TextEditor::textColor() const +{ + return m_highlighter->textColor(); +} + +void TextEditor::setTextColor(const QColor& c) +{ + m_highlighter->setTextColor(c); +} + +QColor TextEditor::highlightBackgroundColor() const +{ + return m_highlightBackground; +} + +void TextEditor::setHighlightBackgroundColor(const QColor& c) +{ + m_highlightBackground = c; + update(); +} + +void TextEditor::explore() +{ + if (m_filename.isEmpty()) { + return; + } + + MOBase::shell::ExploreFile(m_filename); +} + +void TextEditor::onModified(bool b) +{ + if (m_loading) { + return; + } + + dirty(b); + emit modified(b); +} + +void TextEditor::setupToolbar() +{ + auto* widget = wrapEditWidget(); + if (!widget) { + return; + } + + auto* layout = new QVBoxLayout(widget); + + // adding toolbar and edit + layout->addWidget(m_toolbar); + layout->addWidget(this); + + // make the edit stretch + layout->setStretch(0, 0); + layout->setStretch(1, 1); + + // visuals + layout->setContentsMargins(0, 0, 0, 0); + widget->show(); +} + +QWidget* TextEditor::wrapEditWidget() +{ + auto widget = std::make_unique<QWidget>(); + + // wrapping the QPlainTextEdit into a new widget so the toolbar can be + // displayed above it + + if (auto* parentLayout=parentWidget()->layout()) { + // the edit's parent has a regular layout, replace the edit by the new + // widget and delete the QLayoutItem that's returned as it's not needed + delete parentLayout->replaceWidget(this, widget.get()); + + } else if (auto* splitter=qobject_cast<QSplitter*>(parentWidget())) { + // the edit's parent is a QSplitter, which doesn't have a layout; replace + // the edit by using its index in the splitter + auto index = splitter->indexOf(this); + + if (index == -1) { + qCritical( + "TextEditor: cannot wrap edit widget to display a toolbar, " + "parent is a splitter, but widget isn't in it"); + + return nullptr; + } + + splitter->replaceWidget(index, widget.get()); + + } else { + // unknown parent + qCritical( + "TextEditor: cannot wrap edit widget to display a toolbar, " + "no parent or parent has no layout"); + + return nullptr; + } + + return widget.release(); +} + +void TextEditor::resizeEvent(QResizeEvent* e) +{ + QPlainTextEdit::resizeEvent(e); + + QRect cr = contentsRect(); + m_lineNumbers->setGeometry(QRect(cr.left(), cr.top(), m_lineNumbers->areaWidth(), cr.height())); +} + +void TextEditor::paintLineNumbers(QPaintEvent* e, const QColor& textColor) +{ + QStyleOption opt; + opt.init(m_lineNumbers); + + QPainter painter(m_lineNumbers); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); + int bottom = top + (int) blockBoundingRect(block).height(); + + while (block.isValid() && top <= e->rect().bottom()) { + if (block.isVisible() && bottom >= e->rect().top()) { + QString number = QString::number(blockNumber + 1); + painter.setPen(textColor); + + painter.drawText( + 0, top, m_lineNumbers->width() - 3, fontMetrics().height(), + Qt::AlignRight, number); + } + + block = block.next(); + top = bottom; + bottom = top + (int) blockBoundingRect(block).height(); + ++blockNumber; + } +} + +void TextEditor::highlightCurrentLine() +{ + QList<QTextEdit::ExtraSelection> extraSelections; + + if (!isReadOnly()) { + QTextEdit::ExtraSelection selection; + + QColor lineColor = QColor(Qt::yellow).lighter(160); + + selection.format.setBackground(m_highlightBackground); + selection.format.setProperty(QTextFormat::FullWidthSelection, true); + selection.cursor = textCursor(); + selection.cursor.clearSelection(); + extraSelections.append(selection); + } + + setExtraSelections(extraSelections); +} + + +TextEditorHighlighter::TextEditorHighlighter(QTextDocument* doc) : + QSyntaxHighlighter(doc), + m_background(QColor("transparent")), + m_text(QColor("black")) +{ +} + +QColor TextEditorHighlighter::backgroundColor() const +{ + return m_background; +} + +void TextEditorHighlighter::setBackgroundColor(const QColor& c) +{ + m_background = c; + changed(); +} + +QColor TextEditorHighlighter::textColor() const +{ + return m_text; +} + +void TextEditorHighlighter::setTextColor(const QColor& c) +{ + m_text = c; + changed(); +} + +void TextEditorHighlighter::highlightBlock(const QString& s) +{ + QTextCharFormat f; + f.setBackground(m_background); + f.setForeground(m_text); + + setFormat(0, s.size(), f); +} + +void TextEditorHighlighter::changed() +{ + rehighlight(); +} + + +TextEditorLineNumbers::TextEditorLineNumbers(TextEditor& editor) + : QFrame(&editor), m_editor(editor) +{ + setFont(editor.font()); + + connect(&m_editor, &QPlainTextEdit::blockCountChanged, [&]{ updateAreaWidth(); }); + connect(&m_editor, &QPlainTextEdit::updateRequest, [&](auto&& rect, int dy){ updateArea(rect, dy); }); + + updateAreaWidth(); +} + +QSize TextEditorLineNumbers::sizeHint() const +{ + return QSize(areaWidth(), 0); +} + +int TextEditorLineNumbers::areaWidth() const +{ + int digits = 1; + int max = std::max(1, m_editor.blockCount()); + + while (max >= 10) { + max /= 10; + ++digits; + } + + digits = std::max(3, digits); + + int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits + 3; + + return space; +} + +QColor TextEditorLineNumbers::textColor() const +{ + return m_text; +} + +void TextEditorLineNumbers::setTextColor(const QColor& c) +{ + m_text = c; + m_editor.update(); +} + +QColor TextEditorLineNumbers::backgroundColor() const +{ + return m_background; +} + +void TextEditorLineNumbers::setBackgroundColor(const QColor& c) +{ + m_background = c; + m_editor.update(); +} + +void TextEditorLineNumbers::paintEvent(QPaintEvent* e) +{ + QPainter painter(this); + painter.fillRect(e->rect(), m_background); + + QFrame::paintEvent(e); + m_editor.paintLineNumbers(e, m_text); +} + +void TextEditorLineNumbers::updateAreaWidth() +{ + m_editor.setViewportMargins(areaWidth(), 0, 0, 0); +} + +void TextEditorLineNumbers::updateArea(const QRect &rect, int dy) +{ + if (dy) { + scroll(0, dy); + } else { + update(0, rect.y(), width(), rect.height()); + } + + if (rect.contains(m_editor.viewport()->rect())) { + updateAreaWidth(); + } +} + + +TextEditorToolbar::TextEditorToolbar(TextEditor& editor) : + m_editor(editor), m_save(nullptr), m_wordWrap(nullptr), m_explore(nullptr), + m_path(nullptr) +{ + m_save = new QAction( + QIcon(":/MO/gui/save"), QObject::tr("&Save"), &editor); + + m_save->setShortcutContext(Qt::WidgetWithChildrenShortcut); + m_save->setShortcut(Qt::CTRL + Qt::Key_S); + m_editor.addAction(m_save); + + m_wordWrap = new QAction( + QIcon(":/MO/gui/word-wrap"), QObject::tr("&Word wrap"), &editor); + + m_wordWrap->setCheckable(true); + + m_explore = new QAction( + QObject::tr("&Open in Explorer"), &editor); + + m_path = new QLineEdit; + m_path->setReadOnly(true); + + QObject::connect(m_save, &QAction::triggered, [&]{ m_editor.save(); }); + QObject::connect(m_wordWrap, &QAction::triggered, [&]{ m_editor.toggleWordWrap(); }); + QObject::connect(m_explore, &QAction::triggered, [&]{ m_editor.explore(); }); + + auto* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setAlignment(Qt::AlignLeft); + + auto* b = new QToolButton; + b->setDefaultAction(m_save); + layout->addWidget(b); + + b = new QToolButton; + b->setDefaultAction(m_wordWrap); + layout->addWidget(b); + + b = new QToolButton; + b->setDefaultAction(m_explore); + layout->addWidget(b); + + layout->addWidget(m_path); + + QObject::connect(&m_editor, &TextEditor::modified, [&](bool b){ onTextModified(b); }); + QObject::connect(&m_editor, &TextEditor::wordWrapChanged, [&](bool b){ onWordWrap(b); }); + QObject::connect(&m_editor, &TextEditor::loaded, [&](QString f){ onLoaded(f); }); +} + +void TextEditorToolbar::onTextModified(bool b) +{ + m_save->setEnabled(b); +} + +void TextEditorToolbar::onWordWrap(bool b) +{ + m_wordWrap->setChecked(b); +} + +void TextEditorToolbar::onLoaded(const QString& path) +{ + const auto hasDoc = !path.isEmpty(); + + m_explore->setEnabled(hasDoc); + m_wordWrap->setEnabled(hasDoc); + m_path->setEnabled(hasDoc); + m_path->setText(path); +} + +void HTMLEditor::focusOutEvent(QFocusEvent* e) +{ + if (document() && document()->isModified()) { + emit editingFinished(); + } + + QTextEdit::focusInEvent(e); +} diff --git a/src/texteditor.h b/src/texteditor.h new file mode 100644 index 00000000..dc53f1c4 --- /dev/null +++ b/src/texteditor.h @@ -0,0 +1,165 @@ +#ifndef MO_TEXTEDITOR_H +#define MO_TEXTEDITOR_H + +#include <QPlainTextEdit> + +class TextEditor; + +class TextEditorToolbar : public QFrame +{ + Q_OBJECT; + +public: + TextEditorToolbar(TextEditor& editor); + +private: + TextEditor& m_editor; + QAction* m_save; + QAction* m_wordWrap; + QAction* m_explore; + QLineEdit* m_path; + + void onTextModified(bool b); + void onWordWrap(bool b); + void onLoaded(const QString& s); +}; + + +// mostly from https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html +// +class TextEditorLineNumbers : public QFrame +{ + Q_OBJECT; + Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor); + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor); + +public: + TextEditorLineNumbers(TextEditor& editor); + + QSize sizeHint() const override; + int areaWidth() const; + + QColor textColor() const; + void setTextColor(const QColor& c); + + QColor backgroundColor() const; + void setBackgroundColor(const QColor& c); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + TextEditor& m_editor; + QColor m_background, m_text; + + void updateAreaWidth(); + void updateArea(const QRect &rect, int dy); +}; + + +class TextEditorHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT; + +public: + TextEditorHighlighter(QTextDocument* doc); + + QColor backgroundColor() const; + void setBackgroundColor(const QColor& c); + + QColor textColor() const; + void setTextColor(const QColor& c); + +protected: + void highlightBlock(const QString& text) override; + +private: + QColor m_background, m_text; + + void changed(); +}; + + +class TextEditor : public QPlainTextEdit +{ + Q_OBJECT; + Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor); + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor); + Q_PROPERTY(QColor highlightBackgroundColor READ highlightBackgroundColor WRITE setHighlightBackgroundColor); + + friend class TextEditorLineNumbers; + +public: + TextEditor(QWidget* parent=nullptr); + + void setupToolbar(); + + void clear(); + bool load(const QString& filename); + bool save(); + + const QString& filename() const; + + void wordWrap(bool b); + void toggleWordWrap(); + bool wordWrap() const; + + bool dirty() const; + + QColor backgroundColor() const; + void setBackgroundColor(const QColor& c); + + QColor textColor() const; + void setTextColor(const QColor& c); + + QColor highlightBackgroundColor() const; + void setHighlightBackgroundColor(const QColor& c); + + void explore(); + +signals: + void loaded(QString filename); + void modified(bool b); + void wordWrapChanged(bool b); + +protected: + void resizeEvent(QResizeEvent* e) override; + +private: + TextEditorToolbar* m_toolbar; + TextEditorLineNumbers* m_lineNumbers; + TextEditorHighlighter* m_highlighter; + QColor m_highlightBackground; + QString m_filename; + QString m_encoding; + bool m_dirty; + bool m_loading; + + void setDefaultStyle(); + void onModified(bool b); + void dirty(bool b); + + QWidget* wrapEditWidget(); + + void highlightCurrentLine(); + void paintLineNumbers(QPaintEvent* e, const QColor& textColor); +}; + + +class HTMLEditor : public QTextEdit +{ + Q_OBJECT; + +public: + using QTextEdit::QTextEdit; + +signals: + void editingFinished(); + +protected: + void focusOutEvent(QFocusEvent* e); + +private: +}; + +#endif // MO_TEXTEDITOR_H diff --git a/src/tutorials/tutorial_conflictresolution_main.js b/src/tutorials/tutorial_conflictresolution_main.js index 3c782af2..a2aa278d 100644 --- a/src/tutorials/tutorial_conflictresolution_main.js +++ b/src/tutorials/tutorial_conflictresolution_main.js @@ -56,7 +56,7 @@ function getTutorialSteps() { waitForClick()
},
function() {
- tutorial.text = qsTr("<img src=\"qrc:///MO/gui/emblem_conflict_redundant\" /> indicates that the mod is completely overwrtten by another. You could as well disable it.");
+ tutorial.text = qsTr("<img src=\"qrc:///MO/gui/emblem_conflict_redundant\" /> indicates that the mod is completely overwritten by another. You could as well disable it.");
waitForClick()
},
function() {
@@ -65,7 +65,7 @@ function getTutorialSteps() { },
function() {
tutorial.text = qsTr("Option A: Switch to the \"Data\"-tab if necessary")
- if (!tutorialControl.waitForTabOpen("tabWidget", 2)) {
+ if (!tutorialControl.waitForTabOpen("tabWidget", "dataTab")) {
highlightItem("tabWidget", false)
waitForClick()
} else {
@@ -107,7 +107,7 @@ function getTutorialSteps() { function() {
tutorial.text = qsTr("Please open the \"Plugins\"-tab...")
highlightItem("tabWidget", true)
- if (!tutorialControl.waitForTabOpen("tabWidget", 0)) {
+ if (!tutorialControl.waitForTabOpen("tabWidget", "espTab")) {
nextStep()
}
},
diff --git a/src/tutorials/tutorial_conflictresolution_modinfo.js b/src/tutorials/tutorial_conflictresolution_modinfo.js index b5ef461e..04691ef6 100644 --- a/src/tutorials/tutorial_conflictresolution_modinfo.js +++ b/src/tutorials/tutorial_conflictresolution_modinfo.js @@ -3,7 +3,7 @@ function getTutorialSteps() { function() {
tutorial.text = qsTr("Please switch to the \"Conflicts\"-Tab.")
highlightItem("tabWidget", true)
- if (!tutorialControl.waitForTabOpen("tabWidget", 4)) {
+ if (!tutorialControl.waitForTabOpen("tabWidget", "tabConflicts")) {
nextStep()
}
},
diff --git a/src/tutorials/tutorial_firststeps_main.js b/src/tutorials/tutorial_firststeps_main.js index ee97766b..96bfd0e7 100644 --- a/src/tutorials/tutorial_firststeps_main.js +++ b/src/tutorials/tutorial_firststeps_main.js @@ -27,8 +27,8 @@ function getTutorialSteps() function() {
console.log("next")
tutorial.text = qsTr("This button provides multiple sources of information and further tutorials.")
- if (tutorialControl.waitForButton("actionHelp")) {
- highlightItem("actionHelp", true)
+ if (tutorialControl.waitForAction("actionHelp")) {
+ highlightAction("actionHelp", true)
} else {
console.error("help button broken")
waitForClick()
@@ -36,6 +36,7 @@ function getTutorialSteps() },
function() {
+ unhighlight()
tutorial.text = qsTr("Finally there are tooltips on almost every part of Mod Organizer. If there is a control "
+ "in MO you don't understand, please try hovering over it to get a short description or "
+ "use \"Help on UI\" from the help menu to get a longer explanation")
@@ -108,11 +109,11 @@ function getTutorialSteps() tutorial.text = qsTr("Install a few more mods if you want, then enable mods by checking them in the left pane. "
+ "Mods that aren't enabled have no effect on the game whatsoever. ")
highlightItem("modList", true)
- modList.modlist_changed.connect(nextStep)
+ modList.tutorialModlistUpdate.connect(nextStep)
},
function() {
- modList.modlist_changed.disconnect(nextStep)
+ modList.tutorialModlistUpdate.disconnect(nextStep)
unhighlight()
tutorial.text = qsTr("For some mods, enabling it on the left pane is all you have to do...")
waitForClick()
@@ -122,7 +123,7 @@ function getTutorialSteps() tutorial.text = qsTr("...but most contain plugins. These are plugins for the game and are required "
+"to add stuff to the game (new weapons, armors, quests, areas, ...). "
+"Please open the \"Plugins\"-tab to get a list of plugins.")
- if (tutorialControl.waitForTabOpen("tabWidget", 0)) {
+ if (tutorialControl.waitForTabOpen("tabWidget", "espTab")) {
highlightItem("tabWidget", true)
} else {
waitForClick()
@@ -137,7 +138,7 @@ function getTutorialSteps() function() {
tutorial.text = qsTr("A single mod may contain zero, one or multiple esps. Some or all may be optional. "
- + "If in doubt, please consult the documentation of the indiviual mod. "
+ + "If in doubt, please consult the documentation of the individual mod. "
+ "To do so, right-click the mod and select \"Information\".")
highlightItem("modList", true)
manager.activateTutorial("ModInfoDialog", "tutorial_firststeps_modinfo.js")
diff --git a/src/tutorials/tutorial_firststeps_modinfo.js b/src/tutorials/tutorial_firststeps_modinfo.js index 50c38345..1ed0dab7 100644 --- a/src/tutorials/tutorial_firststeps_modinfo.js +++ b/src/tutorials/tutorial_firststeps_modinfo.js @@ -16,7 +16,7 @@ function getTutorialSteps() },
function() {
unhighlight()
- tutorial.text = qsTr("We may re-visit this screen in later tutorials.")
+ tutorial.text = qsTr("We may revisit this screen in later tutorials.")
waitForClick()
}
]
diff --git a/src/tutorials/tutorial_firststeps_settings.js b/src/tutorials/tutorial_firststeps_settings.js index 1dd77d2e..b0e0c3c3 100644 --- a/src/tutorials/tutorial_firststeps_settings.js +++ b/src/tutorials/tutorial_firststeps_settings.js @@ -4,7 +4,7 @@ function getTutorialSteps() function() {
highlightItem("tabWidget", true)
tutorial.text = qsTr("You can use your regular browser to download from Nexus.\nPlease open the \"Nexus\"-tab")
- tutorialControl.waitForTabOpen("tabWidget", 2)
+ tutorialControl.waitForTabOpen("tabWidget", "nexusTab")
},
function() {
@@ -16,9 +16,11 @@ function getTutorialSteps() function() {
highlightItem("nexusBox", false)
- tutorial.text = qsTr("You can also store your Nexus-credentials "
- +"here for automatic login. The password is "
- +"stored unencrypted on your disk!")
+ tutorial.text = qsTr("Use this interface to obtain an API key from NexusMods. "
+ +"This is used for all API connections - downloads, updates "
+ +"etc. MO2 uses the Windows Credential Manager to store "
+ +"this data securely. If the SSO page on Nexus is failing, "
+ +"use the manual entry and copy the API key from your profile.")
waitForClick()
}
]
diff --git a/src/tutorials/tutorial_primer_main.js b/src/tutorials/tutorial_primer_main.js index 5af99237..7972cca4 100644 --- a/src/tutorials/tutorial_primer_main.js +++ b/src/tutorials/tutorial_primer_main.js @@ -42,6 +42,7 @@ function finishCreation(component, widgetName, explanation, maxheight, clickable function tooltipAction(actionName, explanation, maxheight, clickable) {
var rect = tutorialControl.getActionRect(actionName)
+ var offsetRect = tutorialControl.getMenuRect(actionName)
var component = Qt.createComponent("TooltipArea.qml")
if (typeof clickable === 'undefined') {
clickable = false
@@ -51,7 +52,7 @@ function tooltipAction(actionName, explanation, maxheight, clickable) { }
var obj = component.createObject(tutToplevel,
{ "x" : rect.x,
- "y" : rect.y,
+ "y" : rect.y + offsetRect.height,
"width" : rect.width,
"height" : maxheight
})
@@ -70,6 +71,11 @@ function setupTooptips() { tooltipWidget("modList", qsTr("This window shows all the mods that are installed. The column headers can be used for sorting. Only checked mods are active in the current profile."))
tooltipWidget("profileBox", qsTr("Each profile is a separate set of enabled mods and ini settings."))
+ tooltipWidget("listOptionsBtn", qsTr("Perform various actions on your mod list, such as refreshing data and checking for mod updates."))
+ tooltipWidget("openFolderMenu", qsTr("Quick access to various directories, such as your MO2 mods, profiles, saves, and your active game location."))
+ tooltipWidget("restoreModsButton", qsTr("Restore a mod list backup."))
+ tooltipWidget("saveModsButton", qsTr("Create a backup of your current mod list."))
+ tooltipWidget("activeModsCounter", qsTr("Running counter of your active mods. Hover to see a more detailed breakdown."))
tooltipWidget("groupCombo", qsTr("The dropdown allows various ways of grouping the mods shown in the mod list."))
tooltipWidget("displayCategoriesBtn", qsTr("Show/hide the category pane."))
tooltipWidget("modFilterEdit", qsTr("Quickly filter the mod list as you type."))
@@ -79,27 +85,39 @@ function setupTooptips() { tooltipWidget("startButton", qsTr("When this button is clicked, Mod Organizer creates a virtual directory structure then runs the program selected to the left."))
tooltipWidget("linkButton", qsTr("Will create a shortcut for quick access. The shortcut can be placed in the toolbar at the top, in the Start Menu or on the Windows Desktop."))
tooltipWidget("logList", qsTr("Log messages produced by MO. Please note that messages with a light bulb usually don't require your attention."))
+ tooltipWidget("apistats", qsTr("Indicator of your current NexusMods API request limits."))
+ tooltipAction("actionChange_Game", qsTr("Change/manage MO2 instances or switch to portable mode."))
+ tooltipAction("actionInstallMod", qsTr("Browse to and manually install a mod from an archive on your computer."))
+ tooltipAction("actionNexus", qsTr("Automatically open NexusMods to browse and install mods via the API."))
+ tooltipAction("actionAdd_Profile", qsTr("Manage your MO2 profiles."))
+ tooltipAction("actionModify_Executables", qsTr("Open the executable editor to add and modify applications you wish to run with MO2."))
+ tooltipAction("actionTool", qsTr("Select from a collection of additional tools, such as an INI editor, integrated FNIS updater, and more."))
tooltipAction("actionSettings", qsTr("Configure Mod Organizer."))
+ tooltipAction("actionEndorseMO", qsTr("See the status of and/or endorse MO2 on NexusMods."))
tooltipAction("actionNotifications", qsTr("Notifications about the current setup."))
tooltipAction("actionUpdate", qsTr("Activates if there is an update for MO. Please note that if, for any reason, MO can't communicate with NMM, this will not work either."))
+ tooltipAction("actionHelp", qsTr("Access more information about MO2, including these tutorials, a link to the development discord, information about the devs and dependencies."))
- switch (manager.findControl("tabWidget").currentIndex) {
- case 0:
+ switch (tutorialControl.getTabName("tabWidget")) {
+ case "espTab":
tooltipWidget("espList", qsTr("Plugins (esp/esm/esl files) of the mods in the current profile. They need to be checked to be loaded."))
tooltipWidget("bossButton", qsTr("Automatically sort plugins using the bundled LOOT application."))
+ tooltipWidget("restoreButton", qsTr("Restore a backup of your plugin list order."))
+ tooltipWidget("saveButton", qsTr("Save a backup of your plugin list order."))
+ tooltipWidget("activePluginsCounter", qsTr("Counter of your total active plugins. Hover to see a breakdown of plugin types."))
tooltipWidget("espFilterEdit", qsTr("Quickly filter plugin list as you type."))
break
- // case 1:
- // tooltipWidget("bsaList", qsTr("All the asset archives (.bsa files) for all active mods."))
- // break
- case 1:
+ case "bsaTab":
+ tooltipWidget("bsaList", qsTr("All the asset archives (.bsa files) for all active mods."))
+ break
+ case "dataTab":
tooltipWidget("dataTree", qsTr("The directory tree and all files that the program will see."))
break
- case 2:
+ case "savesTab":
tooltipWidget("savegameList", qsTr("Save game browser. Shows all the saves for the current profile and whether or not the current mod-load status is correct."))
break
- case 3:
+ case "downloadTab":
tooltipWidget("downloadView", qsTr("Shows the mods that have been downloaded and if they’ve been installed."))
break
}
diff --git a/src/tutorials/tutorials.js b/src/tutorials/tutorials.js index eb23eab7..cdb16d59 100644 --- a/src/tutorials/tutorials.js +++ b/src/tutorials/tutorials.js @@ -18,8 +18,9 @@ function highlightItem(widgetName, click) { function highlightAction(actionName, click) {
var rect = tutorialControl.getActionRect(actionName)
+ var offsetRect = tutorialControl.getMenuRect(actionName)
highlight.x = rect.x - 1
- highlight.y = rect.y - 1
+ highlight.y = rect.y + offsetRect.height
highlight.width = rect.width + 2
highlight.height = rect.height + 2
if (click) {
diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp index 5ad19fb0..b752667d 100644 --- a/src/usvfsconnector.cpp +++ b/src/usvfsconnector.cpp @@ -92,7 +92,7 @@ void LogWorker::exit() LogLevel logLevel(int level) { - switch (level) { + switch (static_cast<LogLevel>(level)) { case LogLevel::Info: return LogLevel::Info; case LogLevel::Warning: @@ -106,7 +106,7 @@ LogLevel logLevel(int level) CrashDumpsType crashDumpsType(int type) { - switch (type) { + switch (static_cast<CrashDumpsType>(type)) { case CrashDumpsType::Mini: return CrashDumpsType::Mini; case CrashDumpsType::Data: diff --git a/src/version.rc b/src/version.rc index 117bb015..d31c746e 100644 --- a/src/version.rc +++ b/src/version.rc @@ -1,37 +1,37 @@ -#include "Winver.h"
-
-// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number.
-// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser
-// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha
-#define VER_FILEVERSION 2,2,0
-#define VER_FILEVERSION_STR "2.2.0\0"
-
-VS_VERSION_INFO VERSIONINFO
-FILEVERSION VER_FILEVERSION
-PRODUCTVERSION VER_FILEVERSION
-FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
-FILEFLAGS (0)
-FILEOS VOS__WINDOWS32
-FILETYPE VFT_APP
-FILESUBTYPE (0)
-BEGIN
- BLOCK "StringFileInfo"
- BEGIN
- BLOCK "040904B0"
- BEGIN
- VALUE "FileVersion", VER_FILEVERSION_STR
- VALUE "CompanyName", "Mod Organizer 2 Team\0"
- VALUE "FileDescription", "Mod Organizer 2 GUI\0"
- VALUE "OriginalFilename", "ModOrganizer.exe\0"
- VALUE "InternalName", "ModOrganizer2\0"
- VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2019 Mod Organizer 2 contributors\0"
- VALUE "ProductName", "Mod Organizer 2\0"
- VALUE "ProductVersion", VER_FILEVERSION_STR
- END
- END
-
- BLOCK "VarFileInfo"
- BEGIN
- VALUE "Translation", 0x0409L, 1200
- END
-END
+#include "Winver.h" + +// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number. +// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser +// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha +#define VER_FILEVERSION 2,2,1 +#define VER_FILEVERSION_STR "2.2.1\0" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_FILEVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE (0) +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "Mod Organizer 2 GUI\0" + VALUE "OriginalFilename", "ModOrganizer.exe\0" + VALUE "InternalName", "ModOrganizer2\0" + VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2019 Mod Organizer 2 contributors\0" + VALUE "ProductName", "Mod Organizer 2\0" + VALUE "ProductVersion", VER_FILEVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END +END |
