summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLostDragonist <lost.dragonist@gmail.com>2019-05-03 21:32:33 -0500
committerLostDragonist <lost.dragonist@gmail.com>2019-05-03 21:32:33 -0500
commit179a73857125ee604f42b0d5c2d765183c86d2c7 (patch)
treeb9b3f9d62bd5640de839d150a53ab8ef119dab9c /src
parente2b799bd6b5cfd51969fefd1dab5e5b1b7e5f81c (diff)
parent907c5468424b48774f5da2a6b5f96f26590987b0 (diff)
Merge pull request #695 from ModOrganizer2/Develop
Stage for Release 2.2.0
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt46
-rw-r--r--src/aboutdialog.ui44
-rw-r--r--src/bbcode.cpp4
-rw-r--r--src/categories.cpp12
-rw-r--r--src/credentialsdialog.cpp10
-rw-r--r--src/credentialsdialog.h3
-rw-r--r--src/directoryrefresher.cpp44
-rw-r--r--src/directoryrefresher.h1
-rw-r--r--src/dlls.manifest.qt53
-rw-r--r--src/downloadlist.cpp104
-rw-r--r--src/downloadlist.h9
-rw-r--r--src/downloadlistsortproxy.cpp27
-rw-r--r--src/downloadlistwidget.cpp476
-rw-r--r--src/downloadlistwidget.h80
-rw-r--r--src/downloadlistwidget.ui140
-rw-r--r--src/downloadlistwidgetcompact.cpp413
-rw-r--r--src/downloadlistwidgetcompact.h131
-rw-r--r--src/downloadlistwidgetcompact.ui168
-rw-r--r--src/downloadmanager.cpp3922
-rw-r--r--src/downloadmanager.h1143
-rw-r--r--src/editexecutablesdialog.cpp89
-rw-r--r--src/editexecutablesdialog.h14
-rw-r--r--src/editexecutablesdialog.ui37
-rw-r--r--src/executableslist.cpp23
-rw-r--r--src/forcedloaddialog.cpp77
-rw-r--r--src/forcedloaddialog.h34
-rw-r--r--src/forcedloaddialog.ui117
-rw-r--r--src/forcedloaddialogwidget.cpp108
-rw-r--r--src/forcedloaddialogwidget.h41
-rw-r--r--src/forcedloaddialogwidget.ui113
-rw-r--r--src/helper.cpp34
-rw-r--r--src/helper.h12
-rw-r--r--src/icondelegate.cpp2
-rw-r--r--src/installationmanager.cpp35
-rw-r--r--src/installationmanager.h9
-rw-r--r--src/instancemanager.cpp7
-rw-r--r--src/json.cpp522
-rw-r--r--src/json.h94
-rw-r--r--src/lcdnumber.cpp37
-rw-r--r--src/lcdnumber.h35
-rw-r--r--src/loadmechanism.cpp4
-rw-r--r--src/logbuffer.cpp52
-rw-r--r--src/main.cpp1444
-rw-r--r--src/mainwindow.cpp12935
-rw-r--r--src/mainwindow.h1306
-rw-r--r--src/mainwindow.ui3229
-rw-r--r--src/messagedialog.cpp2
-rw-r--r--src/messagedialog.ui129
-rw-r--r--src/moapplication.cpp2
-rw-r--r--src/modflagicondelegate.cpp99
-rw-r--r--src/modflagicondelegate.h15
-rw-r--r--src/modinfo.cpp181
-rw-r--r--src/modinfo.h1475
-rw-r--r--src/modinfobackup.h11
-rw-r--r--src/modinfodialog.cpp3063
-rw-r--r--src/modinfodialog.h4
-rw-r--r--src/modinfodialog.ui17
-rw-r--r--src/modinfoforeign.h15
-rw-r--r--src/modinfooverwrite.cpp7
-rw-r--r--src/modinfooverwrite.h21
-rw-r--r--src/modinforegular.cpp193
-rw-r--r--src/modinforegular.h80
-rw-r--r--src/modinfoseparator.h27
-rw-r--r--src/modinfowithconflictinfo.cpp121
-rw-r--r--src/modinfowithconflictinfo.h28
-rw-r--r--src/modlist.cpp2791
-rw-r--r--src/modlist.h20
-rw-r--r--src/modlistsortproxy.cpp151
-rw-r--r--src/modlistsortproxy.h8
-rw-r--r--src/nexusinterface.cpp1783
-rw-r--r--src/nexusinterface.h978
-rw-r--r--src/nxmaccessmanager.cpp659
-rw-r--r--src/nxmaccessmanager.h250
-rw-r--r--src/organizer.pro18
-rw-r--r--src/organizer_en.ts2843
-rw-r--r--src/organizercore.cpp4867
-rw-r--r--src/organizercore.h24
-rw-r--r--src/organizerproxy.cpp13
-rw-r--r--src/organizerproxy.h4
-rw-r--r--src/overwriteinfodialog.cpp6
-rw-r--r--src/persistentcookiejar.cpp6
-rw-r--r--src/plugincontainer.cpp18
-rw-r--r--src/plugincontainer.h1
-rw-r--r--src/pluginlist.cpp60
-rw-r--r--src/pluginlistsortproxy.cpp41
-rw-r--r--src/pluginlistsortproxy.h2
-rw-r--r--src/problemsdialog.cpp15
-rw-r--r--src/problemsdialog.h6
-rw-r--r--src/problemsdialog.ui2
-rw-r--r--src/profile.cpp241
-rw-r--r--src/profile.h38
-rw-r--r--src/profilesdialog.cpp2
-rw-r--r--src/profilesdialog.ui6
-rw-r--r--src/resources.qrc8
-rw-r--r--src/resources/conflict-mixed-blue.pngbin0 -> 692 bytes
-rw-r--r--src/resources/conflict-overwrite-blue.pngbin0 -> 964 bytes
-rw-r--r--src/resources/conflict-overwritten-blue.pngbin0 -> 577 bytes
-rw-r--r--src/resources/red-archive-conflict-loser.pngbin0 -> 746 bytes
-rw-r--r--src/resources/tracked.pngbin0 -> 2284 bytes
-rw-r--r--src/selfupdater.cpp68
-rw-r--r--src/settings.cpp289
-rw-r--r--src/settings.h53
-rw-r--r--src/settingsdialog.cpp233
-rw-r--r--src/settingsdialog.h347
-rw-r--r--src/settingsdialog.ui2800
-rw-r--r--src/shared/directoryentry.cpp171
-rw-r--r--src/shared/directoryentry.h26
-rw-r--r--src/shared/leaktrace.cpp13
-rw-r--r--src/shared/util.cpp12
-rw-r--r--src/singleinstance.cpp2
-rw-r--r--src/spawn.cpp31
-rw-r--r--src/stylesheets/Night Eyes.qss24
-rw-r--r--src/stylesheets/Paper Automata.qss161
-rw-r--r--src/stylesheets/Paper Dark by 6788.qss116
-rw-r--r--src/stylesheets/Paper Light by 6788.qss104
-rw-r--r--src/stylesheets/Paper/Dark/check.svg10
-rw-r--r--src/stylesheets/Paper/Dark/highlight.svg205
-rw-r--r--src/stylesheets/Parchment v1.1 by Bob.qss587
-rw-r--r--src/stylesheets/Parchment/checkbox-alt-checked.pngbin0 -> 301 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-alt-unchecked-hover.pngbin0 -> 283 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-alt-unchecked.pngbin0 -> 253 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-checked-disabled.pngbin0 -> 309 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-checked-hover.pngbin0 -> 339 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-checked.pngbin0 -> 319 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-disabled.pngbin0 -> 259 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox-hover.pngbin0 -> 339 bytes
-rw-r--r--src/stylesheets/Parchment/checkbox.pngbin0 -> 259 bytes
-rw-r--r--src/stylesheets/dark.qss9
-rw-r--r--src/stylesheets/dracula.qss30
-rw-r--r--src/stylesheets/skyrim.qss10
-rw-r--r--src/stylesheets/vs15 Dark-Green.qss14
-rw-r--r--src/stylesheets/vs15 Dark-Orange.qss14
-rw-r--r--src/stylesheets/vs15 Dark-Purple.qss14
-rw-r--r--src/stylesheets/vs15 Dark-Red.qss14
-rw-r--r--src/stylesheets/vs15 Dark-Yellow.qss14
-rw-r--r--src/stylesheets/vs15 Dark.qss14
-rw-r--r--src/syncoverwritedialog.cpp6
-rw-r--r--src/transfersavesdialog.cpp2
-rw-r--r--src/tutorials/tutorial_firststeps_main.js10
-rw-r--r--src/tutorials/tutorial_primer_main.js2
-rw-r--r--src/usvfsconnector.cpp20
-rw-r--r--src/usvfsconnector.h3
-rw-r--r--src/version.rc8
143 files changed, 28436 insertions, 24541 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3f004ddc..8b368257 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -54,7 +54,6 @@ SET(organizer_SRCS
executableslist.cpp
editexecutablesdialog.cpp
downloadmanager.cpp
- downloadlistwidgetcompact.cpp
downloadlistwidget.cpp
downloadlistsortproxy.cpp
downloadlist.cpp
@@ -80,7 +79,6 @@ SET(organizer_SRCS
previewgenerator.cpp
previewdialog.cpp
aboutdialog.cpp
- json.cpp
modflagicondelegate.cpp
genericicondelegate.cpp
organizerproxy.cpp
@@ -92,6 +90,9 @@ SET(organizer_SRCS
eventfilter.cpp
moshortcut.cpp
listdialog.cpp
+ lcdnumber.cpp
+ forcedloaddialog.cpp
+ forcedloaddialogwidget.cpp
shared/windows_error.cpp
shared/error_report.cpp
@@ -146,7 +147,6 @@ SET(organizer_HDRS
executableslist.h
editexecutablesdialog.h
downloadmanager.h
- downloadlistwidgetcompact.h
downloadlistwidget.h
downloadlistsortproxy.h
downloadlist.h
@@ -172,7 +172,6 @@ SET(organizer_HDRS
previewgenerator.h
previewdialog.h
aboutdialog.h
- json.h
modflagicondelegate.h
genericicondelegate.h
organizerproxy.h
@@ -187,6 +186,9 @@ SET(organizer_HDRS
descriptionpage.h
moshortcut.h
listdialog.h
+ lcdnumber.h
+ forcedloaddialog.h
+ forcedloaddialogwidget.h
shared/windows_error.h
shared/error_report.h
@@ -216,8 +218,6 @@ SET(organizer_UIS
installdialog.ui
finddialog.ui
editexecutablesdialog.ui
- downloadlistwidgetcompact.ui
- downloadlistwidget.ui
credentialsdialog.ui
categoriesdialog.ui
activatemodsdialog.ui
@@ -228,6 +228,8 @@ SET(organizer_UIS
browserdialog.ui
aboutdialog.ui
listdialog.ui
+ forcedloaddialog.ui
+ forcedloaddialogwidget.ui
)
SET(organizer_QRCS
@@ -246,7 +248,8 @@ SOURCE_GROUP(Headers FILES ${organizer_HDRS})
SOURCE_GROUP(UI FILES ${organizer_UIS})
# MO projects
-SET(default_project_path "${CMAKE_SOURCE_DIR}/..")
+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")
@@ -263,6 +266,7 @@ FIND_PACKAGE(Qt5Quick REQUIRED)
FIND_PACKAGE(Qt5Network REQUIRED)
FIND_PACKAGE(Qt5WinExtras REQUIRED)
FIND_PACKAGE(Qt5WebEngineWidgets REQUIRED)
+FIND_PACKAGE(Qt5WebSockets REQUIRED)
FIND_PACKAGE(Qt5Qml REQUIRED)
FIND_PACKAGE(Qt5LinguistTools)
QT5_WRAP_UI(organizer_UIHDRS ${organizer_UIS})
@@ -292,7 +296,7 @@ INCLUDE_DIRECTORIES(${project_path}/uibase/src
${project_path}/bsatk/src
${project_path}/esptk/src
${project_path}/archive/src
- ${project_path}/../usvfs/include
+ ${dependency_project_path}/usvfs/include
${project_path}/game_gamebryo/src/gamebryo
${project_path}/game_gamebryo/src/creation
${project_path}/game_features/src
@@ -321,12 +325,12 @@ ENDIF()
ADD_EXECUTABLE(ModOrganizer WIN32 ${organizer_HDRS} ${organizer_SRCS} ${organizer_UIHDRS} ${organizer_RCS} ${organizer_RCCPPS} ${organizer_translations_qm})
TARGET_LINK_LIBRARIES(ModOrganizer
Qt5::Widgets Qt5::WinExtras Qt5::WebEngineWidgets Qt5::Quick
- Qt5::Qml Qt5::QuickWidgets Qt5::Network
+ Qt5::Qml Qt5::QuickWidgets Qt5::Network Qt5::WebSockets
${Boost_LIBRARIES}
zlibstatic
uibase esptk bsatk githubpp
${usvfs_name}
- Dbghelp advapi32 Version Shlwapi liblz4)
+ Dbghelp advapi32 Version Shlwapi liblz4 Crypt32)
IF (MSVC)
SET_TARGET_PROPERTIES(ModOrganizer PROPERTIES COMPILE_FLAGS "/std:c++latest")
@@ -359,17 +363,19 @@ INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt5
DESTINATION bin/dlls
RENAME dlls.manifest)
+INSTALL(FILES ${organizer_translations_qm} DESTINATION bin/translations)
+
+INSTALL(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/stylesheets
+ ${CMAKE_CURRENT_SOURCE_DIR}/tutorials
+ DESTINATION bin)
+
+#We should do this last, since windeployqt.exe scans the folders in bin for required modules e.g QtQuick.2 required by tutorials.
# use windeployqt.exe to install all required libraries
SET(windeploy_parameters "--no-translations --plugindir qtplugins --libdir dlls --release-with-debug-info --no-compiler-runtime")
INSTALL(
CODE
"EXECUTE_PROCESS(COMMAND
- ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets ${windeploy_parameters}
- WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin
- )
- # run it a second time because on the first run it misses some files
- EXECUTE_PROCESS(COMMAND
- ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets ${windeploy_parameters}
+ ${qt5bin}/windeployqt.exe ModOrganizer.exe --webenginewidgets --websockets ${windeploy_parameters}
WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin
)
EXECUTE_PROCESS(COMMAND
@@ -386,10 +392,4 @@ INSTALL(
)
# qdds.dll needs installing manually as Qt no longer ships with it by default.
-INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats)
-
-install(FILES ${organizer_translations_qm} DESTINATION bin/translations)
-
-INSTALL(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/stylesheets
- ${CMAKE_CURRENT_SOURCE_DIR}/tutorials
- DESTINATION bin)
+INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) \ No newline at end of file
diff --git a/src/aboutdialog.ui b/src/aboutdialog.ui
index 3a6daa82..86b95c83 100644
--- a/src/aboutdialog.ui
+++ b/src/aboutdialog.ui
@@ -157,7 +157,7 @@
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="title">
- <string>Current Maintainers</string>
+ <string>Lead Developers/ Maintainers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
@@ -177,11 +177,6 @@
</item>
<item>
<property name="text">
- <string notr="true">Diana</string>
- </property>
- </item>
- <item>
- <property name="text">
<string notr="true">erasmux</string>
</property>
</item>
@@ -190,7 +185,7 @@
<string notr="true">Silarn</string>
</property>
</item>
- <item>
+ <item>
<property name="text">
<string notr="true">LostDragonist</string>
</property>
@@ -203,11 +198,11 @@
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
- <string>Major Contributors</string>
+ <string>Mo2 devs and Contributors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7" stretch="0">
<item>
- <widget class="QListWidget" name="listWidget_2">
+ <widget class="QListWidget" name="listWidget_3">
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
@@ -216,6 +211,16 @@
<string notr="true">AnyOldName3</string>
</property>
</item>
+ <item>
+ <property name="text">
+ <string>Project579</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>przester</string>
+ </property>
+ </item>
</widget>
</item>
</layout>
@@ -226,9 +231,9 @@
<property name="title">
<string>Translators</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_6">
+ <layout class="QVBoxLayout" name="verticalLayout_61">
<item>
- <widget class="QListWidget" name="listWidget_2">
+ <widget class="QListWidget" name="listWidget_4">
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
@@ -312,6 +317,11 @@
</item>
<item>
<property name="text">
+ <string>zDas (Portuguese)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
<string notr="true">tokcdk (Russian)</string>
</property>
</item>
@@ -332,6 +342,11 @@
</item>
<item>
<property name="text">
+ <string>yohru (Japanese)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
<string>...and all other Transifex contributors!</string>
</property>
<property name="toolTip">
@@ -348,7 +363,7 @@
<property name="title">
<string>Other Supporters &amp;&amp; Contributors</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_7">
+ <layout class="QVBoxLayout" name="verticalLayout_71">
<item>
<widget class="QListWidget" name="listWidget">
<property name="selectionMode">
@@ -356,6 +371,11 @@
</property>
<item>
<property name="text">
+ <string>Tannin (Original Creator)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
<string notr="true">blacksol</string>
</property>
</item>
diff --git a/src/bbcode.cpp b/src/bbcode.cpp
index 56369538..3475f1b2 100644
--- a/src/bbcode.cpp
+++ b/src/bbcode.cpp
@@ -88,7 +88,7 @@ public:
return temp.replace(tagIter->second.first, QString("<font style=\"color: #%1;\">%2</font>").arg(color, content));
}
} else {
- qWarning("don't know how to deal with tag %s", qPrintable(tagName));
+ qWarning("don't know how to deal with tag %s", qUtf8Printable(tagName));
}
} else {
if (tagName == "*") {
@@ -100,7 +100,7 @@ public:
// expression doesn't match. either the input string is invalid
// or the expression is
qWarning("%s doesn't match the expression for %s",
- temp.toUtf8().constData(), tagName.toUtf8().constData());
+ qUtf8Printable(temp), qUtf8Printable(tagName));
length = 0;
return QString();
}
diff --git a/src/categories.cpp b/src/categories.cpp
index 4d89eff9..9e5fa9f7 100644
--- a/src/categories.cpp
+++ b/src/categories.cpp
@@ -72,7 +72,7 @@ void CategoryFactory::loadCategories()
bool ok = false;
int temp = iter->toInt(&ok);
if (!ok) {
- qCritical("invalid id %s", iter->constData());
+ qCritical("invalid category id %s", iter->constData());
}
nexusIDs.push_back(temp);
}
@@ -268,7 +268,7 @@ void CategoryFactory::loadDefaultCategories()
int CategoryFactory::getParentID(unsigned int index) const
{
if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid index %1").arg(index));
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
}
return m_Categories[index].m_ParentID;
@@ -303,7 +303,7 @@ bool CategoryFactory::isDecendantOf(int id, int parentID) const
bool CategoryFactory::hasChildren(unsigned int index) const
{
if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid index %1").arg(index));
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
}
return m_Categories[index].m_HasChildren;
@@ -313,7 +313,7 @@ bool CategoryFactory::hasChildren(unsigned int index) const
QString CategoryFactory::getCategoryName(unsigned int index) const
{
if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid index %1").arg(index));
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
}
return m_Categories[index].m_Name;
@@ -323,7 +323,7 @@ QString CategoryFactory::getCategoryName(unsigned int index) const
int CategoryFactory::getCategoryID(unsigned int index) const
{
if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid index %1").arg(index));
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
}
return m_Categories[index].m_ID;
@@ -334,7 +334,7 @@ int CategoryFactory::getCategoryIndex(int ID) const
{
std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(ID);
if (iter == m_IDMap.end()) {
- throw MyException(QObject::tr("invalid category id %1").arg(ID));
+ throw MyException(QObject::tr("invalid category id: %1").arg(ID));
}
return iter->second;
}
diff --git a/src/credentialsdialog.cpp b/src/credentialsdialog.cpp
index 04774548..73e75387 100644
--- a/src/credentialsdialog.cpp
+++ b/src/credentialsdialog.cpp
@@ -32,16 +32,6 @@ CredentialsDialog::~CredentialsDialog()
delete ui;
}
-QString CredentialsDialog::username() const
-{
- return ui->usernameEdit->text();
-}
-
-QString CredentialsDialog::password() const
-{
- return ui->passwordEdit->text();
-}
-
bool CredentialsDialog::store() const
{
return ui->rememberCheck->isChecked();
diff --git a/src/credentialsdialog.h b/src/credentialsdialog.h
index 8a68c7d8..2f3bcbb7 100644
--- a/src/credentialsdialog.h
+++ b/src/credentialsdialog.h
@@ -34,9 +34,6 @@ public:
explicit CredentialsDialog(QWidget *parent = 0);
~CredentialsDialog();
- QString username() const;
- QString password() const;
-
bool store() const;
bool neverAsk() const;
diff --git a/src/directoryrefresher.cpp b/src/directoryrefresher.cpp
index 272b0596..3ce4691b 100644
--- a/src/directoryrefresher.cpp
+++ b/src/directoryrefresher.cpp
@@ -23,10 +23,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "utility.h"
#include "report.h"
#include "modinfo.h"
+#include "settings.h"
#include <QApplication>
#include <QDir>
#include <QString>
+#include <QTextCodec>
+#include <gameplugins.h>
using namespace MOBase;
@@ -83,13 +86,29 @@ void DirectoryRefresher::addModBSAToStructure(DirectoryEntry *directoryStructure
int priority, const QString &directory, const QStringList &archives)
{
std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
+ IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
+
+ GamePlugins *gamePlugins = game->feature<GamePlugins>();
+ QStringList loadOrder = QStringList();
+ gamePlugins->getLoadOrder(loadOrder);
for (const QString &archive : archives) {
QFileInfo fileInfo(archive);
if (m_EnabledArchives.find(fileInfo.fileName()) != m_EnabledArchives.end()) {
+
+ int order = -1;
+
+ for (auto plugin : loadOrder)
+ {
+ QString name = plugin.left(plugin.size() - 4);
+ if (fileInfo.fileName().startsWith(name + " - ", Qt::CaseInsensitive) || fileInfo.fileName().startsWith(name + ".", Qt::CaseInsensitive)) {
+ order = loadOrder.indexOf(plugin);
+ }
+ }
+
try {
- //directoryStructure->addFromBSA(ToWString(modName), directoryW,
- // ToWString(QDir::toNativeSeparators(fileInfo.absoluteFilePath())), priority);
+ IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
+ directoryStructure->addFromBSA(ToWString(modName), directoryW, ToWString(QDir::toNativeSeparators(fileInfo.absoluteFilePath())), priority, order);
} catch (const std::exception &e) {
throw MyException(tr("failed to parse bsa %1: %2").arg(archive, e.what()));
}
@@ -107,6 +126,10 @@ void DirectoryRefresher::addModFilesToStructure(DirectoryEntry *directoryStructu
// files to this mod
FilesOrigin &origin = directoryStructure->createOrigin(ToWString(modName), directoryW, priority);
for (const QString &filename : stealFiles) {
+ if (filename.isEmpty()) {
+ qWarning("Trying to find file with no name");
+ continue;
+ }
QFileInfo fileInfo(filename);
FileEntry::Ptr file = directoryStructure->findFile(ToWString(fileInfo.fileName()));
if (file.get() != nullptr) {
@@ -115,9 +138,12 @@ void DirectoryRefresher::addModFilesToStructure(DirectoryEntry *directoryStructu
file->removeOrigin(0);
}
origin.addFile(file->getIndex());
- file->addOrigin(origin.getID(), file->getFileTime(), L"");
+ file->addOrigin(origin.getID(), file->getFileTime(), L"", -1);
} else {
- qWarning("%s not found", qPrintable(fileInfo.fileName()));
+ QString warnStr = fileInfo.absolutePath();
+ if (warnStr.isEmpty())
+ warnStr = filename;
+ qWarning("file not found: %s", qUtf8Printable(warnStr));
}
}
} else {
@@ -133,7 +159,10 @@ void DirectoryRefresher::addModToStructure(DirectoryEntry *directoryStructure
, const QStringList &archives)
{
addModFilesToStructure(directoryStructure, modName, priority, directory, stealFiles);
- addModBSAToStructure(directoryStructure, modName, priority, directory, archives);
+
+ if (Settings::instance().archiveParsing()) {
+ addModBSAToStructure(directoryStructure, modName, priority, directory, archives);
+ }
}
void DirectoryRefresher::refresh()
@@ -149,10 +178,9 @@ void DirectoryRefresher::refresh()
std::wstring dataDirectory = QDir::toNativeSeparators(game->dataDirectory().absolutePath()).toStdWString();
m_DirectoryStructure->addFromOrigin(L"data", dataDirectory, 0);
- // TODO what was the point of having the priority in this tuple? the list is already sorted by priority
+ std::sort(m_Mods.begin(), m_Mods.end(), [](auto lhs, auto rhs){return lhs.priority < rhs.priority;});
auto iter = m_Mods.begin();
- //TODO i is the priority here, where higher = more important. the input vector is also sorted by priority but inverted!
for (int i = 1; iter != m_Mods.end(); ++iter, ++i) {
try {
addModToStructure(m_DirectoryStructure, iter->modName, i, iter->absolutePath, iter->stealFiles, iter->archives);
@@ -162,6 +190,8 @@ void DirectoryRefresher::refresh()
emit progress((i * 100) / static_cast<int>(m_Mods.size()) + 1);
}
+ m_DirectoryStructure->getFileRegister()->sortOrigins();
+
emit progress(100);
cleanStructure(m_DirectoryStructure);
diff --git a/src/directoryrefresher.h b/src/directoryrefresher.h
index 53709e1e..a4fc5dbc 100644
--- a/src/directoryrefresher.h
+++ b/src/directoryrefresher.h
@@ -27,6 +27,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <vector>
#include <set>
#include <tuple>
+#include "profile.h"
/**
diff --git a/src/dlls.manifest.qt5 b/src/dlls.manifest.qt5
index 924512d9..88843b77 100644
--- a/src/dlls.manifest.qt5
+++ b/src/dlls.manifest.qt5
@@ -21,6 +21,7 @@
<file name="Qt5Svg.dll"/>
<file name="Qt5WebEngineCore.dll"/>
<file name="Qt5WebEngineWidgets.dll"/>
+ <file name="Qt5WebSockets.dll"/>
<file name="Qt5Widgets.dll"/>
<file name="Qt5WinExtras.dll"/>
-</assembly> \ No newline at end of file
+</assembly>
diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp
index 1e31973d..dd72abbd 100644
--- a/src/downloadlist.cpp
+++ b/src/downloadlist.cpp
@@ -20,6 +20,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "downloadlist.h"
#include "downloadmanager.h"
#include <QEvent>
+#include <QColor>
+#include <QIcon>
#include <QSortFilterProxyModel>
@@ -31,6 +33,11 @@ DownloadList::DownloadList(DownloadManager *manager, QObject *parent)
connect(m_Manager, SIGNAL(aboutToUpdate()), this, SLOT(aboutToUpdate()));
}
+void DownloadList::setMetaDisplay(bool metaDisplay)
+{
+ m_MetaDisplay = metaDisplay;
+}
+
int DownloadList::rowCount(const QModelIndex&) const
{
@@ -62,36 +69,86 @@ QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int
(orientation == Qt::Horizontal)) {
switch (section) {
case COL_NAME: return tr("Name");
- case COL_FILETIME: return tr("Filetime");
case COL_SIZE: return tr("Size");
- default: return tr("Done");
+ case COL_STATUS: return tr("Status");
+ case COL_FILETIME: return tr("Filetime");
+ default: return QVariant();
}
} else {
return QAbstractItemModel::headerData(section, orientation, role);
}
}
-
QVariant DownloadList::data(const QModelIndex &index, int role) const
{
+ bool pendingDownload = index.row() >= m_Manager->numTotalDownloads();
if (role == Qt::DisplayRole) {
- return index.row();
+ if (pendingDownload) {
+ std::tuple<QString, int, int> nexusids = m_Manager->getPendingDownload(index.row() - m_Manager->numTotalDownloads());
+ switch (index.column()) {
+ case COL_NAME: return tr("< game %1 mod %2 file %3 >").arg(std::get<0>(nexusids)).arg(std::get<1>(nexusids)).arg(std::get<2>(nexusids));
+ case COL_SIZE: return tr("Unknown");
+ case COL_STATUS: return tr("Pending");
+ }
+ } else {
+ switch (index.column()) {
+ case COL_NAME: return m_MetaDisplay ? m_Manager->getDisplayName(index.row()) : m_Manager->getFileName(index.row());
+ case COL_SIZE: return sizeFormat(m_Manager->getFileSize(index.row()));
+ case COL_FILETIME: return m_Manager->getFileTime(index.row());
+ case COL_STATUS:
+ switch (m_Manager->getState(index.row())) {
+ // STATE_DOWNLOADING handled by DownloadProgressDelegate
+ case DownloadManager::STATE_STARTED: return tr("Started");
+ case DownloadManager::STATE_CANCELING: return tr("Canceling");
+ case DownloadManager::STATE_PAUSING: return tr("Pausing");
+ case DownloadManager::STATE_CANCELED: return tr("Canceled");
+ case DownloadManager::STATE_PAUSED: return tr("Paused");
+ case DownloadManager::STATE_ERROR: return tr("Error");
+ case DownloadManager::STATE_FETCHINGMODINFO: return tr("Fetching Info");
+ case DownloadManager::STATE_FETCHINGFILEINFO: return tr("Fetching Info");
+ case DownloadManager::STATE_FETCHINGMODINFO_MD5: return tr("Fetching Info");
+ case DownloadManager::STATE_READY: return tr("Downloaded");
+ case DownloadManager::STATE_INSTALLED: return tr("Installed");
+ case DownloadManager::STATE_UNINSTALLED: return tr("Uninstalled");
+ }
+ }
+ }
+ } else if (role == Qt::ForegroundRole && index.column() == COL_STATUS) {
+ if (pendingDownload) {
+ return QColor(Qt::darkBlue);
+ } else {
+ DownloadManager::DownloadState state = m_Manager->getState(index.row());
+ if (state == DownloadManager::STATE_READY)
+ return QColor(Qt::darkGreen);
+ else if (state == DownloadManager::STATE_UNINSTALLED)
+ return QColor(Qt::darkYellow);
+ else if (state == DownloadManager::STATE_PAUSED)
+ return QColor(Qt::darkRed);
+ }
} else if (role == Qt::ToolTipRole) {
- if (index.row() < m_Manager->numTotalDownloads()) {
+ if (pendingDownload) {
+ return tr("Pending download");
+ } else {
QString text = m_Manager->getFileName(index.row()) + "\n";
if (m_Manager->isInfoIncomplete(index.row())) {
text += tr("Information missing, please select \"Query Info\" from the context menu to re-retrieve.");
} else {
const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(index.row());
- return QString("%1 (ID %2) %3<br><span>%4</span>").arg(info->modName).arg(m_Manager->getModID(index.row())).arg(info->version.canonicalString()).arg(info->description);
+ return QString("%1 (ID %2) %3<br><span>%4</span>").arg(info->modName).arg(m_Manager->getModID(index.row())).arg(info->version.canonicalString()).arg(info->description.chopped(4096));
}
return text;
- } else {
- return tr("pending download");
}
- } else {
- return QVariant();
+ } else if (role == Qt::DecorationRole && index.column() == COL_NAME) {
+ if (!pendingDownload && m_Manager->getState(index.row()) >= DownloadManager::STATE_READY
+ && 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
+ return QVariant(Qt::AlignVCenter | Qt::AlignRight);
}
+ return QVariant();
}
@@ -103,13 +160,28 @@ void DownloadList::aboutToUpdate()
void DownloadList::update(int row)
{
- if (row < 0) {
+ if (row < 0)
emit endResetModel();
- } else if (row < this->rowCount()) {
-#pragma message("updating only the one column is a hack")
- emit dataChanged(this->index(row, 2, QModelIndex()), this->index(row, 2, QModelIndex()));
- } else {
+ else if (row < this->rowCount())
+ emit dataChanged(this->index(row, 0, QModelIndex()), this->index(row, this->columnCount(QModelIndex())-1, QModelIndex()));
+ else
qCritical("invalid row %d in download list, update failed", row);
- }
}
+QString DownloadList::sizeFormat(quint64 size) const
+{
+ qreal calc = size;
+ QStringList list;
+ list << "MB" << "GB" << "TB";
+
+ QStringListIterator i(list);
+ QString unit("KB");
+
+ calc /= 1024.0;
+ while (calc >= 1024.0 && i.hasNext()) {
+ unit = i.next();
+ calc /= 1024.0;
+ }
+
+ return QString().setNum(calc, 'f', 2) + " " + unit;
+}
diff --git a/src/downloadlist.h b/src/downloadlist.h
index e8833f0f..bf0cdc37 100644
--- a/src/downloadlist.h
+++ b/src/downloadlist.h
@@ -22,7 +22,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QAbstractTableModel>
-
class DownloadManager;
@@ -38,9 +37,9 @@ public:
enum EColumn {
COL_NAME = 0,
- COL_FILETIME,
COL_STATUS,
- COL_SIZE
+ COL_SIZE,
+ COL_FILETIME
};
public:
@@ -53,6 +52,8 @@ public:
**/
explicit DownloadList(DownloadManager *manager, QObject *parent = 0);
+ void setMetaDisplay(bool metaDisplay);
+
/**
* @brief retrieve the number of rows to display. Invoked by Qt
*
@@ -91,7 +92,9 @@ public slots:
private:
DownloadManager *m_Manager;
+ bool m_MetaDisplay;
+ QString sizeFormat(quint64 size) const;
};
#endif // DOWNLOADLIST_H
diff --git a/src/downloadlistsortproxy.cpp b/src/downloadlistsortproxy.cpp
index f791617a..dc97dc3e 100644
--- a/src/downloadlistsortproxy.cpp
+++ b/src/downloadlistsortproxy.cpp
@@ -37,18 +37,23 @@ void DownloadListSortProxy::updateFilter(const QString &filter)
bool DownloadListSortProxy::lessThan(const QModelIndex &left,
const QModelIndex &right) const
{
- int leftIndex = left.data().toInt();
- int rightIndex = right.data().toInt();
+ int leftIndex = left.row();
+ int rightIndex = right.row();
if ((leftIndex < m_Manager->numTotalDownloads())
&& (rightIndex < m_Manager->numTotalDownloads())) {
if (left.column() == DownloadList::COL_NAME) {
- return m_Manager->getFileName(leftIndex).compare(m_Manager->getFileName(rightIndex), Qt::CaseInsensitive) < 0;
- } else if (left.column() == DownloadList::COL_FILETIME) {
- return m_Manager->getFileTime(leftIndex) < m_Manager->getFileTime(rightIndex);
+ return m_Manager->getFileName(left.row()).compare(m_Manager->getFileName(right.row()), Qt::CaseInsensitive) < 0;
} else if (left.column() == DownloadList::COL_STATUS) {
- return m_Manager->getState(leftIndex) < m_Manager->getState(rightIndex);
- } else if(left.column() == DownloadList::COL_SIZE){
- return m_Manager->getFileSize(leftIndex) < m_Manager->getFileSize(rightIndex);
+ DownloadManager::DownloadState leftState = m_Manager->getState(left.row());
+ DownloadManager::DownloadState rightState = m_Manager->getState(right.row());
+ if (leftState == rightState)
+ return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
+ else
+ return leftState > rightState;
+ } else if (left.column() == DownloadList::COL_SIZE) {
+ return m_Manager->getFileSize(left.row()) < m_Manager->getFileSize(right.row());
+ } else if (left.column() == DownloadList::COL_FILETIME) {
+ return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
} else {
return leftIndex < rightIndex;
}
@@ -63,11 +68,9 @@ bool DownloadListSortProxy::filterAcceptsRow(int sourceRow, const QModelIndex&)
if (m_CurrentFilter.length() == 0) {
return true;
} else if (sourceRow < m_Manager->numTotalDownloads()) {
- int downloadIndex = sourceModel()->index(sourceRow, 0).data().toInt();
-
QString displayedName = Settings::instance().metaDownloads()
- ? m_Manager->getDisplayName(downloadIndex)
- : m_Manager->getFileName(downloadIndex);
+ ? m_Manager->getDisplayName(sourceRow)
+ : m_Manager->getFileName(sourceRow);
return displayedName.contains(m_CurrentFilter, Qt::CaseInsensitive);
} else {
return false;
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp
index 34a90534..f0c4da1a 100644
--- a/src/downloadlistwidget.cpp
+++ b/src/downloadlistwidget.cpp
@@ -17,287 +17,302 @@ 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 "downloadlist.h"
#include "downloadlistwidget.h"
-#include "ui_downloadlistwidget.h"
#include <QPainter>
#include <QMouseEvent>
#include <QMenu>
#include <QMessageBox>
#include <QSortFilterProxyModel>
+#include <QApplication>
+#include <QHeaderView>
+#include <QCheckBox>
+#include <QWidgetAction>
-
-DownloadListWidget::DownloadListWidget(QWidget *parent)
- : QWidget(parent), ui(new Ui::DownloadListWidget)
+void DownloadProgressDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
- ui->setupUi(this);
-}
+ QModelIndex sourceIndex = m_SortProxy->mapToSource(index);
+ bool pendingDownload = (sourceIndex.row() >= m_Manager->numTotalDownloads());
+ if (sourceIndex.column() == DownloadList::COL_STATUS && !pendingDownload
+ && m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_DOWNLOADING) {
+ QProgressBar progressBar;
+ progressBar.setProperty("downloadView", option.widget->property("downloadView"));
+ progressBar.setProperty("downloadProgress", true);
+ progressBar.resize(option.rect.width(), option.rect.height());
+ progressBar.setTextVisible(true);
+ progressBar.setAlignment(Qt::AlignCenter);
+ progressBar.setMinimum(0);
+ progressBar.setMaximum(100);
+ progressBar.setValue(m_Manager->getProgress(sourceIndex.row()).first);
+ progressBar.setFormat(m_Manager->getProgress(sourceIndex.row()).second);
+ progressBar.setStyle(QApplication::style());
+ // paint the background with default delegate first to preserve table cell styling
+ QStyledItemDelegate::paint(painter, option, index);
-DownloadListWidget::~DownloadListWidget()
+ painter->save();
+ painter->translate(option.rect.topLeft());
+ progressBar.render(painter);
+ painter->restore();
+ } else {
+ QStyledItemDelegate::paint(painter, option, index);
+ }
+}
+
+void DownloadListHeader::customResizeSections()
{
- delete ui;
+ // find the rightmost column that is not hidden
+ int rightVisible = count() - 1;
+ while (isSectionHidden(rightVisible) && rightVisible > 0)
+ rightVisible--;
+
+ // if that column is already squashed, squash others to the right side --
+ // otherwise to the left
+ if (sectionSize(rightVisible) == minimumSectionSize()) {
+ for (int idx = rightVisible; idx >= 0; idx--) {
+ if (!isSectionHidden(idx)) {
+ if (length() != width())
+ resizeSection(idx, std::max(sectionSize(idx) + width() - length(), minimumSectionSize()));
+ else
+ break;
+ }
+ }
+ } else {
+ for (int idx = 0; idx <= rightVisible; idx++) {
+ if (!isSectionHidden(idx)) {
+ if (length() != width())
+ resizeSection(idx, std::max(sectionSize(idx) + width() - length(), minimumSectionSize()));
+ else
+ break;
+ }
+ }
+ }
}
+void DownloadListHeader::mouseReleaseEvent(QMouseEvent *event)
+{
+ QHeaderView::mouseReleaseEvent(event);
+ customResizeSections();
+}
-DownloadListWidgetDelegate::DownloadListWidgetDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent)
- : QItemDelegate(parent)
- , m_Manager(manager)
- , m_MetaDisplay(metaDisplay)
- , m_ItemWidget(new DownloadListWidget)
- , m_ContextRow(0)
- , m_View(view)
+DownloadListWidget::DownloadListWidget(QWidget *parent)
+ : QTreeView(parent)
{
- m_NameLabel = m_ItemWidget->findChild<QLabel*>("nameLabel");
- m_SizeLabel = m_ItemWidget->findChild<QLabel*>("sizeLabel");
- m_Progress = m_ItemWidget->findChild<QProgressBar*>("downloadProgress");
- m_InstallLabel = m_ItemWidget->findChild<QLabel*>("installLabel");
+ setHeader(new DownloadListHeader(Qt::Horizontal, this));
- m_InstallLabel->setVisible(false);
- m_Progress->setTextVisible(true);
+ header()->setSectionsMovable(true);
+ header()->setContextMenuPolicy(Qt::CustomContextMenu);
+ header()->setCascadingSectionResizes(true);
+ header()->setStretchLastSection(false);
+ header()->setSectionResizeMode(QHeaderView::Interactive);
+ header()->setDefaultSectionSize(100);
- connect(manager, SIGNAL(stateChanged(int,DownloadManager::DownloadState)), this, SLOT(stateChanged(int,DownloadManager::DownloadState)));
- connect(manager, SIGNAL(downloadRemoved(int)), this, SLOT(resetCache(int)));
-}
+ setUniformRowHeights(true);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ sortByColumn(1, Qt::DescendingOrder);
+ connect(header(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onHeaderCustomContextMenu(QPoint)));
+ connect(this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(onDoubleClick(QModelIndex)));
+ connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenu(QPoint)));
+}
-DownloadListWidgetDelegate::~DownloadListWidgetDelegate()
+DownloadListWidget::~DownloadListWidget()
{
- delete m_ItemWidget;
}
-
-void DownloadListWidgetDelegate::stateChanged(int row,DownloadManager::DownloadState)
+void DownloadListWidget::setManager(DownloadManager *manager)
{
- m_Cache.remove(row);
+ m_Manager = manager;
}
-
-void DownloadListWidgetDelegate::resetCache(int)
+void DownloadListWidget::setSourceModel(DownloadList *sourceModel)
{
- m_Cache.clear();
+ m_SourceModel = sourceModel;
}
-
-void DownloadListWidgetDelegate::drawCache(QPainter *painter, const QStyleOptionViewItem &option, const QPixmap &cache) const
+void DownloadListWidget::setMetaDisplay(bool metaDisplay)
{
- QRect rect = option.rect;
- rect.setLeft(0);
- rect.setWidth(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3));
- painter->drawPixmap(rect, cache);
+ if (m_SourceModel != nullptr)
+ m_SourceModel->setMetaDisplay(metaDisplay);
}
-
-QString DownloadListWidgetDelegate::sizeFormat(quint64 size) const
+void DownloadListWidget::onDoubleClick(const QModelIndex &index)
{
- qreal calc = size;
- QStringList list;
- list << "KB" << "MB" << "GB" << "TB";
+ QModelIndex sourceIndex = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index);
+ if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY)
+ emit installDownload(sourceIndex.row());
+ else if ((m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSED)
+ || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING))
+ emit resumeDownload(sourceIndex.row());
+}
- QStringListIterator i(list);
- QString unit("byte(s)");
+void DownloadListWidget::onHeaderCustomContextMenu(const QPoint &point)
+{
+ QMenu menu;
- while (calc >= 1024.0 && i.hasNext())
- {
- unit = i.next();
- calc /= 1024.0;
- }
+ // display a list of all headers as checkboxes
+ QAbstractItemModel *model = header()->model();
+ for (int i = 1; i < model->columnCount(); ++i) {
+ QString columnName = model->headerData(i, Qt::Horizontal).toString();
+ QCheckBox *checkBox = new QCheckBox(&menu);
+ checkBox->setText(columnName);
+ checkBox->setChecked(!header()->isSectionHidden(i));
+ QWidgetAction *checkableAction = new QWidgetAction(&menu);
+ checkableAction->setDefaultWidget(checkBox);
+ menu.addAction(checkableAction);
+ }
- return QString().setNum(calc, 'f', 2) + " " + unit;
-}
+ menu.exec(header()->viewport()->mapToGlobal(point));
+ // view/hide columns depending on check-state
+ int i = 1;
+ for (const QAction *action : menu.actions()) {
+ const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction*>(action);
+ if (widgetAction != nullptr) {
+ const QCheckBox *checkBox = qobject_cast<const QCheckBox*>(widgetAction->defaultWidget());
+ if (checkBox != nullptr) {
+ header()->setSectionHidden(i, !checkBox->isChecked());
+ }
+ }
+ ++i;
+ }
-void DownloadListWidgetDelegate::paintPendingDownload(int downloadIndex) const
-{
- std::tuple<QString, int, int> nexusids = m_Manager->getPendingDownload(downloadIndex);
- m_NameLabel->setText(tr("< game %1 mod %2 file %3 >").arg(std::get<0>(nexusids)).arg(std::get<1>(nexusids)).arg(std::get<2>(nexusids)));
- m_SizeLabel->setText("???");
- m_InstallLabel->setVisible(true);
- m_InstallLabel->setText(tr("Pending"));
- m_Progress->setVisible(false);
+ qobject_cast<DownloadListHeader*>(header())->customResizeSections();
}
-
-void DownloadListWidgetDelegate::paintRegularDownload(int downloadIndex) const
+void DownloadListWidget::resizeEvent(QResizeEvent *event)
{
- QString name = m_MetaDisplay ? m_Manager->getDisplayName(downloadIndex) : m_Manager->getFileName(downloadIndex);
- if (name.length() > 53) {
- name.truncate(50);
- name.append("...");
- }
- m_NameLabel->setText(name);
- m_SizeLabel->setText(sizeFormat(m_Manager->getFileSize(downloadIndex) ));
- DownloadManager::DownloadState state = m_Manager->getState(downloadIndex);
- if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) {
- QPalette labelPalette;
- m_InstallLabel->setVisible(true);
- m_Progress->setVisible(false);
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Paused - Double Click to resume", 0));
-#else
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Paused - Double Click to resume", 0, QApplication::UnicodeUTF8));
-#endif
- labelPalette.setColor(QPalette::WindowText, Qt::darkRed);
- m_InstallLabel->setPalette(labelPalette);
- } else if (state == DownloadManager::STATE_FETCHINGMODINFO) {
- m_InstallLabel->setText(tr("Fetching Info 1"));
- m_Progress->setVisible(false);
- } else if (state == DownloadManager::STATE_FETCHINGFILEINFO) {
- m_InstallLabel->setText(tr("Fetching Info 2"));
- m_Progress->setVisible(false);
- } else if (state >= DownloadManager::STATE_READY) {
- QPalette labelPalette;
- m_InstallLabel->setVisible(true);
- m_Progress->setVisible(false);
- if (state == DownloadManager::STATE_INSTALLED) {
- // the tr-macro doesn't work here, maybe because the translation is actually associated with DownloadListWidget instead
- // of DownloadListWidgetDelegate?
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Installed - Double Click to re-install", 0));
-#else
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Installed - Double Click to re-install", 0, QApplication::UnicodeUTF8));
-#endif
- labelPalette.setColor(QPalette::WindowText, Qt::darkGray);
- } else if (state == DownloadManager::STATE_UNINSTALLED) {
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Uninstalled - Double Click to re-install", 0));
-#else
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Uninstalled - Double Click to re-install", 0, QApplication::UnicodeUTF8));
-#endif
- labelPalette.setColor(QPalette::WindowText, Qt::lightGray);
- } else {
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Done - Double Click to install", 0));
-#else
- m_InstallLabel->setText(QApplication::translate("DownloadListWidget", "Done - Double Click to install", 0, QApplication::UnicodeUTF8));
-#endif
- labelPalette.setColor(QPalette::WindowText, Qt::darkGreen);
- }
- m_InstallLabel->setPalette(labelPalette);
- if (m_Manager->isInfoIncomplete(downloadIndex)) {
- m_NameLabel->setText("<img src=\":/MO/gui/warning_16\" /> " + m_NameLabel->text());
- }
- } else {
- m_InstallLabel->setVisible(false);
- m_Progress->setVisible(true);
- m_Progress->setValue(m_Manager->getProgress(downloadIndex).first);
- m_Progress->setFormat(m_Manager->getProgress(downloadIndex).second);
- }
+ QTreeView::resizeEvent(event);
+ qobject_cast<DownloadListHeader*>(header())->customResizeSections();
}
-void DownloadListWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+void DownloadListWidget::onCustomContextMenu(const QPoint &point)
{
- try {
- auto iter = m_Cache.find(index.row());
- if (iter != m_Cache.end()) {
- drawCache(painter, option, *iter);
- return;
- }
+ QMenu menu(this);
+ QModelIndex index = indexAt(point);
+ bool hidden = false;
+
+ if (index.row() >= 0) {
+ m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
+ DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
+ hidden = m_Manager->isHidden(m_ContextRow);
- m_ItemWidget->resize(QSize(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3), option.rect.height()));
+ if (state >= DownloadManager::STATE_READY) {
+ menu.addAction(tr("Install"), this, SLOT(issueInstall()));
+ if (m_Manager->isInfoIncomplete(m_ContextRow))
+ menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
+ else
+ menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
+ menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- int downloadIndex = index.data().toInt();
+ menu.addSeparator();
- if (downloadIndex >= m_Manager->numTotalDownloads()) {
- paintPendingDownload(downloadIndex - m_Manager->numTotalDownloads());
- } else {
- paintRegularDownload(downloadIndex);
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ if (hidden)
+ menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
+ else
+ menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
+ } else if (state == DownloadManager::STATE_DOWNLOADING) {
+ menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
+ menu.addAction(tr("Pause"), this, SLOT(issuePause()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
+ || (state == DownloadManager::STATE_PAUSING)) {
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ menu.addAction(tr("Resume"), this, SLOT(issueResume()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
}
-#pragma message("caching disabled because changes in the list (including resorting) doesn't work correctly")
-// if (state >= DownloadManager::STATE_READY) {
- if (false) {
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- QPixmap cache = m_ItemWidget->grab();
-#else
- QPixmap cache = QPixmap::grabWidget(m_ItemWidget);
-#endif
- m_Cache[index.row()] = cache;
- drawCache(painter, option, cache);
- } else {
- painter->save();
- painter->translate(QPoint(0, option.rect.topLeft().y()));
+ 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()));
- m_ItemWidget->render(painter);
- painter->restore();
- }
- } catch (const std::exception &e) {
- qCritical("failed to paint download list: %s", e.what());
+ menu.addSeparator();
+ if (!hidden) {
+ menu.addAction(tr("Hide Installed..."), this, SLOT(issueRemoveFromViewCompleted()));
+ menu.addAction(tr("Hide Uninstalled..."), this, SLOT(issueRemoveFromViewUninstalled()));
+ menu.addAction(tr("Hide All..."), this, SLOT(issueRemoveFromViewAll()));
+ } else {
+ menu.addAction(tr("Un-Hide All..."), this, SLOT(issueRestoreToViewAll()));
}
-}
-QSize DownloadListWidgetDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const
-{
- const int width = m_ItemWidget->minimumWidth();
- const int height = m_ItemWidget->height();
- return QSize(width, height);
+ menu.exec(viewport()->mapToGlobal(point));
}
-
-void DownloadListWidgetDelegate::issueInstall()
+void DownloadListWidget::issueInstall()
{
emit installDownload(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueQueryInfo()
+void DownloadListWidget::issueQueryInfo()
{
emit queryInfo(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueDelete()
+void DownloadListWidget::issueQueryInfoMd5()
{
- if (QMessageBox::question(nullptr, tr("Delete Files?"),
- tr("This will permanently delete the selected download."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(m_ContextRow, true);
- }
+ emit queryInfoMd5(m_ContextRow);
+}
+
+void DownloadListWidget::issueDelete()
+{
+ if (QMessageBox::question(nullptr, tr("Delete Files?"),
+ tr("This will permanently delete the selected download."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(m_ContextRow, true);
+ }
}
-void DownloadListWidgetDelegate::issueRemoveFromView()
+void DownloadListWidget::issueRemoveFromView()
{
+ qDebug() << "removing from view: " << m_ContextRow;
emit removeDownload(m_ContextRow, false);
}
-void DownloadListWidgetDelegate::issueRestoreToView()
+void DownloadListWidget::issueRestoreToView()
{
- emit restoreDownload(m_ContextRow);
+ emit restoreDownload(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueRestoreToViewAll()
+void DownloadListWidget::issueRestoreToViewAll()
{
emit restoreDownload(-1);
}
-void DownloadListWidgetDelegate::issueVisitOnNexus()
+void DownloadListWidget::issueVisitOnNexus()
{
emit visitOnNexus(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueOpenFile()
+void DownloadListWidget::issueOpenFile()
{
emit openFile(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueOpenInDownloadsFolder()
+void DownloadListWidget::issueOpenInDownloadsFolder()
{
emit openInDownloadsFolder(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueCancel()
+void DownloadListWidget::issueCancel()
{
emit cancelDownload(m_ContextRow);
}
-void DownloadListWidgetDelegate::issuePause()
+void DownloadListWidget::issuePause()
{
emit pauseDownload(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueResume()
+void DownloadListWidget::issueResume()
{
emit resumeDownload(m_ContextRow);
}
-void DownloadListWidgetDelegate::issueDeleteAll()
+void DownloadListWidget::issueDeleteAll()
{
if (QMessageBox::question(nullptr, tr("Delete Files?"),
tr("This will remove all finished downloads from this list and from disk."),
@@ -306,7 +321,7 @@ void DownloadListWidgetDelegate::issueDeleteAll()
}
}
-void DownloadListWidgetDelegate::issueDeleteCompleted()
+void DownloadListWidget::issueDeleteCompleted()
{
if (QMessageBox::question(nullptr, tr("Delete Files?"),
tr("This will remove all installed downloads from this list and from disk."),
@@ -315,16 +330,16 @@ void DownloadListWidgetDelegate::issueDeleteCompleted()
}
}
-void DownloadListWidgetDelegate::issueDeleteUninstalled()
+void DownloadListWidget::issueDeleteUninstalled()
{
if (QMessageBox::question(nullptr, tr("Delete Files?"),
- tr("This will remove all uninstalled downloads from this list and from disk."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ tr("This will remove all uninstalled downloads from this list and from disk."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-3, true);
}
}
-void DownloadListWidgetDelegate::issueRemoveFromViewAll()
+void DownloadListWidget::issueRemoveFromViewAll()
{
if (QMessageBox::question(nullptr, tr("Are you sure?"),
tr("This will remove all finished downloads from this list (but NOT from disk)."),
@@ -333,7 +348,7 @@ void DownloadListWidgetDelegate::issueRemoveFromViewAll()
}
}
-void DownloadListWidgetDelegate::issueRemoveFromViewCompleted()
+void DownloadListWidget::issueRemoveFromViewCompleted()
{
if (QMessageBox::question(nullptr, tr("Are you sure?"),
tr("This will remove all installed downloads from this list (but NOT from disk)."),
@@ -342,90 +357,11 @@ void DownloadListWidgetDelegate::issueRemoveFromViewCompleted()
}
}
-void DownloadListWidgetDelegate::issueRemoveFromViewUninstalled()
+void DownloadListWidget::issueRemoveFromViewUninstalled()
{
if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will remove all uninstalled downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ tr("This will remove all uninstalled downloads from this list (but NOT from disk)."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
emit removeDownload(-3, false);
}
}
-
-bool DownloadListWidgetDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
- const QStyleOptionViewItem &option, const QModelIndex &index)
-{
- try {
- if (event->type() == QEvent::MouseButtonDblClick) {
- QModelIndex sourceIndex = qobject_cast<QSortFilterProxyModel*>(model)->mapToSource(index);
- if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY) {
- emit installDownload(sourceIndex.row());
- } else if ((m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_PAUSED) || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING)) {
- emit resumeDownload(sourceIndex.row());
- }
- return true;
- } else if (event->type() == QEvent::MouseButtonRelease) {
- QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
- if (mouseEvent->button() == Qt::RightButton) {
- QMenu menu(m_View);
- bool hidden = false;
- m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model)->mapToSource(index).row();
- if (m_ContextRow < m_Manager->numTotalDownloads()) {
- DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- hidden = m_Manager->isHidden(m_ContextRow);
- if (state >= DownloadManager::STATE_READY) {
- menu.addAction(tr("Install"), this, SLOT(issueInstall()));
- if (m_Manager->isInfoIncomplete(m_ContextRow)) {
- menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfo()));
- }else {
- menu.addAction(tr("Visit on Nexus"), this,SLOT(issueVisitOnNexus()));
- }
-
- menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
-
- menu.addSeparator();
-
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- if (hidden) {
- menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
- } else {
- menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
- }
- } else if (state == DownloadManager::STATE_DOWNLOADING){
- menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
- menu.addAction(tr("Pause"), this, SLOT(issuePause()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) {
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- menu.addAction(tr("Resume"), this, SLOT(issueResume()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- }
-
- 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()));
-
- if (!hidden) {
- menu.addSeparator();
- menu.addAction(tr("Hide Installed..."), this, SLOT(issueRemoveFromViewCompleted()));
- menu.addAction(tr("Hide Uninstalled..."), this, SLOT(issueRemoveFromViewUninstalled()));
- menu.addAction(tr("Hide All..."), this, SLOT(issueRemoveFromViewAll()));
- }
- if (hidden) {
- menu.addSeparator();
- menu.addAction(tr("Un-Hide All..."), this, SLOT(issueRestoreToViewAll()));
- }
-
- menu.exec(mouseEvent->globalPos());
-
- event->accept();
- return true;
- }
- }
- } catch (const std::exception &e) {
- qCritical("failed to handle editor event: %s", e.what());
- }
- return QItemDelegate::editorEvent(event, model, option, index);
-}
diff --git a/src/downloadlistwidget.h b/src/downloadlistwidget.h
index fe7f3b22..ad07b0f1 100644
--- a/src/downloadlistwidget.h
+++ b/src/downloadlistwidget.h
@@ -21,51 +21,64 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define DOWNLOADLISTWIDGET_H
#include "downloadmanager.h"
+#include "downloadlist.h"
+#include "downloadlistsortproxy.h"
#include <QWidget>
#include <QItemDelegate>
#include <QLabel>
#include <QProgressBar>
#include <QTreeView>
+#include <QHeaderView>
+#include <QStyledItemDelegate>
+
namespace Ui {
- class DownloadListWidget;
+ class DownloadListWidget;
}
-class DownloadListWidget : public QWidget
+class DownloadProgressDelegate : public QStyledItemDelegate
{
- Q_OBJECT
+ Q_OBJECT
public:
- explicit DownloadListWidget(QWidget *parent = 0);
- ~DownloadListWidget();
+ DownloadProgressDelegate(DownloadManager *manager, DownloadListSortProxy *sortProxy, QWidget *parent = 0) : QStyledItemDelegate(parent), m_Manager(manager), m_SortProxy(sortProxy) {}
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
private:
- Ui::DownloadListWidget *ui;
+ DownloadManager *m_Manager;
+ DownloadListSortProxy *m_SortProxy;
};
-class DownloadManager;
-
-class DownloadListWidgetDelegate : public QItemDelegate
+class DownloadListHeader : public QHeaderView
{
-
Q_OBJECT
public:
+ explicit DownloadListHeader(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {}
+ void customResizeSections();
+
+private:
+ void mouseReleaseEvent(QMouseEvent *event) override;
+};
- DownloadListWidgetDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent = 0);
- ~DownloadListWidgetDelegate();
+class DownloadListWidget : public QTreeView
+{
+ Q_OBJECT
- virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
- virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
+public:
+ explicit DownloadListWidget(QWidget *parent = 0);
+ ~DownloadListWidget();
- void paintPendingDownload(int downloadIndex) const;
- void paintRegularDownload(int downloadIndex) const;
+ void setManager(DownloadManager *manager);
+ void setSourceModel(DownloadList *sourceModel);
+ void setMetaDisplay(bool metaDisplay);
signals:
-
void installDownload(int index);
void queryInfo(int index);
+ void queryInfoMd5(int index);
void removeDownload(int index, bool deleteFile);
void restoreDownload(int index);
void cancelDownload(int index);
@@ -75,19 +88,10 @@ signals:
void openFile(int index);
void openInDownloadsFolder(int index);
-protected:
-
- QString sizeFormat(quint64 size) const;
- bool editorEvent(QEvent *event, QAbstractItemModel *model,
- const QStyleOptionViewItem &option, const QModelIndex &index);
-
-private:
-
-
- void drawCache(QPainter *painter, const QStyleOptionViewItem &option, const QPixmap &cache) const;
-
private slots:
-
+ void onDoubleClick(const QModelIndex &index);
+ void onCustomContextMenu(const QPoint &point);
+ void onHeaderCustomContextMenu(const QPoint &point);
void issueInstall();
void issueDelete();
void issueRemoveFromView();
@@ -106,26 +110,14 @@ private slots:
void issueRemoveFromViewCompleted();
void issueRemoveFromViewUninstalled();
void issueQueryInfo();
-
- void stateChanged(int row, DownloadManager::DownloadState);
- void resetCache(int);
+ void issueQueryInfoMd5();
private:
-
- DownloadListWidget *m_ItemWidget;
DownloadManager *m_Manager;
-
- bool m_MetaDisplay;
-
- QLabel *m_NameLabel;
- QLabel *m_SizeLabel;
- QProgressBar *m_Progress;
- QLabel *m_InstallLabel;
+ DownloadList *m_SourceModel = 0;
int m_ContextRow;
- QTreeView *m_View;
-
- mutable QMap<int, QPixmap> m_Cache;
+ void resizeEvent(QResizeEvent *event);
};
#endif // DOWNLOADLISTWIDGET_H
diff --git a/src/downloadlistwidget.ui b/src/downloadlistwidget.ui
deleted file mode 100644
index 9e238509..00000000
--- a/src/downloadlistwidget.ui
+++ /dev/null
@@ -1,140 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>DownloadListWidget</class>
- <widget class="QWidget" name="DownloadListWidget">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>315</width>
- <height>81</height>
- </rect>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="windowTitle">
- <string>Placeholder</string>
- </property>
- <property name="autoFillBackground">
- <bool>false</bool>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>1</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>1</number>
- </property>
- <item>
- <widget class="QFrame" name="frame">
- <property name="frameShape">
- <enum>QFrame::Box</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Raised</enum>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QLabel" name="nameLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>1</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
- <size>
- <width>323</width>
- <height>16777215</height>
- </size>
- </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="QLabel" name="sizeLabel">
- <property name="text">
- <string notr="true">0</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string notr="true">KB</string>
- </property>
- <property name="visible">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QLabel" name="installLabel">
- <property name="text">
- <string>Done - Double Click to install</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QProgressBar" name="downloadProgress">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <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="value">
- <number>0</number>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/downloadlistwidgetcompact.cpp b/src/downloadlistwidgetcompact.cpp
deleted file mode 100644
index fcf93754..00000000
--- a/src/downloadlistwidgetcompact.cpp
+++ /dev/null
@@ -1,413 +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 "downloadlistwidgetcompact.h"
-#include "ui_downloadlistwidgetcompact.h"
-#include <QPainter>
-#include <QMouseEvent>
-#include <QMenu>
-#include <QMessageBox>
-#include <QSortFilterProxyModel>
-
-
-DownloadListWidgetCompact::DownloadListWidgetCompact(QWidget *parent) :
- QWidget(parent),
- ui(new Ui::DownloadListWidgetCompact)
-{
- ui->setupUi(this);
-}
-
-DownloadListWidgetCompact::~DownloadListWidgetCompact()
-{
- delete ui;
-}
-
-
-DownloadListWidgetCompactDelegate::DownloadListWidgetCompactDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent)
- : QItemDelegate(parent)
- , m_Manager(manager)
- , m_MetaDisplay(metaDisplay)
- , m_ItemWidget(new DownloadListWidgetCompact)
- , m_View(view)
-{
- m_NameLabel = m_ItemWidget->findChild<QLabel*>("nameLabel");
- m_SizeLabel = m_ItemWidget->findChild<QLabel*>("sizeLabel");
- m_Progress = m_ItemWidget->findChild<QProgressBar*>("downloadProgress");
- m_DoneLabel = m_ItemWidget->findChild<QLabel*>("doneLabel");
-
- m_DoneLabel->setVisible(false);
-
- connect(manager, SIGNAL(stateChanged(int,DownloadManager::DownloadState)),
- this, SLOT(stateChanged(int,DownloadManager::DownloadState)));
- connect(manager, SIGNAL(downloadRemoved(int)), this, SLOT(resetCache(int)));
-}
-
-
-DownloadListWidgetCompactDelegate::~DownloadListWidgetCompactDelegate()
-{
- delete m_ItemWidget;
-}
-
-
-void DownloadListWidgetCompactDelegate::stateChanged(int row,DownloadManager::DownloadState)
-{
- m_Cache.remove(row);
-}
-
-
-void DownloadListWidgetCompactDelegate::resetCache(int)
-{
- m_Cache.clear();
-}
-
-
-void DownloadListWidgetCompactDelegate::drawCache(QPainter *painter, const QStyleOptionViewItem &option, const QPixmap &cache) const
-{
- QRect rect = option.rect;
- rect.setLeft(0);
- rect.setWidth(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3));
- painter->drawPixmap(rect, cache);
-}
-
-QString DownloadListWidgetCompactDelegate::sizeFormat(quint64 size) const
-{
- qreal calc = size;
- QStringList list;
- list << "KB" << "MB" << "GB" << "TB";
-
- QStringListIterator i(list);
- QString unit("byte(s)");
-
- while (calc >= 1024.0 && i.hasNext())
- {
- unit = i.next();
- calc /= 1024.0;
- }
-
- return QString().setNum(calc, 'f', 2) + " " + unit;
-}
-
-void DownloadListWidgetCompactDelegate::paintPendingDownload(int downloadIndex) const
-{
- std::tuple<QString, int, int> nexusids = m_Manager->getPendingDownload(downloadIndex);
- m_NameLabel->setText(tr("< game %1 mod %2 file %3 >").arg(std::get<0>(nexusids)).arg(std::get<1>(nexusids)).arg(std::get<2>(nexusids)));
- //if (m_SizeLabel != nullptr) {
- // m_SizeLabel->setText("???");
- //}
- m_DoneLabel->setVisible(true);
- m_DoneLabel->setText(tr("Pending"));
- m_Progress->setVisible(false);
-}
-
-
-void DownloadListWidgetCompactDelegate::paintRegularDownload(int downloadIndex) const
-{
- QString name = m_MetaDisplay ? m_Manager->getDisplayName(downloadIndex) : m_Manager->getFileName(downloadIndex);
- if (name.length() > 53) {
- name.truncate(50);
- name.append("...");
- }
- m_NameLabel->setText(name);
-
- DownloadManager::DownloadState state = m_Manager->getState(downloadIndex);
-
- if (m_SizeLabel != nullptr) {
- m_SizeLabel->setText(sizeFormat(m_Manager->getFileSize(downloadIndex)) + " ");
- m_SizeLabel->setVisible(true);
- }
- //else {
- // m_SizeLabel->setVisible(false);
- //}
-
- if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) {
- m_DoneLabel->setVisible(true);
- m_Progress->setVisible(false);
- m_DoneLabel->setText(QString("%1<img src=\":/MO/gui/inactive\">").arg(tr("Paused")));
- } else if (state == DownloadManager::STATE_FETCHINGMODINFO) {
- m_DoneLabel->setText(QString("%1").arg(tr("Fetching Info 1")));
- } else if (state == DownloadManager::STATE_FETCHINGFILEINFO) {
- m_DoneLabel->setText(QString("%1").arg(tr("Fetching Info 2")));
- } else if (state >= DownloadManager::STATE_READY) {
- m_DoneLabel->setVisible(true);
- m_Progress->setVisible(false);
- if (state == DownloadManager::STATE_INSTALLED) {
- m_DoneLabel->setText(QString("%1<img src=\":/MO/gui/check\">").arg(tr("Installed")));
- } else if (state == DownloadManager::STATE_UNINSTALLED) {
- m_DoneLabel->setText(QString("%1<img src=\":/MO/gui/awaiting\">").arg(tr("Uninstalled")));
- } else {
- m_DoneLabel->setText(QString("%1<img src=\":/MO/gui/active\">").arg(tr("Done")));
- }
- if (m_Manager->isInfoIncomplete(downloadIndex)) {
- m_NameLabel->setText("<img src=\":/MO/gui/warning_16\"/> " + m_NameLabel->text());
- }
- } else {
- m_DoneLabel->setVisible(false);
- m_Progress->setVisible(true);
- m_Progress->setValue(m_Manager->getProgress(downloadIndex).first);
- m_Progress->setFormat(m_Manager->getProgress(downloadIndex).second);
- }
-}
-
-void DownloadListWidgetCompactDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
-{
-#pragma message("This is quite costy - room for optimization?")
- try {
- auto iter = m_Cache.find(index.row());
- if (iter != m_Cache.end()) {
- drawCache(painter, option, *iter);
- return;
- }
-
- m_ItemWidget->resize(QSize(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3), option.rect.height()));
- if (index.row() % 2 == 1) {
- m_ItemWidget->setBackgroundRole(QPalette::AlternateBase);
- } else {
- m_ItemWidget->setBackgroundRole(QPalette::Base);
- }
-
- int downloadIndex = index.data().toInt();
- if (downloadIndex >= m_Manager->numTotalDownloads()) {
- paintPendingDownload(downloadIndex - m_Manager->numTotalDownloads());
- } else {
- paintRegularDownload(downloadIndex);
- }
-
-#pragma message("caching disabled because changes in the list (including resorting) doesn't work correctly")
- if (false) {
-// if (state >= DownloadManager::STATE_READY) {
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- QPixmap cache = m_ItemWidget->grab();
-#else
- QPixmap cache = QPixmap::grabWidget(m_ItemWidget);
-#endif
- m_Cache[index.row()] = cache;
- drawCache(painter, option, cache);
- } else {
- painter->save();
- painter->translate(QPoint(0, option.rect.topLeft().y()));
-
- m_ItemWidget->render(painter);
- painter->restore();
- }
- } catch (const std::exception &e) {
- qCritical("failed to paint download list item %d: %s", index.row(), e.what());
- }
-}
-
-QSize DownloadListWidgetCompactDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const
-{
- const int width = m_ItemWidget->minimumWidth();
- const int height = m_ItemWidget->height();
- return QSize(width, height);
-}
-
-
-void DownloadListWidgetCompactDelegate::issueInstall()
-{
- emit installDownload(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueQueryInfo()
-{
- emit queryInfo(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueDelete()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will permanently delete the selected download."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(m_ContextIndex.row(), true);
- }
-}
-
-void DownloadListWidgetCompactDelegate::issueRemoveFromView()
-{
- emit removeDownload(m_ContextIndex.row(), false);
-}
-
-void DownloadListWidgetCompactDelegate::issueVisitOnNexus()
-{
- emit visitOnNexus(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueOpenFile()
-{
- emit openFile(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueOpenInDownloadsFolder()
-{
- emit openInDownloadsFolder(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueRestoreToView()
-{
- emit restoreDownload(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueRestoreToViewAll()
-{
- emit restoreDownload(-1);
-}
-
-
-void DownloadListWidgetCompactDelegate::issueCancel()
-{
- emit cancelDownload(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issuePause()
-{
- emit pauseDownload(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueResume()
-{
- emit resumeDownload(m_ContextIndex.row());
-}
-
-void DownloadListWidgetCompactDelegate::issueDeleteAll()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will remove all finished downloads from this list and from disk."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-1, true);
- }
-}
-
-void DownloadListWidgetCompactDelegate::issueDeleteCompleted()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will remove all installed downloads from this list and from disk."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-2, true);
- }
-}
-
-void DownloadListWidgetCompactDelegate::issueDeleteUninstalled()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will remove all uninstalled downloads from this list and from disk."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-3, true);
- }
-}
-
-void DownloadListWidgetCompactDelegate::issueRemoveFromViewAll()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will permanently remove all finished downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-1, false);
- }
-}
-
-void DownloadListWidgetCompactDelegate::issueRemoveFromViewCompleted()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will permanently remove all installed downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-2, false);
- }
-}
-
-void DownloadListWidgetCompactDelegate::issueRemoveFromViewUninstalled()
-{
- if (QMessageBox::question(nullptr, tr("Are you sure?"),
- tr("This will permanently remove all uninstalled downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-3, false);
- }
-}
-
-
-bool DownloadListWidgetCompactDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
- const QStyleOptionViewItem &option, const QModelIndex &index)
-{
- try {
- if (event->type() == QEvent::MouseButtonDblClick) {
- QModelIndex sourceIndex = qobject_cast<QSortFilterProxyModel*>(model)->mapToSource(index);
- if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY) {
- emit installDownload(sourceIndex.row());
- } else if ((m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_PAUSED) || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING)) {
- emit resumeDownload(sourceIndex.row());
- }
- return true;
- } else if (event->type() == QEvent::MouseButtonRelease) {
- QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
- if (mouseEvent->button() == Qt::RightButton) {
- QMenu menu;
- bool hidden = false;
- m_ContextIndex = qobject_cast<QSortFilterProxyModel*>(model)->mapToSource(index);
- if (m_ContextIndex.row() < m_Manager->numTotalDownloads()) {
- DownloadManager::DownloadState state = m_Manager->getState(m_ContextIndex.row());
- hidden = m_Manager->isHidden(m_ContextIndex.row());
- if (state >= DownloadManager::STATE_READY) {
- menu.addAction(tr("Install"), this, SLOT(issueInstall()));
- if (m_Manager->isInfoIncomplete(m_ContextIndex.row())) {
- menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfo()));
- }else {
- menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
- }
- menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- menu.addSeparator();
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- if (hidden) {
- menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
- } else {
- menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
- }
- } else if (state == DownloadManager::STATE_DOWNLOADING){
- menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
- menu.addAction(tr("Pause"), this, SLOT(issuePause()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) {
- menu.addAction(tr("Remove"), this, SLOT(issueDelete()));
- menu.addAction(tr("Resume"), this, SLOT(issueResume()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- }
- 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()));
- if (!hidden) {
- menu.addSeparator();
- menu.addAction(tr("Hide Installed..."), this, SLOT(issueRemoveFromViewCompleted()));
- menu.addAction(tr("Hide Uninstalled..."), this, SLOT(issueRemoveFromViewUninstalled()));
- menu.addAction(tr("Hide All..."), this, SLOT(issueRemoveFromViewAll()));
- }
- if (hidden) {
- menu.addSeparator();
- menu.addAction(tr("Un-Hide All..."), this, SLOT(issueRestoreToViewAll()));
- }
- menu.exec(mouseEvent->globalPos());
-
- event->accept();
- return false;
- }
- }
- } catch (const std::exception &e) {
- qCritical("failed to handle editor event: %s", e.what());
- }
-
- return QItemDelegate::editorEvent(event, model, option, index);
-}
diff --git a/src/downloadlistwidgetcompact.h b/src/downloadlistwidgetcompact.h
deleted file mode 100644
index eb109f29..00000000
--- a/src/downloadlistwidgetcompact.h
+++ /dev/null
@@ -1,131 +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/>.
-*/
-
-#ifndef DOWNLOADLISTWIDGETCOMPACT_H
-#define DOWNLOADLISTWIDGETCOMPACT_H
-
-#include <QWidget>
-#include <QItemDelegate>
-#include <QLabel>
-#include <QProgressBar>
-#include <QTreeView>
-#include "downloadmanager.h"
-
-
-namespace Ui {
-class DownloadListWidgetCompact;
-}
-
-class DownloadListWidgetCompact : public QWidget
-{
- Q_OBJECT
-
-public:
- explicit DownloadListWidgetCompact(QWidget *parent = 0);
- ~DownloadListWidgetCompact();
-
-private:
- Ui::DownloadListWidgetCompact *ui;
- int m_ContextRow;
-};
-
-class DownloadManager;
-
-class DownloadListWidgetCompactDelegate : public QItemDelegate
-{
-
- Q_OBJECT
-
-public:
-
- DownloadListWidgetCompactDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent = 0);
- ~DownloadListWidgetCompactDelegate();
-
- virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
- virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
-
-signals:
-
- void installDownload(int index);
- void queryInfo(int index);
- void removeDownload(int index, bool deleteFile);
- void restoreDownload(int index);
- void cancelDownload(int index);
- void pauseDownload(int index);
- void resumeDownload(int index);
- void visitOnNexus(int index);
- void openFile(int index);
- void openInDownloadsFolder(int index);
-
-protected:
-
- QString sizeFormat(quint64 size) const;
- bool editorEvent(QEvent *event, QAbstractItemModel *model,
- const QStyleOptionViewItem &option, const QModelIndex &index);
-
-private:
-
- void drawCache(QPainter *painter, const QStyleOptionViewItem &option, const QPixmap &cache) const;
- void paintPendingDownload(int downloadIndex) const;
- void paintRegularDownload(int downloadIndex) const;
-
-private slots:
-
- void issueInstall();
- void issueDelete();
- void issueRemoveFromView();
- void issueRestoreToView();
- void issueRestoreToViewAll();
- void issueVisitOnNexus();
- void issueOpenFile();
- void issueOpenInDownloadsFolder();
- void issueCancel();
- void issuePause();
- void issueResume();
- void issueDeleteAll();
- void issueDeleteCompleted();
- void issueDeleteUninstalled();
- void issueRemoveFromViewAll();
- void issueRemoveFromViewCompleted();
- void issueRemoveFromViewUninstalled();
- void issueQueryInfo();
-
- void stateChanged(int row, DownloadManager::DownloadState);
- void resetCache(int);
-private:
-
- DownloadListWidgetCompact *m_ItemWidget;
- DownloadManager *m_Manager;
-
- bool m_MetaDisplay;
-
- QLabel *m_NameLabel;
- QLabel *m_SizeLabel;
- QProgressBar *m_Progress;
- QLabel *m_DoneLabel;
-
- QModelIndex m_ContextIndex;
-
- QTreeView *m_View;
-
- mutable QMap<int, QPixmap> m_Cache;
-
-};
-
-#endif // DOWNLOADLISTWIDGETCOMPACT_H
diff --git a/src/downloadlistwidgetcompact.ui b/src/downloadlistwidgetcompact.ui
deleted file mode 100644
index a3fa958c..00000000
--- a/src/downloadlistwidgetcompact.ui
+++ /dev/null
@@ -1,168 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>DownloadListWidgetCompact</class>
- <widget class="QWidget" name="DownloadListWidgetCompact">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>315</width>
- <height>24</height>
- </rect>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="windowTitle">
- <string>Placeholder</string>
- </property>
- <property name="autoFillBackground">
- <bool>true</bool>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0">
- <property name="spacing">
- <number>2</number>
- </property>
- <property name="leftMargin">
- <number>2</number>
- </property>
- <property name="topMargin">
- <number>2</number>
- </property>
- <property name="rightMargin">
- <number>2</number>
- </property>
- <property name="bottomMargin">
- <number>2</number>
- </property>
- <item>
- <widget class="QLabel" name="nameLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::NoContextMenu</enum>
- </property>
- <property name="text">
- <string>Placeholder</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>10</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="sizeLabel">
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="doneLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="palette">
- <palette>
- <active>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>118</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>118</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>120</red>
- <green>120</green>
- <blue>120</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::NoContextMenu</enum>
- </property>
- <property name="text">
- <string>Done</string>
- </property>
- <property name="scaledContents">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QProgressBar" name="downloadProgress">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>1</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>16</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::NoContextMenu</enum>
- </property>
- <property name="value">
- <number>0</number>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp
index f1cbf109..25542ef3 100644
--- a/src/downloadmanager.cpp
+++ b/src/downloadmanager.cpp
@@ -1,1856 +1,2066 @@
-/*
-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 "downloadmanager.h"
-
-#include "nxmurl.h"
-#include "nexusinterface.h"
-#include "nxmaccessmanager.h"
-#include "iplugingame.h"
-#include "downloadmanager.h"
-#include <nxmurl.h>
-#include <taskprogressmanager.h>
-#include "utility.h"
-#include "selectiondialog.h"
-#include "bbcode.h"
-#include <utility.h>
-#include <report.h>
-
-#include <QTimer>
-#include <QFileInfo>
-#include <QRegExp>
-#include <QDirIterator>
-#include <QDesktopServices>
-#include <QInputDialog>
-#include <QMessageBox>
-#include <QCoreApplication>
-#include <QTextDocument>
-
-#include <boost/bind.hpp>
-#include <regex>
-
-
-using namespace MOBase;
-
-
-// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads
-
-
-static const char UNFINISHED[] = ".unfinished";
-
-unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U;
-int DownloadManager::m_DirWatcherDisabler = 0;
-
-
-DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs)
-{
- DownloadInfo *info = new DownloadInfo;
- info->m_DownloadID = s_NextDownloadID++;
- info->m_StartTime.start();
- info->m_PreResumeSize = 0LL;
- info->m_Progress = std::make_pair<int, QString>(0, " 0.0 Bytes/s ");
- info->m_ResumePos = 0;
- info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo);
- info->m_Urls = URLs;
- info->m_CurrentUrl = 0;
- info->m_Tries = AUTOMATIC_RETRIES;
- info->m_State = STATE_STARTED;
- info->m_TaskProgressId = TaskProgressManager::instance().getId();
- info->m_Reply = nullptr;
-
- return info;
-}
-
-DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory)
-{
- DownloadInfo *info = new DownloadInfo;
-
- QString metaFileName = filePath + ".meta";
- QFileInfo metaFileInfo(metaFileName);
- if (QDir::fromNativeSeparators(metaFileInfo.path()).compare(QDir::fromNativeSeparators(outputDirectory), Qt::CaseInsensitive) != 0) return nullptr;
- QSettings metaFile(metaFileName, QSettings::IniFormat);
- if (!showHidden && metaFile.value("removed", false).toBool()) {
- return nullptr;
- } else {
- info->m_Hidden = metaFile.value("removed", false).toBool();
- }
-
- QString fileName = QFileInfo(filePath).fileName();
-
- if (fileName.endsWith(UNFINISHED)) {
- info->m_FileName = fileName.mid(
- 0, fileName.length() - static_cast<int>(strlen(UNFINISHED)));
- info->m_State = STATE_PAUSED;
- } else {
- info->m_FileName = fileName;
-
- if (metaFile.value("paused", false).toBool()) {
- info->m_State = STATE_PAUSED;
- } else if (metaFile.value("uninstalled", false).toBool()) {
- info->m_State = STATE_UNINSTALLED;
- } else if (metaFile.value("installed", false).toBool()) {
- info->m_State = STATE_INSTALLED;
- } else {
- info->m_State = STATE_READY;
- }
- }
-
- info->m_DownloadID = s_NextDownloadID++;
- info->m_Output.setFileName(filePath);
- info->m_TotalSize = QFileInfo(filePath).size();
- info->m_PreResumeSize = info->m_TotalSize;
- info->m_CurrentUrl = 0;
- info->m_Urls = metaFile.value("url", "").toString().split(";");
- info->m_Tries = 0;
- info->m_TaskProgressId = TaskProgressManager::instance().getId();
- QString gameName = metaFile.value("gameName", "").toString();
- int modID = metaFile.value("modID", 0).toInt();
- int fileID = metaFile.value("fileID", 0).toInt();
- info->m_FileInfo = new ModRepositoryFileInfo(gameName, modID, fileID);
- info->m_FileInfo->name = metaFile.value("name", "").toString();
- if (info->m_FileInfo->name == "0") {
- // bug in earlier version
- info->m_FileInfo->name = "";
- }
- info->m_FileInfo->modName = metaFile.value("modName", "").toString();
- info->m_FileInfo->gameName = gameName;
- info->m_FileInfo->modID = modID;
- info->m_FileInfo->fileID = fileID;
- info->m_FileInfo->description = metaFile.value("description").toString();
- info->m_FileInfo->version.parse(metaFile.value("version", "0").toString());
- info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString());
- info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt();
- info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt();
- info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString();
- info->m_FileInfo->userData = metaFile.value("userData").toMap();
- info->m_Reply = nullptr;
-
- return info;
-}
-
-void DownloadManager::startDisableDirWatcher()
-{
- DownloadManager::m_DirWatcherDisabler++;
-}
-
-
-void DownloadManager::endDisableDirWatcher()
-{
- if (DownloadManager::m_DirWatcherDisabler > 0)
- {
- if (DownloadManager::m_DirWatcherDisabler == 1)
- QCoreApplication::processEvents();
- DownloadManager::m_DirWatcherDisabler--;
- }
- else {
- DownloadManager::m_DirWatcherDisabler = 0;
- }
-}
-
-void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile)
-{
- QString oldMetaFileName = QString("%1.meta").arg(m_FileName);
- m_FileName = QFileInfo(newName).fileName();
- if ((m_State == DownloadManager::STATE_STARTED) ||
- (m_State == DownloadManager::STATE_DOWNLOADING) ||
- (m_State == DownloadManager::STATE_PAUSED)) {
- newName.append(UNFINISHED);
- oldMetaFileName = QString("%1%2.meta").arg(m_FileName).arg(UNFINISHED);
- }
- if (renameFile) {
- if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) {
- reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName));
- return;
- }
-
- QFile metaFile(QFileInfo(newName).path() + "/" + oldMetaFileName);
- if (metaFile.exists())
- metaFile.rename(newName.mid(0).append(".meta"));
- }
- if (!m_Output.isOpen()) {
- // can't set file name if it's open
- m_Output.setFileName(newName);
- }
-}
-
-bool DownloadManager::DownloadInfo::isPausedState()
-{
- return m_State == STATE_PAUSED || m_State == STATE_ERROR;
-}
-
-QString DownloadManager::DownloadInfo::currentURL()
-{
- return m_Urls[m_CurrentUrl];
-}
-
-
-DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent)
- : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false),
- m_DateExpression("/Date\\((\\d+)\\)/")
-{
- connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
- m_TimeoutTimer.setSingleShot(false);
- //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout()));
- m_TimeoutTimer.start(5 * 1000);
-}
-
-
-DownloadManager::~DownloadManager()
-{
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) {
- delete *iter;
- }
- m_ActiveDownloads.clear();
-}
-
-
-bool DownloadManager::downloadsInProgress()
-{
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) {
- if ((*iter)->m_State < STATE_READY) {
- return true;
- }
- }
- return false;
-}
-
-bool DownloadManager::downloadsInProgressNoPause()
-{
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) {
- if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) {
- return true;
- }
- }
- return false;
-}
-
-
-void DownloadManager::pauseAll()
-{
-
- // first loop: pause all downloads
- for (int i = 0; i < m_ActiveDownloads.count(); ++i) {
- if (m_ActiveDownloads[i]->m_State < STATE_READY) {
- pauseDownload(i);
- }
- }
-
- ::Sleep(100);
-
- bool done = false;
- QTime startTime = QTime::currentTime();
- // further loops: busy waiting for all downloads to complete. This could be neater...
- while (!done && (startTime.secsTo(QTime::currentTime()) < 5)) {
- QCoreApplication::processEvents();
- done = true;
- foreach (DownloadInfo *info, m_ActiveDownloads) {
- if ((info->m_State < STATE_CANCELED) ||
- (info->m_State == STATE_FETCHINGFILEINFO) ||
- (info->m_State == STATE_FETCHINGMODINFO)) {
- done = false;
- break;
- }
- }
- if (!done) {
- ::Sleep(100);
- }
- }
-
-}
-
-
-void DownloadManager::setOutputDirectory(const QString &outputDirectory)
-{
- QStringList directories = m_DirWatcher.directories();
- if (directories.length() != 0) {
- m_DirWatcher.removePaths(directories);
- }
- m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory);
- refreshList();
- m_DirWatcher.addPath(m_OutputDirectory);
-}
-
-
-void DownloadManager::setPreferredServers(const std::map<QString, int> &preferredServers)
-{
- m_PreferredServers = preferredServers;
-}
-
-
-void DownloadManager::setSupportedExtensions(const QStringList &extensions)
-{
- m_SupportedExtensions = extensions;
- refreshList();
-}
-
-void DownloadManager::setShowHidden(bool showHidden)
-{
- m_ShowHidden = showHidden;
- refreshList();
-}
-
-void DownloadManager::setPluginContainer(PluginContainer *pluginContainer)
-{
- m_NexusInterface->setPluginContainer(pluginContainer);
-}
-
-
-
-
-
-
-void DownloadManager::refreshList()
-{
- try {
- //avoid triggering other refreshes
- startDisableDirWatcher();
-
- int downloadsBefore = m_ActiveDownloads.size();
-
- // remove finished downloads
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) {
- if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) {
- delete *iter;
- iter = m_ActiveDownloads.erase(iter);
- } else {
- ++iter;
- }
- }
-
- QStringList nameFilters(m_SupportedExtensions);
- foreach (const QString &extension, m_SupportedExtensions) {
- nameFilters.append("*." + extension);
- }
-
- nameFilters.append(QString("*").append(UNFINISHED));
- QDir dir(QDir::fromNativeSeparators(m_OutputDirectory));
-
- // find orphaned meta files and delete them (sounds cruel but it's better for everyone)
- QStringList orphans;
- QStringList metaFiles = dir.entryList(QStringList() << "*.meta");
- foreach (const QString &metaFile, metaFiles) {
- QString baseFile = metaFile.left(metaFile.length() - 5);
- if (!QFile::exists(dir.absoluteFilePath(baseFile))) {
- orphans.append(dir.absoluteFilePath(metaFile));
- }
- }
- if (orphans.size() > 0) {
- qDebug("%d orphaned meta files will be deleted", orphans.size());
- shellDelete(orphans, true);
- }
-
- // add existing downloads to list
- foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) {
- bool Exists = false;
- for (QVector<DownloadInfo*>::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) {
- if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) {
- Exists = true;
- } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) {
- Exists = true;
- }
- }
- if (Exists) {
- continue;
- }
-
- QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file;
-
- DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory);
- if (info != nullptr) {
- m_ActiveDownloads.push_front(info);
- }
- }
-
- //if (m_ActiveDownloads.size() != downloadsBefore) {
- qDebug("downloads after refresh: %d", m_ActiveDownloads.size());
- //}
- emit update(-1);
-
- //let watcher trigger refreshes again
- endDisableDirWatcher();
-
- } catch (const std::bad_alloc&) {
- reportError(tr("Memory allocation error (in refreshing directory)."));
- }
-}
-
-
-bool DownloadManager::addDownload(const QStringList &URLs, QString gameName,
- int modID, int fileID, const ModRepositoryFileInfo *fileInfo)
-{
- QString fileName = QFileInfo(URLs.first()).fileName();
- if (fileName.isEmpty()) {
- fileName = "unknown";
- }
-
- QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit());
- qDebug("selected download url: %s", qPrintable(preferredUrl.toString()));
- QNetworkRequest request(preferredUrl);
- request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent());
- return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo);
-}
-
-
-bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo)
-{
- QString fileName = getFileNameFromNetworkReply(reply);
- if (fileName.isEmpty()) {
- fileName = "unknown";
- }
-
- return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->gameName, fileInfo->modID, fileInfo->fileID, fileInfo);
-}
-
-
-bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName,
- QString gameName, int modID, int fileID, const ModRepositoryFileInfo *fileInfo)
-{
- // download invoked from an already open network reply (i.e. download link in the browser)
- DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs);
-
- QString baseName = fileName;
- if (!fileInfo->fileName.isEmpty()) {
- baseName = fileInfo->fileName;
- } else {
- QString dispoName = getFileNameFromNetworkReply(reply);
-
- if (!dispoName.isEmpty()) {
- baseName = dispoName;
- }
- }
-
- startDisableDirWatcher();
- newDownload->setName(getDownloadFileName(baseName), false);
- endDisableDirWatcher();
-
- startDownload(reply, newDownload, false);
-// emit update(-1);
- return true;
-}
-
-
-void DownloadManager::removePending(QString gameName, int modID, int fileID)
-{
- emit aboutToUpdate();
- for (auto iter : m_PendingDownloads) {
- if (gameName.compare(std::get<0>(iter), Qt::CaseInsensitive) == 0 && (std::get<1>(iter) == modID) && (std::get<2>(iter) == fileID)) {
- m_PendingDownloads.removeAt(m_PendingDownloads.indexOf(iter));
- break;
- }
- }
- emit update(-1);
-}
-
-
-void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume)
-{
- reply->setReadBufferSize(1024 * 1024); // don't read more than 1MB at once to avoid memory troubles
- newDownload->m_Reply = reply;
- setState(newDownload, STATE_DOWNLOADING);
- if (newDownload->m_Urls.count() == 0) {
- newDownload->m_Urls = QStringList(reply->url().toString());
- }
-
- QIODevice::OpenMode mode = QIODevice::WriteOnly;
- if (resume) {
- mode |= QIODevice::Append;
- }
-
- newDownload->m_StartTime.start();
- createMetaFile(newDownload);
-
- if (!newDownload->m_Output.open(mode)) {
- reportError(tr("failed to download %1: could not open output file: %2")
- .arg(reply->url().toString()).arg(newDownload->m_Output.fileName()));
- return;
- }
-
- connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64)));
- connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
- connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
- connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged()));
-
- if (!resume) {
- newDownload->m_PreResumeSize = newDownload->m_Output.size();
- removePending(newDownload->m_FileInfo->gameName, newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID);
-
- emit aboutToUpdate();
- m_ActiveDownloads.append(newDownload);
-
- emit update(-1);
- emit downloadAdded();
-
- if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) {
- setState(newDownload, STATE_PAUSING);
- QCoreApplication::processEvents();
- if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name has already been downloaded. "
- "Do you want to download it again? The new file will receive a different name."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
- if (reply->isFinished())
- setState(newDownload, STATE_CANCELED);
- else
- setState(newDownload, STATE_CANCELING);
- } else {
- startDisableDirWatcher();
- newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true);
- endDisableDirWatcher();
- if (newDownload->m_State == STATE_PAUSED)
- resumeDownload(indexByName(newDownload->m_FileName));
- else
- setState(newDownload, STATE_DOWNLOADING);
- }
- } else
- connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
-
-
- QCoreApplication::processEvents();
-
- if (newDownload->m_State != STATE_DOWNLOADING &&
- newDownload->m_State != STATE_READY &&
- newDownload->m_State != STATE_FETCHINGMODINFO &&
- reply->isFinished()) {
- downloadFinished(indexByName(newDownload->m_FileName));
- return;
- }
- } else
- connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
-}
-
-
-void DownloadManager::addNXMDownload(const QString &url)
-{
- NXMUrl nxmInfo(url);
-
- QStringList validGames;
- validGames.append(m_ManagedGame->gameShortName());
- validGames.append(m_ManagedGame->validShortNames());
- qDebug("add nxm download: %s", qPrintable(url));
- if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) {
- qDebug("download requested for wrong game (game: %s, url: %s)", qPrintable(m_ManagedGame->gameShortName()), qPrintable(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);
- return;
- }
-
- 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()) {
- qDebug("download requested is already started (mod id: %s, file id: %s)", qPrintable(QString(nxmInfo.modId())), qPrintable(QString(nxmInfo.fileId())));
- QMessageBox::information(nullptr, tr("Already Started"), tr("A download for this mod file has already been queued."), QMessageBox::Ok);
- return;
- }
- }
-
- for (DownloadInfo *download : m_ActiveDownloads) {
- if (download->m_FileInfo->modID == nxmInfo.modId() && download->m_FileInfo->fileID == nxmInfo.fileId()) {
- if (download->m_State == STATE_DOWNLOADING || download->m_State == STATE_PAUSED || download->m_State == STATE_STARTED) {
- qDebug("download requested is already started (mod: %s, file: %s)", qPrintable(QString(download->m_FileInfo->modID)),
- qPrintable(download->m_FileInfo->fileName));
-
- QMessageBox::information(nullptr, tr("Already Started"), tr("There is already a download started for this file (mod: %1, file: %2).")
- .arg(download->m_FileInfo->modName).arg(download->m_FileInfo->fileName), QMessageBox::Ok);
- return;
- }
- }
- }
-
- emit aboutToUpdate();
-
- m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId()));
-
- emit update(-1);
- emit downloadAdded();
- m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), ""));
-}
-
-
-void DownloadManager::removeFile(int index, bool deleteFile)
-{
- //Avoid triggering refreshes from DirWatcher
- startDisableDirWatcher();
-
- if (index >= m_ActiveDownloads.size()) {
- throw MyException(tr("remove: invalid download index %1").arg(index));
- }
-
- DownloadInfo *download = m_ActiveDownloads.at(index);
- QString filePath = m_OutputDirectory + "/" + download->m_FileName;
- if ((download->m_State == STATE_STARTED) ||
- (download->m_State == STATE_DOWNLOADING)) {
- // shouldn't have been possible
- qCritical("tried to remove active download");
- endDisableDirWatcher();
- return;
- }
-
- if ((download->m_State == STATE_PAUSED) || (download->m_State == STATE_ERROR)) {
- filePath = download->m_Output.fileName();
- }
-
- if (deleteFile) {
- if (!shellDelete(QStringList(filePath), true)) {
- reportError(tr("failed to delete %1").arg(filePath));
- endDisableDirWatcher();
- return;
- }
-
- QFile metaFile(filePath.append(".meta"));
- if (metaFile.exists() && !shellDelete(QStringList(filePath), true)) {
- reportError(tr("failed to delete meta file for %1").arg(filePath));
- }
- } else {
- QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat);
- if(!download->m_Hidden)
- metaSettings.setValue("removed", true);
- }
-
- endDisableDirWatcher();
-}
-
-class LessThanWrapper
-{
-public:
- LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {}
- bool operator()(int LHS, int RHS) {
- return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0;
-
- }
-
-private:
- DownloadManager *m_Manager;
-};
-
-
-bool DownloadManager::ByName(int LHS, int RHS)
-{
- return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName;
-}
-
-
-void DownloadManager::refreshAlphabeticalTranslation()
-{
- m_AlphabeticalTranslation.clear();
- int pos = 0;
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) {
- m_AlphabeticalTranslation.push_back(pos);
- }
-
- qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this));
-}
-
-
-void DownloadManager::restoreDownload(int index)
-{
-
- if (index < 0) {
- DownloadState minState = STATE_READY ;
- index = 0;
-
- for (QVector<DownloadInfo*>::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) {
-
- if ((*iter)->m_State >= minState) {
- restoreDownload(index);
- }
- index++;
- }
- }
- else {
- if (index >= m_ActiveDownloads.size()) {
- throw MyException(tr("restore: invalid download index: %1").arg(index));
- }
-
- DownloadInfo *download = m_ActiveDownloads.at(index);
- if (download->m_Hidden) {
- download->m_Hidden = false;
-
- QString filePath = m_OutputDirectory + "/" + download->m_FileName;
-
- //avoid dirWatcher triggering refreshes
- startDisableDirWatcher();
- QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat);
- metaSettings.setValue("removed", false);
-
- endDisableDirWatcher();
- }
- }
-}
-
-
-void DownloadManager::removeDownload(int index, bool deleteFile)
-{
- try {
- //avoid dirWatcher triggering refreshes
- startDisableDirWatcher();
-
- emit aboutToUpdate();
-
- if (index < 0) {
- DownloadState minState;
- if (index == -3) {
- minState = STATE_UNINSTALLED;
- }
- else
- minState = index == -1 ? STATE_READY : STATE_INSTALLED;
-
- index = 0;
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) {
- if ((*iter)->m_State >= minState) {
- removeFile(index, deleteFile);
- delete *iter;
- iter = m_ActiveDownloads.erase(iter);
- //QCoreApplication::processEvents();
- }
- else {
- ++iter;
- ++index;
- }
- }
- } else {
- if (index >= m_ActiveDownloads.size()) {
- reportError(tr("remove: invalid download index %1").arg(index));
- //emit update(-1);
- endDisableDirWatcher();
- return;
- }
-
- removeFile(index, deleteFile);
- delete m_ActiveDownloads.at(index);
- m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index);
- }
- emit update(-1);
- endDisableDirWatcher();
- } catch (const std::exception &e) {
- qCritical("failed to remove download: %s", e.what());
- }
- refreshList();
-}
-
-
-void DownloadManager::cancelDownload(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("cancel: invalid download index %1").arg(index));
- return;
- }
-
- if (m_ActiveDownloads.at(index)->m_State == STATE_DOWNLOADING) {
- setState(m_ActiveDownloads.at(index), STATE_CANCELING);
- }
-}
-
-
-void DownloadManager::pauseDownload(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("pause: invalid download index %1").arg(index));
- return;
- }
-
- DownloadInfo *info = m_ActiveDownloads.at(index);
-
- if (info->m_State == STATE_DOWNLOADING) {
- if ((info->m_Reply != nullptr) && (info->m_Reply->isRunning())) {
- setState(info, STATE_PAUSING);
- } else {
- setState(info, STATE_PAUSED);
- }
- } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) {
- setState(info, STATE_READY);
- }
-}
-
-void DownloadManager::resumeDownload(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("resume: invalid download index %1").arg(index));
- return;
- }
- DownloadInfo *info = m_ActiveDownloads[index];
- info->m_Tries = AUTOMATIC_RETRIES;
- resumeDownloadInt(index);
-}
-
-void DownloadManager::resumeDownloadInt(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("resume (int): invalid download index %1").arg(index));
- return;
- }
- DownloadInfo *info = m_ActiveDownloads[index];
-
- // Check for finished download;
- if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr
- && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) {
- setState(info, STATE_DOWNLOADING);
- downloadFinished(index);
- return;
- }
-
- if (info->isPausedState() || info->m_State == STATE_PAUSING) {
- if (info->m_State == STATE_PAUSING) {
- if (info->m_Output.isOpen()) {
- writeData(info);
- if (info->m_State == STATE_PAUSING) {
- setState(info, STATE_PAUSED);
- }
- }
- }
- if ((info->m_Urls.size() == 0)
- || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) {
- emit showMessage(tr("No known download urls. Sorry, this download can't be resumed."));
- return;
- }
- if (info->m_State == STATE_ERROR) {
- info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count();
- }
- qDebug("request resume from url %s", qPrintable(info->currentURL()));
- QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit()));
- request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent());
- if (info->m_State != STATE_ERROR) {
- info->m_ResumePos = info->m_Output.size();
- QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-";
- request.setRawHeader("Range", rangeHeader);
- }
- std::get<0>(info->m_SpeedDiff) = 0;
- std::get<1>(info->m_SpeedDiff) = 0;
- std::get<2>(info->m_SpeedDiff) = 0;
- std::get<3>(info->m_SpeedDiff) = 0;
- std::get<4>(info->m_SpeedDiff) = 0;
- qDebug("resume at %lld bytes", info->m_ResumePos);
- startDownload(m_NexusInterface->getAccessManager()->get(request), info, true);
- }
- emit update(index);
-}
-
-
-DownloadManager::DownloadInfo *DownloadManager::downloadInfoByID(unsigned int id)
-{
- auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(),
- [id](DownloadInfo *info) { return info->m_DownloadID == id; });
- if (iter != m_ActiveDownloads.end()) {
- return *iter;
- } else {
- return nullptr;
- }
-}
-
-
-void DownloadManager::queryInfo(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("query: invalid download index %1").arg(index));
- return;
- }
- DownloadInfo *info = m_ActiveDownloads[index];
-
- if (info->m_FileInfo->repository != "Nexus") {
- qWarning("re-querying file info is currently only possible with Nexus");
- return;
- }
-
- if (info->m_State < DownloadManager::STATE_READY) {
- // UI shouldn't allow this
- return;
- }
-
- if (info->m_FileInfo->modID <= 0) {
- QString fileName = getFileName(index);
- QString ignore;
- NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true);
- if (info->m_FileInfo->modID < 0) {
- bool ok = false;
- int modId = QInputDialog::getInt(
- nullptr, tr("Please enter the nexus mod id"), tr("Mod ID:"), 1, 1,
- std::numeric_limits<int>::max(), 1, &ok);
- // careful now: while the dialog was displayed, events were processed.
- // the download list might have changed and our info-ptr invalidated.
- m_ActiveDownloads[index]->m_FileInfo->modID = modId;
- return;
- }
- }
-
- if (info->m_FileInfo->gameName.size() == 0) {
- SelectionDialog selection(tr("Please select the source game code for %1").arg(getFileName(index)));
-
- std::vector<std::pair<QString, QString>> choices = m_NexusInterface->getGameChoices(m_ManagedGame);
- for (auto choice : choices) {
- selection.addChoice(choice.first, choice.second, choice.first);
- }
- if (selection.exec() == QDialog::Accepted) {
- info->m_FileInfo->gameName = selection.getChoiceData().toString();
- } else {
- info->m_FileInfo->gameName = m_ManagedGame->gameShortName();
- }
- }
- info->m_ReQueried = true;
- setState(info, STATE_FETCHINGMODINFO);
-}
-
-void DownloadManager::visitOnNexus(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("VisitNexus: invalid download index %1").arg(index));
- return;
- }
- DownloadInfo *info = m_ActiveDownloads[index];
-
- if (info->m_FileInfo->repository != "Nexus") {
- qWarning("Visiting mod page is currently only possible with Nexus");
- return;
- }
-
- if (info->m_State < DownloadManager::STATE_READY) {
- // UI shouldn't allow this
- return;
- }
- int modID = info->m_FileInfo->modID;
-
- QString gameName = info->m_FileInfo->gameName;
- if (modID > 0) {
- QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName)));
- }
- else {
- emit showMessage(tr("Nexus ID for this Mod is unknown"));
- }
-}
-
-void DownloadManager::openFile(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- 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);
- return;
- }
-
- ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- return;
-}
-
-void DownloadManager::openInDownloadsFolder(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- 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") + "\"";
-
- ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL);
- return;
- }
-
- ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- return;
-}
-
-
-int DownloadManager::numTotalDownloads() const
-{
- return m_ActiveDownloads.size();
-}
-
-int DownloadManager::numPendingDownloads() const
-{
- return m_PendingDownloads.size();
-}
-
-std::tuple<QString, int, int> DownloadManager::getPendingDownload(int index)
-{
- if ((index < 0) || (index >= m_PendingDownloads.size())) {
- throw MyException(tr("get pending: invalid download index %1").arg(index));
- }
-
- return m_PendingDownloads.at(index);
-}
-
-QString DownloadManager::getFilePath(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("get path: invalid download index %1").arg(index));
- }
-
- return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName;
-}
-
-QString DownloadManager::getFileTypeString(int fileType)
-{
- switch (fileType) {
- case 1: return tr("Main");
- case 2: return tr("Update");
- case 3: return tr("Optional");
- case 4: return tr("Old");
- case 5: return tr("Misc");
- default: return tr("Unknown");
- }
-}
-
-QString DownloadManager::getDisplayName(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("display name: invalid download index %1").arg(index));
- }
-
- DownloadInfo *info = m_ActiveDownloads.at(index);
-
- QTextDocument doc;
- if (!info->m_FileInfo->name.isEmpty()) {
- doc.setHtml(info->m_FileInfo->name);
- return QString("%1 (%2, v%3)").arg(doc.toPlainText())
- .arg(getFileTypeString(info->m_FileInfo->fileCategory))
- .arg(info->m_FileInfo->version.displayString());
- } else {
- doc.setHtml(info->m_FileName);
- return doc.toPlainText();
- }
-}
-
-QString DownloadManager::getFileName(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("file name: invalid download index %1").arg(index));
- }
-
- return m_ActiveDownloads.at(index)->m_FileName;
-}
-
-QDateTime DownloadManager::getFileTime(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("file time: invalid download index %1").arg(index));
- }
-
- DownloadInfo *info = m_ActiveDownloads.at(index);
- if (!info->m_Created.isValid()) {
- info->m_Created = QFileInfo(info->m_Output).created();
- }
-
- return info->m_Created;
-}
-
-qint64 DownloadManager::getFileSize(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("file size: invalid download index %1").arg(index));
- }
-
- return m_ActiveDownloads.at(index)->m_TotalSize;
-}
-
-
-std::pair<int, QString> DownloadManager::getProgress(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("progress: invalid download index %1").arg(index));
- }
-
- return m_ActiveDownloads.at(index)->m_Progress;
-}
-
-
-DownloadManager::DownloadState DownloadManager::getState(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("state: invalid download index %1").arg(index));
- }
-
- return m_ActiveDownloads.at(index)->m_State;
-}
-
-
-bool DownloadManager::isInfoIncomplete(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("infocomplete: invalid download index %1").arg(index));
- }
-
- DownloadInfo *info = m_ActiveDownloads.at(index);
- if (info->m_FileInfo->repository != "Nexus") {
- // other repositories currently don't support re-querying info anyway
- return false;
- }
- return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0);
-}
-
-
-int DownloadManager::getModID(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("mod id: invalid download index %1").arg(index));
- }
- return m_ActiveDownloads.at(index)->m_FileInfo->modID;
-}
-
-QString DownloadManager::getGameName(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("mod id: invalid download index %1").arg(index));
- }
- return m_ActiveDownloads.at(index)->m_FileInfo->gameName;
-}
-
-bool DownloadManager::isHidden(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("ishidden: invalid download index %1").arg(index));
- }
- return m_ActiveDownloads.at(index)->m_Hidden;
-}
-
-
-const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("file info: invalid download index %1").arg(index));
- }
-
- return m_ActiveDownloads.at(index)->m_FileInfo;
-}
-
-
-void DownloadManager::markInstalled(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("mark installed: invalid download index %1").arg(index));
- }
-
- //Avoid triggering refreshes from DirWatcher
- startDisableDirWatcher();
-
- DownloadInfo *info = m_ActiveDownloads.at(index);
- QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
- metaFile.setValue("installed", true);
- metaFile.setValue("uninstalled", false);
-
- endDisableDirWatcher();
-
- setState(m_ActiveDownloads.at(index), STATE_INSTALLED);
-}
-
-void DownloadManager::markInstalled(QString fileName)
-{
- int index = indexByName(fileName);
- if (index >= 0) {
- markInstalled(index);
- } else {
- DownloadInfo *info = getDownloadInfo(fileName);
- if (info != nullptr) {
- //Avoid triggering refreshes from DirWatcher
- startDisableDirWatcher();
-
- QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
- metaFile.setValue("installed", true);
- metaFile.setValue("uninstalled", false);
- delete info;
-
- endDisableDirWatcher();
- }
- }
-}
-
-DownloadManager::DownloadInfo* DownloadManager::getDownloadInfo(QString fileName)
-{
- return DownloadInfo::createFromMeta(fileName, true, m_OutputDirectory);
-}
-
-void DownloadManager::markUninstalled(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- throw MyException(tr("mark uninstalled: invalid download index %1").arg(index));
- }
-
- //Avoid triggering refreshes from DirWatcher
- startDisableDirWatcher();
-
- DownloadInfo *info = m_ActiveDownloads.at(index);
- QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
- metaFile.setValue("uninstalled", true);
-
- endDisableDirWatcher();
-
- setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED);
-}
-
-
-void DownloadManager::markUninstalled(QString fileName)
-{
- int index = indexByName(fileName);
- if (index >= 0) {
- markUninstalled(index);
- } else {
- QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName;
- DownloadInfo *info = getDownloadInfo(filePath);
- if (info != nullptr) {
-
- //Avoid triggering refreshes from DirWatcher
- startDisableDirWatcher();
-
- QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
- metaFile.setValue("uninstalled", true);
- delete info;
-
- endDisableDirWatcher();
- }
- }
-}
-
-
-QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const
-{
- QString fullPath = m_OutputDirectory + "/" + baseName;
- if (QFile::exists(fullPath) && rename) {
- int i = 1;
- while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) {
- ++i;
- }
-
- fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName);
- }
- return fullPath;
-}
-
-
-QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply)
-{
- if (reply->hasRawHeader("Content-Disposition")) {
- std::regex exp("filename=\"(.*)\"");
-
- std::cmatch result;
- if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) {
- return QString::fromUtf8(result.str(1).c_str());
- }
- }
-
- return QString();
-}
-
-
-void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadManager::DownloadState state)
-{
- int row = 0;
- for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
- if (m_ActiveDownloads[i] == info) {
- row = i;
- break;
- }
- }
- info->m_State = state;
- switch (state) {
- case STATE_PAUSED:
- case STATE_ERROR: {
- info->m_Reply->abort();
- info->m_Output.close();
- } break;
- case STATE_CANCELED: {
- info->m_Reply->abort();
- } break;
- case STATE_FETCHINGMODINFO: {
- m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString()));
- } break;
- case STATE_FETCHINGFILEINFO: {
- m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString()));
- } break;
- case STATE_READY: {
- createMetaFile(info);
- emit downloadComplete(row);
- } break;
- default: /* NOP */ break;
- }
- emit stateChanged(row, state);
-}
-
-
-DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const
-{
- // reverse search as newer, thus more relevant, downloads are at the end
- for (int i = m_ActiveDownloads.size() - 1; i >= 0; --i) {
- if (m_ActiveDownloads[i]->m_Reply == reply) {
- if (index != nullptr) {
- *index = i;
- }
- return m_ActiveDownloads[i];
- }
- }
- return nullptr;
-}
-
-
-void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
-{
- if (bytesTotal == 0) {
- return;
- }
- int index = 0;
- try {
- DownloadInfo *info = findDownload(this->sender(), &index);
- if (info != nullptr) {
- info->m_HasData = true;
- if (info->m_State == STATE_CANCELING) {
- setState(info, STATE_CANCELED);
- } else if (info->m_State == STATE_PAUSING) {
- setState(info, STATE_PAUSED);
- }
- else {
- if (bytesTotal > info->m_TotalSize) {
- info->m_TotalSize = bytesTotal;
- }
- int oldProgress = info->m_Progress.first;
- info->m_Progress.first = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal);
-
- int elapsed = info->m_StartTime.elapsed();
- std::get<0>(info->m_SpeedDiff) = bytesReceived - std::get<2>(info->m_SpeedDiff);
- std::get<1>(info->m_SpeedDiff) = elapsed - std::get<3>(info->m_SpeedDiff);
- std::get<2>(info->m_SpeedDiff) = bytesReceived;
- std::get<3>(info->m_SpeedDiff) = elapsed;
-
- double calc = ((double)std::get<0>(info->m_SpeedDiff)) / (((double)(std::get<1>(info->m_SpeedDiff)) / 5000.0));
- std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2;
-
- // calculate the download speed
- double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000);
-
- QString unit;
- if (speed < 1000) {
- unit = "Bytes/s";
- }
- else if (speed < 1000*1024) {
- speed /= 1024;
- unit = "KB/s";
- }
- else {
- speed /= 1024 * 1024;
- unit = "MB/s";
- }
-
- info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(speed, 8, 'f', 1,' ').arg(unit, -8, ' ');
-
- TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal);
- emit update(index);
- }
- }
- } catch (const std::bad_alloc&) {
- reportError(tr("Memory allocation error (in processing progress event)."));
- }
-}
-
-
-void DownloadManager::downloadReadyRead()
-{
- try {
- writeData(findDownload(this->sender()));
- } catch (const std::bad_alloc&) {
- reportError(tr("Memory allocation error (in processing downloaded data)."));
- }
-}
-
-
-void DownloadManager::createMetaFile(DownloadInfo *info)
-{
- //Avoid triggering refreshes from DirWatcher
- startDisableDirWatcher();
-
- QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat);
- metaFile.setValue("gameName", info->m_FileInfo->gameName);
- metaFile.setValue("modID", info->m_FileInfo->modID);
- metaFile.setValue("fileID", info->m_FileInfo->fileID);
- metaFile.setValue("url", info->m_Urls.join(";"));
- metaFile.setValue("name", info->m_FileInfo->name);
- metaFile.setValue("description", info->m_FileInfo->description);
- metaFile.setValue("modName", info->m_FileInfo->modName);
- metaFile.setValue("version", info->m_FileInfo->version.canonicalString());
- metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString());
- metaFile.setValue("fileTime", info->m_FileInfo->fileTime);
- metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory);
- metaFile.setValue("category", info->m_FileInfo->categoryID);
- metaFile.setValue("repository", info->m_FileInfo->repository);
- metaFile.setValue("userData", info->m_FileInfo->userData);
- metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED);
- metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED);
- metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) ||
- (info->m_State == DownloadManager::STATE_ERROR));
- metaFile.setValue("removed", info->m_Hidden);
-
- endDisableDirWatcher();
- // slightly hackish...
- for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
- if (m_ActiveDownloads[i] == info) {
- emit update(i);
- }
- }
-}
-
-
-void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
- if (idIter == m_RequestIDs.end()) {
- return;
- } else {
- m_RequestIDs.erase(idIter);
- }
-
- QVariantMap result = resultData.toMap();
-
- DownloadInfo *info = downloadInfoByID(userData.toInt());
- if (info == nullptr) return;
- info->m_FileInfo->categoryID = result["category_id"].toInt();
- QTextDocument doc;
- doc.setHtml(result["name"].toString().trimmed());
- info->m_FileInfo->modName = doc.toPlainText();
- info->m_FileInfo->newestVersion.parse(result["version"].toString());
- if (info->m_FileInfo->fileID != 0) {
- setState(info, STATE_READY);
- } else {
- setState(info, STATE_FETCHINGFILEINFO);
- }
-}
-
-
-QDateTime DownloadManager::matchDate(const QString &timeString)
-{
- if (m_DateExpression.exactMatch(timeString)) {
- return QDateTime::fromMSecsSinceEpoch(m_DateExpression.cap(1).toLongLong());
- } else {
- qWarning("date not matched: %s", qPrintable(timeString));
- return QDateTime::currentDateTime();
- }
-}
-
-
-void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
- if (idIter == m_RequestIDs.end()) {
- return;
- } else {
- m_RequestIDs.erase(idIter);
- }
-
- DownloadInfo *info = downloadInfoByID(userData.toInt());
- if (info == nullptr) return;
-
- QVariantList result = resultData.toList();
-
- // MO sometimes prepends <digit>_ to the filename in case of duplicate downloads.
- // this may muck up the file name comparison
- QString alternativeLocalName = info->m_FileName;
-
- QRegExp expression("^\\d_(.*)$");
- if (expression.indexIn(alternativeLocalName) == 0) {
- alternativeLocalName = expression.cap(1);
- }
-
- bool found = false;
-
- for (QVariant file : result) {
- QVariantMap fileInfo = file.toMap();
- QString fileName = fileInfo["uri"].toString();
- QString fileNameVariant = fileName.mid(0).replace(' ', '_');
- if ((fileName == info->m_FileName) || (fileName == alternativeLocalName) ||
- (fileNameVariant == info->m_FileName) || (fileNameVariant == alternativeLocalName)) {
- info->m_FileInfo->name = fileInfo["name"].toString();
- info->m_FileInfo->version.parse(fileInfo["version"].toString());
- if (!info->m_FileInfo->version.isValid()) {
- info->m_FileInfo->version = info->m_FileInfo->newestVersion;
- }
- info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt();
- info->m_FileInfo->fileTime = matchDate(fileInfo["date"].toString());
- info->m_FileInfo->fileID = fileInfo["id"].toInt();
- info->m_FileInfo->fileName = fileInfo["uri"].toString();
- info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["description"].toString());
- found = true;
- break;
- }
- }
-
- if (info->m_ReQueried) {
- if (found) {
- emit showMessage(tr("Information updated"));
- } else if (result.count() == 0) {
- emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?"));
- } else {
- SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one."));
- for (QVariant file : result) {
- QVariantMap fileInfo = file.toMap();
- selection.addChoice(fileInfo["uri"].toString(), "", file);
- }
- if (selection.exec() == QDialog::Accepted) {
- QVariantMap fileInfo = selection.getChoiceData().toMap();
- info->m_FileInfo->name = fileInfo["name"].toString();
- info->m_FileInfo->version.parse(fileInfo["version"].toString());
- info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt();
- info->m_FileInfo->fileID = fileInfo["id"].toInt();
- } else {
- emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?"));
- }
- }
- } else {
- if (info->m_FileInfo->fileID == 0) {
- qWarning("could not determine file id for %s (state %d)",
- info->m_FileName.toUtf8().constData(), info->m_State);
- }
- }
-
- setState(info, STATE_READY);
-}
-
-
-void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
- if (idIter == m_RequestIDs.end()) {
- return;
- } else {
- m_RequestIDs.erase(idIter);
- }
-
- ModRepositoryFileInfo *info = new ModRepositoryFileInfo();
-
- QVariantMap result = resultData.toMap();
- info->name = result["name"].toString();
- info->version.parse(result["version"].toString());
- if (!info->version.isValid()) {
- info->version = info->newestVersion;
- }
- info->fileName = result["uri"].toString();
- info->fileCategory = result["category_id"].toInt();
- info->fileTime = matchDate(result["date"].toString());
- info->description = BBCode::convertToHTML(result["description"].toString());
-
- info->repository = "Nexus";
- info->gameName = gameName;
- info->modID = modID;
- info->fileID = fileID;
-
- QObject *test = info;
- m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(gameName, modID, fileID, this, qVariantFromValue(test), QString()));
-}
-
-static int evaluateFileInfoMap(const QVariantMap &map, const std::map<QString, int> &preferredServers)
-{
- int result = 0;
-
- int users = map["ConnectedUsers"].toInt();
- // 0 users is probably a sign that the server is offline. Since there is currently no
- // mechanism to try a different server, we avoid those without users
- if (users == 0) {
- result -= 500;
- } else {
- result -= users;
- }
-
- auto preference = preferredServers.find(map["Name"].toString());
-
- if (preference != preferredServers.end()) {
- result += 100 + preference->second * 20;
- }
-
- if (map["IsPremium"].toBool()) result += 5;
-
- return result;
-}
-
-// sort function to sort by best download server
-bool DownloadManager::ServerByPreference(const std::map<QString, int> &preferredServers, const QVariant &LHS, const QVariant &RHS)
-{
- return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers);
-}
-
-int DownloadManager::startDownloadURLs(const QStringList &urls)
-{
- ModRepositoryFileInfo info;
- addDownload(urls, "", -1, -1, &info);
- return m_ActiveDownloads.size() - 1;
-}
-
-int DownloadManager::startDownloadNexusFile(int modID, int fileID)
-{
- int newID = m_ActiveDownloads.size();
- addNXMDownload(QString("nxm://%1/mods/%2/files/%3").arg(m_ManagedGame->gameShortName()).arg(modID).arg(fileID));
- return newID;
-}
-
-QString DownloadManager::downloadPath(int id)
-{
- return getFilePath(id);
-}
-
-int DownloadManager::indexByName(const QString &fileName) const
-{
- for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
- if (m_ActiveDownloads[i]->m_FileName == fileName) {
- return i;
- }
- }
- return -1;
-}
-
-void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
- if (idIter == m_RequestIDs.end()) {
- return;
- } else {
- m_RequestIDs.erase(idIter);
- }
-
- ModRepositoryFileInfo *info = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(userData));
- QVariantList resultList = resultData.toList();
- if (resultList.length() == 0) {
- removePending(gameName, modID, fileID);
- emit showMessage(tr("No download server available. Please try again later."));
- return;
- }
-
- std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2));
-
- info->userData["downloadMap"] = resultList;
-
- QStringList URLs;
-
- foreach (const QVariant &server, resultList) {
- URLs.append(server.toMap()["URI"].toString());
- }
- addDownload(URLs, gameName, modID, fileID, info);
-}
-
-
-void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString)
-{
- std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
- if (idIter == m_RequestIDs.end()) {
- return;
- } else {
- m_RequestIDs.erase(idIter);
- }
-
- int index = 0;
-
- for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) {
- DownloadInfo *info = *iter;
- if (info->m_FileInfo->modID == modID) {
- if (info->m_State < STATE_FETCHINGMODINFO) {
- m_ActiveDownloads.erase(iter);
- delete info;
- } else {
- setState(info, STATE_READY);
- }
- emit update(index);
- break;
- }
- }
-
- removePending(gameName, modID, fileID);
- emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString));
-}
-
-
-void DownloadManager::downloadFinished(int index)
-{
- DownloadInfo *info;
- if (index)
- info = m_ActiveDownloads[index];
- else
- info = findDownload(this->sender(), &index);
-
- if (info != nullptr) {
- QNetworkReply *reply = info->m_Reply;
- QByteArray data;
- if (reply->isOpen() && info->m_HasData) {
- data = reply->readAll();
- info->m_Output.write(data);
- }
- info->m_Output.close();
- TaskProgressManager::instance().forgetMe(info->m_TaskProgressId);
-
- bool error = false;
- if ((info->m_State != STATE_CANCELING) &&
- (info->m_State != STATE_PAUSING)) {
- bool textData = reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive);
- if (textData)
- emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString()));
- if ((info->m_Output.size() == 0) ||
- ((reply->error() != QNetworkReply::NoError)
- && (reply->error() != QNetworkReply::OperationCanceledError))) {
- if (reply->error() == QNetworkReply::UnknownContentError)
- emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size()));
- if (info->m_Tries == 0) {
- emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error()));
- }
- error = true;
- setState(info, STATE_ERROR);
- }
- }
-
- if (info->m_State == STATE_CANCELING) {
- setState(info, STATE_CANCELED);
- } else if (info->m_State == STATE_PAUSING) {
- if (info->m_Output.isOpen() && info->m_HasData) {
- info->m_Output.write(info->m_Reply->readAll());
- }
- setState(info, STATE_PAUSED);
- }
-
- if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) {
- emit aboutToUpdate();
- info->m_Output.remove();
- delete info;
- m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index);
- if (error)
- emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers."));
- emit update(-1);
- } else if (info->isPausedState() || info->m_State == STATE_PAUSING) {
- info->m_Output.close();
- createMetaFile(info);
- emit update(index);
- } else {
- QString url = info->m_Urls[info->m_CurrentUrl];
- if (info->m_FileInfo->userData.contains("downloadMap")) {
- foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) {
- QVariantMap serverMap = server.toMap();
- if (serverMap["URI"].toString() == url) {
- int deltaTime = info->m_StartTime.secsTo(QTime::currentTime());
- if (deltaTime > 5) {
- emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime);
- } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise
- break;
- }
- }
- }
-
- bool isNexus = info->m_FileInfo->repository == "Nexus";
- // need to change state before changing the file name, otherwise .unfinished is appended
- if (isNexus) {
- setState(info, STATE_FETCHINGMODINFO);
- } else {
- setState(info, STATE_NOFETCH);
- }
-
- QString newName = getFileNameFromNetworkReply(reply);
- QString oldName = QFileInfo(info->m_Output).fileName();
-
- startDisableDirWatcher();
- if (!newName.isEmpty() && (oldName.isEmpty())) {
- info->setName(getDownloadFileName(newName), true);
- } else {
- info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension
- }
- endDisableDirWatcher();
-
- if (!isNexus) {
- setState(info, STATE_READY);
- }
-
- emit update(index);
- }
- reply->close();
- reply->deleteLater();
-
- if ((info->m_Tries > 0) && error) {
- --info->m_Tries;
- resumeDownloadInt(index);
- }
- } else {
- qWarning("no download index %d", index);
- }
-}
-
-
-void DownloadManager::downloadError(QNetworkReply::NetworkError error)
-{
- if (error != QNetworkReply::OperationCanceledError) {
- QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
- qWarning("%s (%d)", reply != nullptr ? qPrintable(reply->errorString())
- : "Download error occured",
- error);
- }
-}
-
-
-void DownloadManager::metaDataChanged()
-{
- int index = 0;
-
- DownloadInfo *info = findDownload(this->sender(), &index);
- if (info != nullptr) {
- QString newName = getFileNameFromNetworkReply(info->m_Reply);
- if (!newName.isEmpty() && (info->m_FileName.isEmpty())) {
- startDisableDirWatcher();
- info->setName(getDownloadFileName(newName), true);
- endDisableDirWatcher();
- refreshAlphabeticalTranslation();
- if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) {
- reportError(tr("failed to re-open %1").arg(info->m_FileName));
- setState(info, STATE_CANCELING);
- }
- }
- } else {
- qWarning("meta data event for unknown download");
- }
-}
-
-void DownloadManager::directoryChanged(const QString&)
-{
- if(DownloadManager::m_DirWatcherDisabler==0)
- refreshList();
-}
-
-void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame)
-{
- m_ManagedGame = managedGame;
-}
-
-void DownloadManager::checkDownloadTimeout()
-{
- for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
- if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 &&
- m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr &&
- m_ActiveDownloads[i]->m_Reply->isOpen()) {
- pauseDownload(i);
- downloadFinished(i);
- resumeDownload(i);
- }
- }
-}
-
-void DownloadManager::writeData(DownloadInfo *info)
-{
- if (info != nullptr) {
- qint64 ret = info->m_Output.write(info->m_Reply->readAll());
- if (ret < info->m_Reply->size()) {
- QString fileName = info->m_FileName; // m_FileName may be destroyed after setState
- setState(info, DownloadState::STATE_CANCELED);
- qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit());
- reportError(tr("Unable to write download to drive (return %1).\n"
- "Check the drive's available storage.\n\n"
- "Canceling download \"%2\"...").arg(ret).arg(fileName));
- }
- }
-} \ No newline at end of file
+/*
+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 "downloadmanager.h"
+
+#include "organizercore.h"
+#include "nxmurl.h"
+#include "nexusinterface.h"
+#include "nxmaccessmanager.h"
+#include "iplugingame.h"
+#include <nxmurl.h>
+#include <taskprogressmanager.h>
+#include "utility.h"
+#include "selectiondialog.h"
+#include "bbcode.h"
+#include <utility.h>
+#include <report.h>
+
+#include <QTimer>
+#include <QFileInfo>
+#include <QRegExp>
+#include <QDirIterator>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QMessageBox>
+#include <QCoreApplication>
+#include <QTextDocument>
+
+#include <boost/bind.hpp>
+#include <regex>
+
+
+using namespace MOBase;
+
+
+// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads
+
+
+static const char UNFINISHED[] = ".unfinished";
+
+unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U;
+int DownloadManager::m_DirWatcherDisabler = 0;
+
+
+DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs)
+{
+ DownloadInfo *info = new DownloadInfo;
+ info->m_DownloadID = s_NextDownloadID++;
+ info->m_StartTime.start();
+ info->m_PreResumeSize = 0LL;
+ info->m_Progress = std::make_pair<int, QString>(0, "0.0 B/s ");
+ info->m_ResumePos = 0;
+ info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo);
+ info->m_Urls = URLs;
+ info->m_CurrentUrl = 0;
+ info->m_Tries = AUTOMATIC_RETRIES;
+ info->m_State = STATE_STARTED;
+ info->m_TaskProgressId = TaskProgressManager::instance().getId();
+ info->m_Reply = nullptr;
+
+ return info;
+}
+
+DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory)
+{
+ DownloadInfo *info = new DownloadInfo;
+
+ QString metaFileName = filePath + ".meta";
+ QFileInfo metaFileInfo(metaFileName);
+ if (QDir::fromNativeSeparators(metaFileInfo.path()).compare(QDir::fromNativeSeparators(outputDirectory), Qt::CaseInsensitive) != 0) return nullptr;
+ QSettings metaFile(metaFileName, QSettings::IniFormat);
+ if (!showHidden && metaFile.value("removed", false).toBool()) {
+ return nullptr;
+ } else {
+ info->m_Hidden = metaFile.value("removed", false).toBool();
+ }
+
+ QString fileName = QFileInfo(filePath).fileName();
+
+ if (fileName.endsWith(UNFINISHED)) {
+ info->m_FileName = fileName.mid(
+ 0, fileName.length() - static_cast<int>(strlen(UNFINISHED)));
+ info->m_State = STATE_PAUSED;
+ } else {
+ info->m_FileName = fileName;
+
+ if (metaFile.value("paused", false).toBool()) {
+ info->m_State = STATE_PAUSED;
+ } else if (metaFile.value("uninstalled", false).toBool()) {
+ info->m_State = STATE_UNINSTALLED;
+ } else if (metaFile.value("installed", false).toBool()) {
+ info->m_State = STATE_INSTALLED;
+ } else {
+ info->m_State = STATE_READY;
+ }
+ }
+
+ info->m_DownloadID = s_NextDownloadID++;
+ info->m_Output.setFileName(filePath);
+ info->m_TotalSize = QFileInfo(filePath).size();
+ info->m_PreResumeSize = info->m_TotalSize;
+ info->m_CurrentUrl = 0;
+ info->m_Urls = metaFile.value("url", "").toString().split(";");
+ info->m_Tries = 0;
+ info->m_TaskProgressId = TaskProgressManager::instance().getId();
+ QString gameName = metaFile.value("gameName", "").toString();
+ int modID = metaFile.value("modID", 0).toInt();
+ int fileID = metaFile.value("fileID", 0).toInt();
+ info->m_FileInfo = new ModRepositoryFileInfo(gameName, modID, fileID);
+ info->m_FileInfo->name = metaFile.value("name", "").toString();
+ if (info->m_FileInfo->name == "0") {
+ // bug in earlier version
+ info->m_FileInfo->name = "";
+ }
+ info->m_FileInfo->modName = metaFile.value("modName", "").toString();
+ info->m_FileInfo->gameName = gameName;
+ info->m_FileInfo->modID = modID;
+ info->m_FileInfo->fileID = fileID;
+ info->m_FileInfo->description = metaFile.value("description").toString();
+ info->m_FileInfo->version.parse(metaFile.value("version", "0").toString());
+ info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString());
+ info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt();
+ info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt();
+ info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString();
+ info->m_FileInfo->userData = metaFile.value("userData").toMap();
+ info->m_Reply = nullptr;
+
+ return info;
+}
+
+void DownloadManager::startDisableDirWatcher()
+{
+ DownloadManager::m_DirWatcherDisabler++;
+}
+
+
+void DownloadManager::endDisableDirWatcher()
+{
+ if (DownloadManager::m_DirWatcherDisabler > 0)
+ {
+ if (DownloadManager::m_DirWatcherDisabler == 1)
+ QCoreApplication::processEvents();
+ DownloadManager::m_DirWatcherDisabler--;
+ }
+ else {
+ DownloadManager::m_DirWatcherDisabler = 0;
+ }
+}
+
+void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile)
+{
+ QString oldMetaFileName = QString("%1.meta").arg(m_FileName);
+ m_FileName = QFileInfo(newName).fileName();
+ if ((m_State == DownloadManager::STATE_STARTED) ||
+ (m_State == DownloadManager::STATE_DOWNLOADING) ||
+ (m_State == DownloadManager::STATE_PAUSED)) {
+ newName.append(UNFINISHED);
+ oldMetaFileName = QString("%1%2.meta").arg(m_FileName).arg(UNFINISHED);
+ }
+ if (renameFile) {
+ if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) {
+ reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName));
+ return;
+ }
+
+ QFile metaFile(QFileInfo(newName).path() + "/" + oldMetaFileName);
+ if (metaFile.exists())
+ metaFile.rename(newName.mid(0).append(".meta"));
+ }
+ if (!m_Output.isOpen()) {
+ // can't set file name if it's open
+ m_Output.setFileName(newName);
+ }
+}
+
+bool DownloadManager::DownloadInfo::isPausedState()
+{
+ return m_State == STATE_PAUSED || m_State == STATE_ERROR;
+}
+
+QString DownloadManager::DownloadInfo::currentURL()
+{
+ return m_Urls[m_CurrentUrl];
+}
+
+
+DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent)
+ : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false)
+{
+ m_OrganizerCore = dynamic_cast<OrganizerCore*>(parent);
+ connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
+ m_TimeoutTimer.setSingleShot(false);
+ //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout()));
+ m_TimeoutTimer.start(5 * 1000);
+}
+
+
+DownloadManager::~DownloadManager()
+{
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) {
+ delete *iter;
+ }
+ m_ActiveDownloads.clear();
+}
+
+
+bool DownloadManager::downloadsInProgress()
+{
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) {
+ if ((*iter)->m_State < STATE_READY) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool DownloadManager::downloadsInProgressNoPause()
+{
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) {
+ if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+void DownloadManager::pauseAll()
+{
+
+ // first loop: pause all downloads
+ for (int i = 0; i < m_ActiveDownloads.count(); ++i) {
+ if (m_ActiveDownloads[i]->m_State < STATE_READY) {
+ pauseDownload(i);
+ }
+ }
+
+ ::Sleep(100);
+
+ bool done = false;
+ QTime startTime = QTime::currentTime();
+ // further loops: busy waiting for all downloads to complete. This could be neater...
+ while (!done && (startTime.secsTo(QTime::currentTime()) < 5)) {
+ QCoreApplication::processEvents();
+ done = true;
+ foreach (DownloadInfo *info, m_ActiveDownloads) {
+ if ((info->m_State < STATE_CANCELED) ||
+ (info->m_State == STATE_FETCHINGFILEINFO) ||
+ (info->m_State == STATE_FETCHINGMODINFO) ||
+ (info->m_State == STATE_FETCHINGMODINFO_MD5)) {
+ done = false;
+ break;
+ }
+ }
+ if (!done) {
+ ::Sleep(100);
+ }
+ }
+
+}
+
+
+void DownloadManager::setOutputDirectory(const QString &outputDirectory)
+{
+ QStringList directories = m_DirWatcher.directories();
+ if (directories.length() != 0) {
+ m_DirWatcher.removePaths(directories);
+ }
+ m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory);
+ refreshList();
+ m_DirWatcher.addPath(m_OutputDirectory);
+}
+
+
+void DownloadManager::setPreferredServers(const std::map<QString, int> &preferredServers)
+{
+ m_PreferredServers = preferredServers;
+}
+
+
+void DownloadManager::setSupportedExtensions(const QStringList &extensions)
+{
+ m_SupportedExtensions = extensions;
+ refreshList();
+}
+
+void DownloadManager::setShowHidden(bool showHidden)
+{
+ m_ShowHidden = showHidden;
+ refreshList();
+}
+
+void DownloadManager::setPluginContainer(PluginContainer *pluginContainer)
+{
+ m_NexusInterface->setPluginContainer(pluginContainer);
+}
+
+
+
+
+
+
+void DownloadManager::refreshList()
+{
+ try {
+ //avoid triggering other refreshes
+ startDisableDirWatcher();
+
+ int downloadsBefore = m_ActiveDownloads.size();
+
+ // remove finished downloads
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) {
+ if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) {
+ delete *iter;
+ iter = m_ActiveDownloads.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+
+ QStringList nameFilters(m_SupportedExtensions);
+ foreach (const QString &extension, m_SupportedExtensions) {
+ nameFilters.append("*." + extension);
+ }
+
+ nameFilters.append(QString("*").append(UNFINISHED));
+ QDir dir(QDir::fromNativeSeparators(m_OutputDirectory));
+
+ // find orphaned meta files and delete them (sounds cruel but it's better for everyone)
+ QStringList orphans;
+ QStringList metaFiles = dir.entryList(QStringList() << "*.meta");
+ foreach (const QString &metaFile, metaFiles) {
+ QString baseFile = metaFile.left(metaFile.length() - 5);
+ if (!QFile::exists(dir.absoluteFilePath(baseFile))) {
+ orphans.append(dir.absoluteFilePath(metaFile));
+ }
+ }
+ if (orphans.size() > 0) {
+ qDebug("%d orphaned meta files will be deleted", orphans.size());
+ shellDelete(orphans, true);
+ }
+
+ // add existing downloads to list
+ foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) {
+ bool Exists = false;
+ for (QVector<DownloadInfo*>::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) {
+ if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) {
+ Exists = true;
+ } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) {
+ Exists = true;
+ }
+ }
+ if (Exists) {
+ continue;
+ }
+
+ QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file;
+
+ DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory);
+ if (info != nullptr) {
+ m_ActiveDownloads.push_front(info);
+ }
+ }
+
+ //if (m_ActiveDownloads.size() != downloadsBefore) {
+ qDebug("Downloads after refresh: %d", m_ActiveDownloads.size());
+ //}
+ emit update(-1);
+
+ //let watcher trigger refreshes again
+ endDisableDirWatcher();
+
+ } catch (const std::bad_alloc&) {
+ reportError(tr("Memory allocation error (in refreshing directory)."));
+ }
+}
+
+
+bool DownloadManager::addDownload(const QStringList &URLs, QString gameName,
+ int modID, int fileID, const ModRepositoryFileInfo *fileInfo)
+{
+ QString fileName = QFileInfo(URLs.first()).fileName();
+ if (fileName.isEmpty()) {
+ fileName = "unknown";
+ }
+
+ QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit());
+ qDebug("selected download url: %s", qUtf8Printable(preferredUrl.toString()));
+ QNetworkRequest request(preferredUrl);
+ request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent());
+ return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo);
+}
+
+
+bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo)
+{
+ QString fileName = getFileNameFromNetworkReply(reply);
+ if (fileName.isEmpty()) {
+ fileName = "unknown";
+ }
+
+ return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->gameName, fileInfo->modID, fileInfo->fileID, fileInfo);
+}
+
+
+bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName,
+ QString gameName, int modID, int fileID, const ModRepositoryFileInfo *fileInfo)
+{
+ // download invoked from an already open network reply (i.e. download link in the browser)
+ DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs);
+
+ QString baseName = fileName;
+ if (!fileInfo->fileName.isEmpty()) {
+ baseName = fileInfo->fileName;
+ } else {
+ QString dispoName = getFileNameFromNetworkReply(reply);
+
+ if (!dispoName.isEmpty()) {
+ baseName = dispoName;
+ }
+ }
+
+ startDisableDirWatcher();
+ newDownload->setName(getDownloadFileName(baseName), false);
+ endDisableDirWatcher();
+
+ startDownload(reply, newDownload, false);
+// emit update(-1);
+ return true;
+}
+
+
+void DownloadManager::removePending(QString gameName, int modID, int fileID)
+{
+ QString gameShortName = gameName;
+ QStringList games(m_ManagedGame->validShortNames());
+ games += m_ManagedGame->gameShortName();
+ for (auto game : games) {
+ MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game);
+ if (gamePlugin != nullptr && gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) {
+ gameShortName = gamePlugin->gameShortName();
+ break;
+ }
+ }
+ emit aboutToUpdate();
+ for (auto iter : m_PendingDownloads) {
+ if (gameShortName.compare(std::get<0>(iter), Qt::CaseInsensitive) == 0 && (std::get<1>(iter) == modID) && (std::get<2>(iter) == fileID)) {
+ m_PendingDownloads.removeAt(m_PendingDownloads.indexOf(iter));
+ break;
+ }
+ }
+ emit update(-1);
+}
+
+
+void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume)
+{
+ reply->setReadBufferSize(1024 * 1024); // don't read more than 1MB at once to avoid memory troubles
+ newDownload->m_Reply = reply;
+ setState(newDownload, STATE_DOWNLOADING);
+ if (newDownload->m_Urls.count() == 0) {
+ newDownload->m_Urls = QStringList(reply->url().toString());
+ }
+
+ QIODevice::OpenMode mode = QIODevice::WriteOnly;
+ if (resume) {
+ mode |= QIODevice::Append;
+ }
+
+ newDownload->m_StartTime.start();
+ createMetaFile(newDownload);
+
+ if (!newDownload->m_Output.open(mode)) {
+ reportError(tr("failed to download %1: could not open output file: %2")
+ .arg(reply->url().toString()).arg(newDownload->m_Output.fileName()));
+ return;
+ }
+
+ connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64)));
+ connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
+ connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged()));
+
+ if (!resume) {
+ newDownload->m_PreResumeSize = newDownload->m_Output.size();
+ removePending(newDownload->m_FileInfo->gameName, newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID);
+
+ emit aboutToUpdate();
+ m_ActiveDownloads.append(newDownload);
+
+ emit update(-1);
+ emit downloadAdded();
+
+ if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) {
+ setState(newDownload, STATE_PAUSING);
+ QCoreApplication::processEvents();
+ if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. "
+ "Do you want to download it again? The new file will receive a different name.").arg(newDownload->m_FileName),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
+ if (reply->isFinished())
+ setState(newDownload, STATE_CANCELED);
+ else
+ setState(newDownload, STATE_CANCELING);
+ } else {
+ startDisableDirWatcher();
+ newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true);
+ endDisableDirWatcher();
+ if (newDownload->m_State == STATE_PAUSED)
+ resumeDownload(indexByName(newDownload->m_FileName));
+ else
+ setState(newDownload, STATE_DOWNLOADING);
+ }
+ } else
+ connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
+
+
+ QCoreApplication::processEvents();
+
+ if (newDownload->m_State != STATE_DOWNLOADING &&
+ newDownload->m_State != STATE_READY &&
+ newDownload->m_State != STATE_FETCHINGMODINFO &&
+ reply->isFinished()) {
+ downloadFinished(indexByName(newDownload->m_FileName));
+ return;
+ }
+ } else
+ connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+
+void DownloadManager::addNXMDownload(const QString &url)
+{
+ NXMUrl nxmInfo(url);
+
+ QStringList validGames;
+ validGames.append(m_ManagedGame->gameShortName());
+ validGames.append(m_ManagedGame->validShortNames());
+ qDebug("add nxm download: %s", qUtf8Printable(url));
+ if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) {
+ 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);
+ return;
+ }
+
+ 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()) {
+ 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"));
+
+ debugStr = debugStr.arg(nxmInfo.modId()).arg(nxmInfo.fileId());
+ infoStr = infoStr.arg(nxmInfo.modId()).arg(nxmInfo.fileId());
+
+ qDebug(qUtf8Printable(debugStr));
+ QMessageBox::information(nullptr, tr("Already Queued"), infoStr, QMessageBox::Ok);
+ return;
+ }
+ }
+
+ for (DownloadInfo *download : m_ActiveDownloads) {
+ if (download->m_FileInfo->modID == nxmInfo.modId() && download->m_FileInfo->fileID == nxmInfo.fileId()) {
+ if (download->m_State == STATE_DOWNLOADING || download->m_State == STATE_PAUSED || download->m_State == STATE_STARTED) {
+ QString debugStr("download requested is already started (mod %1: %2, file %3: %4)");
+ QString infoStr(tr("There is already a download started for this file.\n\nMod %1:\t%2\nFile %3:\t%4"));
+
+ // %1
+ debugStr = debugStr.arg(download->m_FileInfo->modID);
+ infoStr = infoStr.arg(download->m_FileInfo->modID);
+
+ // %2
+ if (!download->m_FileInfo->name.isEmpty()) {
+ debugStr = debugStr.arg(download->m_FileInfo->name);
+ infoStr = infoStr.arg(download->m_FileInfo->name);
+ } else if (!download->m_FileInfo->modName.isEmpty()) {
+ debugStr = debugStr.arg(download->m_FileInfo->modName);
+ infoStr = infoStr.arg(download->m_FileInfo->modName);
+ } else {
+ debugStr = debugStr.arg(QStringLiteral("<blank>"));
+ infoStr = infoStr.arg(QStringLiteral("<blank>"));
+ }
+
+ // %3
+ debugStr = debugStr.arg(download->m_FileInfo->fileID);
+ infoStr = infoStr.arg(download->m_FileInfo->fileID);
+
+ // %4
+ if (!download->m_FileInfo->fileName.isEmpty()) {
+ debugStr = debugStr.arg(download->m_FileInfo->fileName);
+ infoStr = infoStr.arg(download->m_FileInfo->fileName);
+ } else if (!download->m_FileName.isEmpty()) {
+ debugStr = debugStr.arg(download->m_FileName);
+ infoStr = infoStr.arg(download->m_FileName);
+ } else {
+ debugStr = debugStr.arg(QStringLiteral("<blank>"));
+ infoStr = infoStr.arg(QStringLiteral("<blank>"));
+ }
+
+ qDebug(qUtf8Printable(debugStr));
+ QMessageBox::information(nullptr, tr("Already Started"), infoStr, QMessageBox::Ok);
+ return;
+ }
+ }
+ }
+
+ emit aboutToUpdate();
+
+ m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId()));
+
+ emit update(-1);
+ emit downloadAdded();
+ ModRepositoryFileInfo *info = new ModRepositoryFileInfo();
+
+ info->nexusKey = nxmInfo.key();
+ info->nexusExpires = nxmInfo.expires();
+
+ QObject *test = info;
+ m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, qVariantFromValue(test), ""));
+}
+
+
+void DownloadManager::removeFile(int index, bool deleteFile)
+{
+ //Avoid triggering refreshes from DirWatcher
+ startDisableDirWatcher();
+
+ if (index >= m_ActiveDownloads.size()) {
+ throw MyException(tr("remove: invalid download index %1").arg(index));
+ }
+
+ DownloadInfo *download = m_ActiveDownloads.at(index);
+ QString filePath = m_OutputDirectory + "/" + download->m_FileName;
+ if ((download->m_State == STATE_STARTED) ||
+ (download->m_State == STATE_DOWNLOADING)) {
+ // shouldn't have been possible
+ qCritical("tried to remove active download");
+ endDisableDirWatcher();
+ return;
+ }
+
+ if ((download->m_State == STATE_PAUSED) || (download->m_State == STATE_ERROR)) {
+ filePath = download->m_Output.fileName();
+ }
+
+ if (deleteFile) {
+ if (!shellDelete(QStringList(filePath), true)) {
+ reportError(tr("failed to delete %1").arg(filePath));
+ endDisableDirWatcher();
+ return;
+ }
+
+ QFile metaFile(filePath.append(".meta"));
+ if (metaFile.exists() && !shellDelete(QStringList(filePath), true)) {
+ reportError(tr("failed to delete meta file for %1").arg(filePath));
+ }
+ } else {
+ QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat);
+ if(!download->m_Hidden)
+ metaSettings.setValue("removed", true);
+ }
+
+ endDisableDirWatcher();
+}
+
+class LessThanWrapper
+{
+public:
+ LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {}
+ bool operator()(int LHS, int RHS) {
+ return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0;
+
+ }
+
+private:
+ DownloadManager *m_Manager;
+};
+
+
+bool DownloadManager::ByName(int LHS, int RHS)
+{
+ return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName;
+}
+
+
+void DownloadManager::refreshAlphabeticalTranslation()
+{
+ m_AlphabeticalTranslation.clear();
+ int pos = 0;
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) {
+ m_AlphabeticalTranslation.push_back(pos);
+ }
+
+ qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this));
+}
+
+
+void DownloadManager::restoreDownload(int index)
+{
+
+ if (index < 0) {
+ DownloadState minState = STATE_READY ;
+ index = 0;
+
+ for (QVector<DownloadInfo*>::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) {
+
+ if ((*iter)->m_State >= minState) {
+ restoreDownload(index);
+ }
+ index++;
+ }
+ }
+ else {
+ if (index >= m_ActiveDownloads.size()) {
+ throw MyException(tr("restore: invalid download index: %1").arg(index));
+ }
+
+ DownloadInfo *download = m_ActiveDownloads.at(index);
+ if (download->m_Hidden) {
+ download->m_Hidden = false;
+
+ QString filePath = m_OutputDirectory + "/" + download->m_FileName;
+
+ //avoid dirWatcher triggering refreshes
+ startDisableDirWatcher();
+ QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat);
+ metaSettings.setValue("removed", false);
+
+ endDisableDirWatcher();
+ }
+ }
+}
+
+
+void DownloadManager::removeDownload(int index, bool deleteFile)
+{
+ try {
+ //avoid dirWatcher triggering refreshes
+ startDisableDirWatcher();
+
+ emit aboutToUpdate();
+
+ if (index < 0) {
+ bool removeAll = (index == -1);
+ DownloadState removeState = (index == -2 ? STATE_INSTALLED : STATE_UNINSTALLED);
+
+ index = 0;
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) {
+ DownloadState downloadState = (*iter)->m_State;
+ if ((removeAll && (downloadState >= STATE_READY)) ||
+ (removeState == downloadState)) {
+ removeFile(index, deleteFile);
+ delete *iter;
+ iter = m_ActiveDownloads.erase(iter);
+ } else {
+ ++iter;
+ ++index;
+ }
+ }
+ } else {
+ if (index >= m_ActiveDownloads.size()) {
+ reportError(tr("remove: invalid download index %1").arg(index));
+ //emit update(-1);
+ endDisableDirWatcher();
+ return;
+ }
+
+ removeFile(index, deleteFile);
+ delete m_ActiveDownloads.at(index);
+ m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index);
+ }
+ emit update(-1);
+ endDisableDirWatcher();
+ } catch (const std::exception &e) {
+ qCritical("failed to remove download: %s", e.what());
+ }
+ refreshList();
+}
+
+
+void DownloadManager::cancelDownload(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("cancel: invalid download index %1").arg(index));
+ return;
+ }
+
+ if (m_ActiveDownloads.at(index)->m_State == STATE_DOWNLOADING) {
+ setState(m_ActiveDownloads.at(index), STATE_CANCELING);
+ }
+}
+
+
+void DownloadManager::pauseDownload(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("pause: invalid download index %1").arg(index));
+ return;
+ }
+
+ DownloadInfo *info = m_ActiveDownloads.at(index);
+
+ if (info->m_State == STATE_DOWNLOADING) {
+ if ((info->m_Reply != nullptr) && (info->m_Reply->isRunning())) {
+ setState(info, STATE_PAUSING);
+ } else {
+ setState(info, STATE_PAUSED);
+ }
+ } else if ((info->m_State == STATE_FETCHINGMODINFO) ||
+ (info->m_State == STATE_FETCHINGFILEINFO) ||
+ (info->m_State == STATE_FETCHINGMODINFO_MD5)) {
+ setState(info, STATE_READY);
+ }
+}
+
+void DownloadManager::resumeDownload(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("resume: invalid download index %1").arg(index));
+ return;
+ }
+ DownloadInfo *info = m_ActiveDownloads[index];
+ info->m_Tries = AUTOMATIC_RETRIES;
+ resumeDownloadInt(index);
+}
+
+void DownloadManager::resumeDownloadInt(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("resume (int): invalid download index %1").arg(index));
+ return;
+ }
+ DownloadInfo *info = m_ActiveDownloads[index];
+
+ // Check for finished download;
+ if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr
+ && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) {
+ setState(info, STATE_DOWNLOADING);
+ downloadFinished(index);
+ return;
+ }
+
+ if (info->isPausedState() || info->m_State == STATE_PAUSING) {
+ if (info->m_State == STATE_PAUSING) {
+ if (info->m_Output.isOpen()) {
+ writeData(info);
+ if (info->m_State == STATE_PAUSING) {
+ setState(info, STATE_PAUSED);
+ }
+ }
+ }
+ if ((info->m_Urls.size() == 0)
+ || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) {
+ emit showMessage(tr("No known download urls. Sorry, this download can't be resumed."));
+ return;
+ }
+ if (info->m_State == STATE_ERROR) {
+ info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count();
+ }
+ qDebug("request resume from url %s", qUtf8Printable(info->currentURL()));
+ QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit()));
+ request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent());
+ if (info->m_State != STATE_ERROR) {
+ info->m_ResumePos = info->m_Output.size();
+ QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-";
+ request.setRawHeader("Range", rangeHeader);
+ }
+ std::get<0>(info->m_SpeedDiff) = 0;
+ std::get<1>(info->m_SpeedDiff) = 0;
+ std::get<2>(info->m_SpeedDiff) = 0;
+ std::get<3>(info->m_SpeedDiff) = 0;
+ std::get<4>(info->m_SpeedDiff) = 0;
+ qDebug("resume at %lld bytes", info->m_ResumePos);
+ startDownload(m_NexusInterface->getAccessManager()->get(request), info, true);
+ }
+ emit update(index);
+}
+
+
+DownloadManager::DownloadInfo *DownloadManager::downloadInfoByID(unsigned int id)
+{
+ auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(),
+ [id](DownloadInfo *info) { return info->m_DownloadID == id; });
+ if (iter != m_ActiveDownloads.end()) {
+ return *iter;
+ } else {
+ return nullptr;
+ }
+}
+
+
+void DownloadManager::queryInfo(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("query: invalid download index %1").arg(index));
+ return;
+ }
+ DownloadInfo *info = m_ActiveDownloads[index];
+
+ if (info->m_FileInfo->repository != "Nexus") {
+ qWarning("re-querying file info is currently only possible with Nexus");
+ return;
+ }
+
+ if (info->m_State < DownloadManager::STATE_READY) {
+ // UI shouldn't allow this
+ return;
+ }
+
+ if (info->m_FileInfo->modID <= 0) {
+ QString fileName = getFileName(index);
+ QString ignore;
+ NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true);
+ if (info->m_FileInfo->modID < 0) {
+ bool ok = false;
+ int modId = QInputDialog::getInt(
+ nullptr, tr("Please enter the nexus mod id"), tr("Mod ID:"), 1, 1,
+ std::numeric_limits<int>::max(), 1, &ok);
+ // careful now: while the dialog was displayed, events were processed.
+ // the download list might have changed and our info-ptr invalidated.
+ if (ok)
+ m_ActiveDownloads[index]->m_FileInfo->modID = modId;
+ return;
+ }
+ }
+
+ if (info->m_FileInfo->gameName.size() == 0) {
+ SelectionDialog selection(tr("Please select the source game code for %1").arg(getFileName(index)));
+
+ std::vector<std::pair<QString, QString>> choices = m_NexusInterface->getGameChoices(m_ManagedGame);
+ for (auto choice : choices) {
+ selection.addChoice(choice.first, choice.second, choice.first);
+ }
+ if (selection.exec() == QDialog::Accepted) {
+ info->m_FileInfo->gameName = selection.getChoiceData().toString();
+ } else {
+ info->m_FileInfo->gameName = m_ManagedGame->gameShortName();
+ }
+ }
+ info->m_ReQueried = true;
+ setState(info, STATE_FETCHINGMODINFO);
+}
+
+void DownloadManager::queryInfoMd5(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("query: invalid download index %1").arg(index));
+ return;
+ }
+ DownloadInfo *info = m_ActiveDownloads[index];
+
+ if (info->m_FileInfo->repository != "Nexus") {
+ qWarning("re-querying file info is currently only possible with Nexus");
+ return;
+ }
+
+ if (info->m_State < DownloadManager::STATE_READY) {
+ // UI shouldn't allow this
+ return;
+ }
+
+ info->m_GamesToQuery << m_ManagedGame->gameShortName();
+ info->m_GamesToQuery << m_ManagedGame->validShortNames();
+
+ QFile downloadFile(info->m_FileName);
+ if (!downloadFile.exists()) {
+ downloadFile.setFileName(m_OrganizerCore->downloadsPath() + "\\" + info->m_FileName);
+ }
+ if (!downloadFile.exists()) {
+ qDebug("Can't find download file %s", info->m_FileName);
+ return;
+ }
+ if (!downloadFile.open(QIODevice::ReadOnly)) {
+ qDebug("Can't open download file %s", info->m_FileName);
+ return;
+ }
+ info->m_Hash = QCryptographicHash::hash(downloadFile.readAll(), QCryptographicHash::Md5);
+ downloadFile.close();
+
+ info->m_ReQueried = true;
+ setState(info, STATE_FETCHINGMODINFO_MD5);
+}
+
+void DownloadManager::visitOnNexus(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("VisitNexus: invalid download index %1").arg(index));
+ return;
+ }
+ DownloadInfo *info = m_ActiveDownloads[index];
+
+ if (info->m_FileInfo->repository != "Nexus") {
+ qWarning("Visiting mod page is currently only possible with Nexus");
+ return;
+ }
+
+ if (info->m_State < DownloadManager::STATE_READY) {
+ // UI shouldn't allow this
+ return;
+ }
+ int modID = info->m_FileInfo->modID;
+
+ QString gameName = info->m_FileInfo->gameName;
+ if (modID > 0) {
+ QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName)));
+ }
+ else {
+ emit showMessage(tr("Nexus ID for this Mod is unknown"));
+ }
+}
+
+void DownloadManager::openFile(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ 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);
+ return;
+ }
+
+ ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ return;
+}
+
+void DownloadManager::openInDownloadsFolder(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ 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") + "\"";
+
+ ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL);
+ return;
+ }
+
+ ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ return;
+}
+
+
+int DownloadManager::numTotalDownloads() const
+{
+ return m_ActiveDownloads.size();
+}
+
+int DownloadManager::numPendingDownloads() const
+{
+ return m_PendingDownloads.size();
+}
+
+std::tuple<QString, int, int> DownloadManager::getPendingDownload(int index)
+{
+ if ((index < 0) || (index >= m_PendingDownloads.size())) {
+ throw MyException(tr("get pending: invalid download index %1").arg(index));
+ }
+
+ return m_PendingDownloads.at(index);
+}
+
+QString DownloadManager::getFilePath(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("get path: invalid download index %1").arg(index));
+ }
+
+ return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName;
+}
+
+QString DownloadManager::getFileTypeString(int fileType)
+{
+ switch (fileType) {
+ 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");
+ }
+}
+
+QString DownloadManager::getDisplayName(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("display name: invalid download index %1").arg(index));
+ }
+
+ DownloadInfo *info = m_ActiveDownloads.at(index);
+
+ QTextDocument doc;
+ if (!info->m_FileInfo->name.isEmpty()) {
+ doc.setHtml(info->m_FileInfo->name);
+ return QString("%1 (%2, v%3)").arg(doc.toPlainText())
+ .arg(getFileTypeString(info->m_FileInfo->fileCategory))
+ .arg(info->m_FileInfo->version.displayString());
+ } else {
+ doc.setHtml(info->m_FileName);
+ return doc.toPlainText();
+ }
+}
+
+QString DownloadManager::getFileName(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("file name: invalid download index %1").arg(index));
+ }
+
+ return m_ActiveDownloads.at(index)->m_FileName;
+}
+
+QDateTime DownloadManager::getFileTime(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("file time: invalid download index %1").arg(index));
+ }
+
+ DownloadInfo *info = m_ActiveDownloads.at(index);
+ if (!info->m_Created.isValid()) {
+ info->m_Created = QFileInfo(info->m_Output).created();
+ }
+
+ return info->m_Created;
+}
+
+qint64 DownloadManager::getFileSize(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("file size: invalid download index %1").arg(index));
+ }
+
+ return m_ActiveDownloads.at(index)->m_TotalSize;
+}
+
+
+std::pair<int, QString> DownloadManager::getProgress(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("progress: invalid download index %1").arg(index));
+ }
+
+ return m_ActiveDownloads.at(index)->m_Progress;
+}
+
+
+DownloadManager::DownloadState DownloadManager::getState(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("state: invalid download index %1").arg(index));
+ }
+
+ return m_ActiveDownloads.at(index)->m_State;
+}
+
+
+bool DownloadManager::isInfoIncomplete(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("infocomplete: invalid download index %1").arg(index));
+ }
+
+ DownloadInfo *info = m_ActiveDownloads.at(index);
+ if (info->m_FileInfo->repository != "Nexus") {
+ // other repositories currently don't support re-querying info anyway
+ return false;
+ }
+ return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0);
+}
+
+
+int DownloadManager::getModID(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("mod id: invalid download index %1").arg(index));
+ }
+ return m_ActiveDownloads.at(index)->m_FileInfo->modID;
+}
+
+QString DownloadManager::getGameName(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("mod id: invalid download index %1").arg(index));
+ }
+ return m_ActiveDownloads.at(index)->m_FileInfo->gameName;
+}
+
+bool DownloadManager::isHidden(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("ishidden: invalid download index %1").arg(index));
+ }
+ return m_ActiveDownloads.at(index)->m_Hidden;
+}
+
+
+const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("file info: invalid download index %1").arg(index));
+ }
+
+ return m_ActiveDownloads.at(index)->m_FileInfo;
+}
+
+
+void DownloadManager::markInstalled(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("mark installed: invalid download index %1").arg(index));
+ }
+
+ //Avoid triggering refreshes from DirWatcher
+ startDisableDirWatcher();
+
+ DownloadInfo *info = m_ActiveDownloads.at(index);
+ QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
+ metaFile.setValue("installed", true);
+ metaFile.setValue("uninstalled", false);
+
+ endDisableDirWatcher();
+
+ setState(m_ActiveDownloads.at(index), STATE_INSTALLED);
+}
+
+void DownloadManager::markInstalled(QString fileName)
+{
+ int index = indexByName(fileName);
+ if (index >= 0) {
+ markInstalled(index);
+ } else {
+ DownloadInfo *info = getDownloadInfo(fileName);
+ if (info != nullptr) {
+ //Avoid triggering refreshes from DirWatcher
+ startDisableDirWatcher();
+
+ QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
+ metaFile.setValue("installed", true);
+ metaFile.setValue("uninstalled", false);
+ delete info;
+
+ endDisableDirWatcher();
+ }
+ }
+}
+
+DownloadManager::DownloadInfo* DownloadManager::getDownloadInfo(QString fileName)
+{
+ return DownloadInfo::createFromMeta(fileName, true, m_OutputDirectory);
+}
+
+void DownloadManager::markUninstalled(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("mark uninstalled: invalid download index %1").arg(index));
+ }
+
+ //Avoid triggering refreshes from DirWatcher
+ startDisableDirWatcher();
+
+ DownloadInfo *info = m_ActiveDownloads.at(index);
+ QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
+ metaFile.setValue("uninstalled", true);
+
+ endDisableDirWatcher();
+
+ setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED);
+}
+
+
+void DownloadManager::markUninstalled(QString fileName)
+{
+ int index = indexByName(fileName);
+ if (index >= 0) {
+ markUninstalled(index);
+ } else {
+ QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName;
+ DownloadInfo *info = getDownloadInfo(filePath);
+ if (info != nullptr) {
+
+ //Avoid triggering refreshes from DirWatcher
+ startDisableDirWatcher();
+
+ QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat);
+ metaFile.setValue("uninstalled", true);
+ delete info;
+
+ endDisableDirWatcher();
+ }
+ }
+}
+
+
+QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const
+{
+ QString fullPath = m_OutputDirectory + "/" + baseName;
+ if (QFile::exists(fullPath) && rename) {
+ int i = 1;
+ while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) {
+ ++i;
+ }
+
+ fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName);
+ }
+ return fullPath;
+}
+
+
+QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply)
+{
+ if (reply->hasRawHeader("Content-Disposition")) {
+ std::regex exp("filename=\"(.*)\"");
+
+ std::cmatch result;
+ if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) {
+ return QString::fromUtf8(result.str(1).c_str());
+ }
+ }
+
+ return QString();
+}
+
+
+void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadManager::DownloadState state)
+{
+ int row = 0;
+ for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
+ if (m_ActiveDownloads[i] == info) {
+ row = i;
+ break;
+ }
+ }
+ info->m_State = state;
+ switch (state) {
+ case STATE_PAUSED:
+ case STATE_ERROR: {
+ info->m_Reply->abort();
+ info->m_Output.close();
+ } break;
+ case STATE_CANCELED: {
+ info->m_Reply->abort();
+ } break;
+ case STATE_FETCHINGMODINFO: {
+ m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString()));
+ } break;
+ case STATE_FETCHINGFILEINFO: {
+ m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString()));
+ } break;
+ case STATE_FETCHINGMODINFO_MD5: {
+ qDebug(qUtf8Printable(QString("Searching %1 for MD5 of %2").arg(info->m_GamesToQuery[0]).arg(QString(info->m_Hash.toHex()))));
+ m_RequestIDs.insert(m_NexusInterface->requestInfoFromMd5(info->m_GamesToQuery[0], info->m_Hash, this, info->m_DownloadID, QString()));
+ } break;
+ case STATE_READY: {
+ createMetaFile(info);
+ emit downloadComplete(row);
+ } break;
+ default: /* NOP */ break;
+ }
+ emit stateChanged(row, state);
+}
+
+
+DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const
+{
+ // reverse search as newer, thus more relevant, downloads are at the end
+ for (int i = m_ActiveDownloads.size() - 1; i >= 0; --i) {
+ if (m_ActiveDownloads[i]->m_Reply == reply) {
+ if (index != nullptr) {
+ *index = i;
+ }
+ return m_ActiveDownloads[i];
+ }
+ }
+ return nullptr;
+}
+
+
+void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ if (bytesTotal == 0) {
+ return;
+ }
+ int index = 0;
+ try {
+ DownloadInfo *info = findDownload(this->sender(), &index);
+ if (info != nullptr) {
+ info->m_HasData = true;
+ if (info->m_State == STATE_CANCELING) {
+ setState(info, STATE_CANCELED);
+ } else if (info->m_State == STATE_PAUSING) {
+ setState(info, STATE_PAUSED);
+ }
+ else {
+ if (bytesTotal > info->m_TotalSize) {
+ info->m_TotalSize = bytesTotal;
+ }
+ int oldProgress = info->m_Progress.first;
+ info->m_Progress.first = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal);
+
+ int elapsed = info->m_StartTime.elapsed();
+ std::get<0>(info->m_SpeedDiff) = bytesReceived - std::get<2>(info->m_SpeedDiff);
+ std::get<1>(info->m_SpeedDiff) = elapsed - std::get<3>(info->m_SpeedDiff);
+ std::get<2>(info->m_SpeedDiff) = bytesReceived;
+ std::get<3>(info->m_SpeedDiff) = elapsed;
+
+ double calc = ((double)std::get<0>(info->m_SpeedDiff)) / (((double)(std::get<1>(info->m_SpeedDiff)) / 5000.0));
+ std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2;
+
+ // calculate the download speed
+ double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000);
+
+ QString unit;
+ if (speed < 1000) {
+ unit = "B/s";
+ }
+ else if (speed < 1000*1024) {
+ speed /= 1024;
+ unit = "KB/s";
+ }
+ else {
+ speed /= 1024 * 1024;
+ unit = "MB/s";
+ }
+
+ info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(QString::number(speed, 'f', 1)).arg(unit);
+
+ TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal);
+ emit update(index);
+ }
+ }
+ } catch (const std::bad_alloc&) {
+ reportError(tr("Memory allocation error (in processing progress event)."));
+ }
+}
+
+
+void DownloadManager::downloadReadyRead()
+{
+ try {
+ writeData(findDownload(this->sender()));
+ } catch (const std::bad_alloc&) {
+ reportError(tr("Memory allocation error (in processing downloaded data)."));
+ }
+}
+
+
+void DownloadManager::createMetaFile(DownloadInfo *info)
+{
+ //Avoid triggering refreshes from DirWatcher
+ startDisableDirWatcher();
+
+ QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat);
+ metaFile.setValue("gameName", info->m_FileInfo->gameName);
+ metaFile.setValue("modID", info->m_FileInfo->modID);
+ metaFile.setValue("fileID", info->m_FileInfo->fileID);
+ metaFile.setValue("url", info->m_Urls.join(";"));
+ metaFile.setValue("name", info->m_FileInfo->name);
+ metaFile.setValue("description", info->m_FileInfo->description);
+ metaFile.setValue("modName", info->m_FileInfo->modName);
+ metaFile.setValue("version", info->m_FileInfo->version.canonicalString());
+ metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString());
+ metaFile.setValue("fileTime", info->m_FileInfo->fileTime);
+ metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory);
+ metaFile.setValue("category", info->m_FileInfo->categoryID);
+ metaFile.setValue("repository", info->m_FileInfo->repository);
+ metaFile.setValue("userData", info->m_FileInfo->userData);
+ metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED);
+ metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED);
+ metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) ||
+ (info->m_State == DownloadManager::STATE_ERROR));
+ metaFile.setValue("removed", info->m_Hidden);
+
+ endDisableDirWatcher();
+ // slightly hackish...
+ for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
+ if (m_ActiveDownloads[i] == info) {
+ emit update(i);
+ }
+ }
+}
+
+
+void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
+ if (idIter == m_RequestIDs.end()) {
+ return;
+ } else {
+ m_RequestIDs.erase(idIter);
+ }
+
+ QVariantMap result = resultData.toMap();
+
+ DownloadInfo *info = downloadInfoByID(userData.toInt());
+ if (info == nullptr) return;
+ info->m_FileInfo->categoryID = result["category_id"].toInt();
+ QTextDocument doc;
+ doc.setHtml(result["name"].toString().trimmed());
+ info->m_FileInfo->modName = doc.toPlainText();
+ if (info->m_FileInfo->fileID != 0) {
+ setState(info, STATE_READY);
+ } else {
+ setState(info, STATE_FETCHINGFILEINFO);
+ }
+}
+
+void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
+ if (idIter == m_RequestIDs.end()) {
+ return;
+ } else {
+ m_RequestIDs.erase(idIter);
+ }
+
+ DownloadInfo *info = downloadInfoByID(userData.toInt());
+ if (info == nullptr) return;
+
+ QVariantMap result = resultData.toMap();
+ QVariantList files = result["files"].toList();
+
+
+ // MO sometimes prepends <digit>_ to the filename in case of duplicate downloads.
+ // this may muck up the file name comparison
+ QString alternativeLocalName = info->m_FileName;
+
+ QRegExp expression("^\\d_(.*)$");
+ if (expression.indexIn(alternativeLocalName) == 0) {
+ alternativeLocalName = expression.cap(1);
+ }
+
+ bool found = false;
+
+ for (QVariant file : files) {
+ QVariantMap fileInfo = file.toMap();
+ QString fileName = fileInfo["file_name"].toString();
+ QString fileNameVariant = fileName.mid(0).replace(' ', '_');
+ if ((fileName == info->m_RemoteFileName) || (fileNameVariant == info->m_RemoteFileName) ||
+ (fileName == info->m_FileName) || (fileNameVariant == info->m_FileName) ||
+ (fileName == alternativeLocalName) || (fileNameVariant == alternativeLocalName)) {
+ info->m_FileInfo->name = fileInfo["name"].toString();
+ info->m_FileInfo->version.parse(fileInfo["version"].toString());
+ if (!info->m_FileInfo->version.isValid()) {
+ info->m_FileInfo->version = info->m_FileInfo->newestVersion;
+ }
+ info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt();
+ info->m_FileInfo->fileTime = QDateTime::fromMSecsSinceEpoch(fileInfo["uploaded_timestamp"].toLongLong());
+ info->m_FileInfo->fileID = fileInfo["file_id"].toInt();
+ info->m_FileInfo->fileName = fileInfo["file_name"].toString();
+ info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["description"].toString());
+ found = true;
+ break;
+ }
+ }
+
+ if (info->m_ReQueried) {
+ if (found) {
+ emit showMessage(tr("Information updated"));
+ } else if (result.count() == 0) {
+ emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?"));
+ } else {
+ SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one."));
+ for (QVariant file : files) {
+ QVariantMap fileInfo = file.toMap();
+ if (fileInfo["category_id"].toInt() != 6)
+ selection.addChoice(fileInfo["file_name"].toString(), "", file);
+ }
+ if (selection.exec() == QDialog::Accepted) {
+ QVariantMap fileInfo = selection.getChoiceData().toMap();
+ info->m_FileInfo->name = fileInfo["name"].toString();
+ info->m_FileInfo->version.parse(fileInfo["version"].toString());
+ info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt();
+ info->m_FileInfo->fileID = fileInfo["file_id"].toInt();
+ } else {
+ emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?"));
+ }
+ }
+ } else {
+ if (info->m_FileInfo->fileID == 0) {
+ qWarning("could not determine file id for %s (state %d)",
+ qUtf8Printable(info->m_FileName), info->m_State);
+ }
+ }
+
+ setState(info, STATE_READY);
+}
+
+
+void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
+ if (idIter == m_RequestIDs.end()) {
+ return;
+ } else {
+ m_RequestIDs.erase(idIter);
+ }
+
+ ModRepositoryFileInfo *info = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(userData));
+
+ QVariantMap result = resultData.toMap();
+ info->name = result["name"].toString();
+ info->version.parse(result["version"].toString());
+ if (!info->version.isValid()) {
+ info->version = info->newestVersion;
+ }
+ info->fileName = result["file_name"].toString();
+ info->fileCategory = result["category_id"].toInt();
+ info->fileTime = QDateTime::fromMSecsSinceEpoch(result["uploaded_timestamp"].toLongLong());
+ info->description = BBCode::convertToHTML(result["description"].toString());
+
+ info->repository = "Nexus";
+
+ QStringList games(m_ManagedGame->validShortNames());
+ games += m_ManagedGame->gameShortName();
+ for (auto game : games) {
+ MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game);
+ if (gamePlugin != nullptr && gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) {
+ info->gameName = gamePlugin->gameShortName();
+ }
+ }
+
+ info->modID = modID;
+ info->fileID = fileID;
+
+ QObject *test = info;
+ m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(info->gameName, info->modID, info->fileID, this, qVariantFromValue(test), QString()));
+}
+
+static int evaluateFileInfoMap(const QVariantMap &map, const std::map<QString, int> &preferredServers)
+{
+ int result = 0;
+
+ auto preference = preferredServers.find(map["short_name"].toString());
+
+ if (preference != preferredServers.end()) {
+ result += 100 + preference->second * 20;
+ }
+
+ return result;
+}
+
+// sort function to sort by best download server
+bool DownloadManager::ServerByPreference(const std::map<QString, int> &preferredServers, const QVariant &LHS, const QVariant &RHS)
+{
+ return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers);
+}
+
+int DownloadManager::startDownloadURLs(const QStringList &urls)
+{
+ ModRepositoryFileInfo info;
+ addDownload(urls, "", -1, -1, &info);
+ return m_ActiveDownloads.size() - 1;
+}
+
+int DownloadManager::startDownloadNexusFile(int modID, int fileID)
+{
+ int newID = m_ActiveDownloads.size();
+ addNXMDownload(QString("nxm://%1/mods/%2/files/%3").arg(m_ManagedGame->gameShortName()).arg(modID).arg(fileID));
+ return newID;
+}
+
+QString DownloadManager::downloadPath(int id)
+{
+ return getFilePath(id);
+}
+
+int DownloadManager::indexByName(const QString &fileName) const
+{
+ for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
+ if (m_ActiveDownloads[i]->m_FileName == fileName) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
+ if (idIter == m_RequestIDs.end()) {
+ return;
+ } else {
+ m_RequestIDs.erase(idIter);
+ }
+
+ ModRepositoryFileInfo *info = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(userData));
+ QVariantList resultList = resultData.toList();
+ if (resultList.length() == 0) {
+ removePending(gameName, modID, fileID);
+ emit showMessage(tr("No download server available. Please try again later."));
+ return;
+ }
+
+ std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2));
+
+ info->userData["downloadMap"] = resultList;
+
+ QStringList URLs;
+
+ foreach (const QVariant &server, resultList) {
+ URLs.append(server.toMap()["URI"].toString());
+ }
+ addDownload(URLs, gameName, modID, fileID, info);
+}
+
+
+void DownloadManager::nxmFileInfoFromMd5Available(QString gameName, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
+ if (idIter == m_RequestIDs.end()) {
+ return;
+ } else {
+ m_RequestIDs.erase(idIter);
+ }
+
+ DownloadInfo *info = downloadInfoByID(userData.toInt());
+
+ //This can come back with multiple results with the same file was uploaded multiple times (for whatever reason)
+ auto resultlist = resultData.toList();
+ int chosenIdx = resultlist.count() > 1 ? -1 : 0;
+
+ //Look for the exact file name
+ if (chosenIdx < 0) {
+ for (int i = 0; i < resultlist.count(); i++) {
+ auto results = resultlist[i].toMap();
+ auto fileDetails = results["file_details"].toMap();
+
+ if (fileDetails["file_name"].toString().compare(info->m_FileName, Qt::CaseInsensitive) == 0) {
+ chosenIdx = i;
+ break;
+ }
+ }
+ }
+
+ //Look for the only active one
+ if (chosenIdx < 0) {
+ for (int i = 0; i < resultlist.count(); i++) {
+ auto results = resultlist[i].toMap();
+ auto fileDetails = results["file_details"].toMap();
+
+ if (fileDetails["category_id"].toInt() != 6) { //6 = deleted
+ if (chosenIdx < 0) {
+ chosenIdx = i; //intentional to not break in order to check other results
+ } else {
+ qDebug("Multiple active files found during MD5 search. Defaulting to time stamps...");
+ chosenIdx = -1;
+ break;
+ }
+ }
+ }
+ }
+
+ //Look for the most recent one
+ if (chosenIdx < 0) {
+ int mostRecentTime = INT_MIN;
+ for (int i = 0; i < resultlist.count(); i++) {
+ auto results = resultlist[i].toMap();
+ auto fileDetails = results["file_details"].toMap();
+
+ int fileTime = fileDetails["uploaded_timestamp"].toInt();
+ if (fileTime > mostRecentTime) {
+ chosenIdx = i;
+ mostRecentTime = fileTime;
+ }
+ }
+ }
+
+ //If nothing is preferred, just use the first one
+ if (chosenIdx < 0) chosenIdx = 0;
+
+ auto results = resultlist[chosenIdx].toMap();
+ auto fileDetails = results["file_details"].toMap();
+ auto modDetails = results["mod"].toMap();
+
+ info->m_FileInfo->name = fileDetails["name"].toString();
+ info->m_FileInfo->fileID = fileDetails["file_id"].toInt();
+ info->m_FileInfo->description = BBCode::convertToHTML(fileDetails["description"].toString());
+ info->m_FileInfo->version.parse(fileDetails["version"].toString());
+ if (!info->m_FileInfo->version.isValid())
+ info->m_FileInfo->version.parse(fileDetails["mod_version"].toString());
+ info->m_FileInfo->fileCategory = fileDetails["category_id"].toInt();
+
+ info->m_FileInfo->modID = modDetails["mod_id"].toInt();
+ info->m_FileInfo->modName = modDetails["name"].toString();
+ info->m_FileInfo->categoryID = modDetails["category_id"].toInt();
+
+ QString gameShortName = gameName;
+ QStringList games(m_ManagedGame->validShortNames());
+ games += m_ManagedGame->gameShortName();
+ for (auto game : games) {
+ MOBase::IPluginGame *gamePlugin = m_OrganizerCore->getGame(game);
+ if (gamePlugin != nullptr && gamePlugin->gameNexusName().compare(gameName, Qt::CaseInsensitive) == 0) {
+ gameShortName = gamePlugin->gameShortName();
+ break;
+ }
+ }
+
+ info->m_FileInfo->gameName = gameShortName;
+
+ //If the file description is not present, send another query to get it
+ if (!fileDetails["description"].isValid()) {
+ info->m_RemoteFileName = fileDetails["file_name"].toString();
+ setState(info, STATE_FETCHINGFILEINFO);
+ } else {
+ setState(info, STATE_READY);
+ }
+}
+
+
+void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString)
+{
+ std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
+ if (idIter == m_RequestIDs.end()) {
+ return;
+ } else {
+ m_RequestIDs.erase(idIter);
+ }
+
+ DownloadInfo *userDataInfo = downloadInfoByID(userData.toInt());
+
+ int index = 0;
+
+ for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) {
+ DownloadInfo *info = *iter;
+ if (info != userDataInfo)
+ continue;
+
+ // MD5 searches continue until all possible games are done
+ if (info->m_State == STATE_FETCHINGMODINFO_MD5) {
+ if (info->m_GamesToQuery.count() >= 2) {
+ info->m_GamesToQuery.pop_front();
+ setState(info, STATE_FETCHINGMODINFO_MD5);
+ return;
+ } else {
+ info->m_State = STATE_READY;
+ queryInfo(index);
+ emit update(index);
+ return;
+ }
+ }
+
+ if (info->m_FileInfo->modID == modID) {
+ if (info->m_State < STATE_FETCHINGMODINFO) {
+ m_ActiveDownloads.erase(iter);
+ delete info;
+ } else {
+ setState(info, STATE_READY);
+ }
+ emit update(index);
+ break;
+ }
+ }
+
+ removePending(gameName, modID, fileID);
+ emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString));
+}
+
+
+void DownloadManager::downloadFinished(int index)
+{
+ DownloadInfo *info;
+ if (index)
+ info = m_ActiveDownloads[index];
+ else
+ info = findDownload(this->sender(), &index);
+
+ if (info != nullptr) {
+ QNetworkReply *reply = info->m_Reply;
+ QByteArray data;
+ if (reply->isOpen() && info->m_HasData) {
+ data = reply->readAll();
+ info->m_Output.write(data);
+ }
+ info->m_Output.close();
+ TaskProgressManager::instance().forgetMe(info->m_TaskProgressId);
+
+ bool error = false;
+ if ((info->m_State != STATE_CANCELING) &&
+ (info->m_State != STATE_PAUSING)) {
+ bool textData = reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive);
+ if (textData)
+ emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString()));
+ if ((info->m_Output.size() == 0) ||
+ ((reply->error() != QNetworkReply::NoError)
+ && (reply->error() != QNetworkReply::OperationCanceledError))) {
+ if (reply->error() == QNetworkReply::UnknownContentError)
+ emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size()));
+ if (info->m_Tries == 0) {
+ emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ }
+ error = true;
+ setState(info, STATE_ERROR);
+ }
+ }
+
+ if (info->m_State == STATE_CANCELING) {
+ setState(info, STATE_CANCELED);
+ } else if (info->m_State == STATE_PAUSING) {
+ if (info->m_Output.isOpen() && info->m_HasData) {
+ info->m_Output.write(info->m_Reply->readAll());
+ }
+ setState(info, STATE_PAUSED);
+ }
+
+ if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) {
+ emit aboutToUpdate();
+ info->m_Output.remove();
+ delete info;
+ m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index);
+ if (error)
+ emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers."));
+ emit update(-1);
+ } else if (info->isPausedState() || info->m_State == STATE_PAUSING) {
+ info->m_Output.close();
+ createMetaFile(info);
+ emit update(index);
+ } else {
+ QString url = info->m_Urls[info->m_CurrentUrl];
+ if (info->m_FileInfo->userData.contains("downloadMap")) {
+ foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) {
+ QVariantMap serverMap = server.toMap();
+ if (serverMap["URI"].toString() == url) {
+ int deltaTime = info->m_StartTime.secsTo(QTime::currentTime());
+ if (deltaTime > 5) {
+ emit downloadSpeed(serverMap["short_name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime);
+ } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise
+ break;
+ }
+ }
+ }
+
+ bool isNexus = info->m_FileInfo->repository == "Nexus";
+ // need to change state before changing the file name, otherwise .unfinished is appended
+ if (isNexus) {
+ setState(info, STATE_FETCHINGMODINFO);
+ } else {
+ setState(info, STATE_NOFETCH);
+ }
+
+ QString newName = getFileNameFromNetworkReply(reply);
+ QString oldName = QFileInfo(info->m_Output).fileName();
+
+ startDisableDirWatcher();
+ if (!newName.isEmpty() && (oldName.isEmpty())) {
+ info->setName(getDownloadFileName(newName), true);
+ } else {
+ info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension
+ }
+ endDisableDirWatcher();
+
+ if (!isNexus) {
+ setState(info, STATE_READY);
+ }
+
+ emit update(index);
+ }
+ reply->close();
+ reply->deleteLater();
+
+ if ((info->m_Tries > 0) && error) {
+ --info->m_Tries;
+ resumeDownloadInt(index);
+ }
+ } else {
+ qWarning("no download index %d", index);
+ }
+}
+
+
+void DownloadManager::downloadError(QNetworkReply::NetworkError error)
+{
+ if (error != QNetworkReply::OperationCanceledError) {
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ qWarning("%s (%d)", reply != nullptr ? qUtf8Printable(reply->errorString())
+ : "Download error occured",
+ error);
+ }
+}
+
+
+void DownloadManager::metaDataChanged()
+{
+ int index = 0;
+
+ DownloadInfo *info = findDownload(this->sender(), &index);
+ if (info != nullptr) {
+ QString newName = getFileNameFromNetworkReply(info->m_Reply);
+ if (!newName.isEmpty() && (info->m_FileName.isEmpty())) {
+ startDisableDirWatcher();
+ info->setName(getDownloadFileName(newName), true);
+ endDisableDirWatcher();
+ refreshAlphabeticalTranslation();
+ if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) {
+ reportError(tr("failed to re-open %1").arg(info->m_FileName));
+ setState(info, STATE_CANCELING);
+ }
+ }
+ } else {
+ qWarning("meta data event for unknown download");
+ }
+}
+
+void DownloadManager::directoryChanged(const QString&)
+{
+ if(DownloadManager::m_DirWatcherDisabler==0)
+ refreshList();
+}
+
+void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame)
+{
+ m_ManagedGame = managedGame;
+}
+
+void DownloadManager::checkDownloadTimeout()
+{
+ for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
+ if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 &&
+ m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr &&
+ m_ActiveDownloads[i]->m_Reply->isOpen()) {
+ pauseDownload(i);
+ downloadFinished(i);
+ resumeDownload(i);
+ }
+ }
+}
+
+void DownloadManager::writeData(DownloadInfo *info)
+{
+ if (info != nullptr) {
+ qint64 ret = info->m_Output.write(info->m_Reply->readAll());
+ if (ret < info->m_Reply->size()) {
+ QString fileName = info->m_FileName; // m_FileName may be destroyed after setState
+ setState(info, DownloadState::STATE_CANCELED);
+ qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit());
+ reportError(tr("Unable to write download to drive (return %1).\n"
+ "Check the drive's available storage.\n\n"
+ "Canceling download \"%2\"...").arg(ret).arg(fileName));
+ }
+ }
+}
diff --git a/src/downloadmanager.h b/src/downloadmanager.h
index 4cbe31b7..feef0eaa 100644
--- a/src/downloadmanager.h
+++ b/src/downloadmanager.h
@@ -1,568 +1,575 @@
-/*
-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/>.
-*/
-
-#ifndef DOWNLOADMANAGER_H
-#define DOWNLOADMANAGER_H
-
-#include <idownloadmanager.h>
-#include <modrepositoryfileinfo.h>
-#include <set>
-#include <QObject>
-#include <QUrl>
-#include <QQueue>
-#include <QFile>
-#include <QNetworkReply>
-#include <QTime>
-#include <QTimer>
-#include <QVector>
-#include <QMap>
-#include <QStringList>
-#include <QFileSystemWatcher>
-#include <QSettings>
-
-namespace MOBase { class IPluginGame; }
-
-class NexusInterface;
-class PluginContainer;
-
-/*!
- * \brief manages downloading of files and provides progress information for gui elements
- **/
-class DownloadManager : public MOBase::IDownloadManager
-{
- Q_OBJECT
-
-public:
-
- enum DownloadState {
- STATE_STARTED = 0,
- STATE_DOWNLOADING,
- STATE_CANCELING,
- STATE_PAUSING,
- STATE_CANCELED,
- STATE_PAUSED,
- STATE_ERROR,
- STATE_FETCHINGMODINFO,
- STATE_FETCHINGFILEINFO,
- STATE_NOFETCH,
- STATE_READY,
- STATE_INSTALLED,
- STATE_UNINSTALLED
- };
-
-private:
-
- struct DownloadInfo {
- ~DownloadInfo() { delete m_FileInfo; }
- unsigned int m_DownloadID;
- QString m_FileName;
- QFile m_Output;
- QNetworkReply *m_Reply;
- QTime m_StartTime;
- qint64 m_PreResumeSize;
- std::pair<int, QString> m_Progress;
- std::tuple<int, int, int, int, int> m_SpeedDiff;
- bool m_HasData;
- DownloadState m_State;
- int m_CurrentUrl;
- QStringList m_Urls;
- qint64 m_ResumePos;
- qint64 m_TotalSize;
- QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere
-
- int m_Tries;
- bool m_ReQueried;
-
- quint32 m_TaskProgressId;
-
- MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr };
-
- bool m_Hidden;
-
- static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs);
- static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory);
-
- /**
- * @brief rename the file
- * this will change the file name as well as the display name. It will automatically
- * append .unfinished to the name if this file is still being downloaded
- * @param newName the new name to setName
- * @param renameFile if true, the file is assumed to exist and renamed. If the file does not
- * yet exist, set this to false
- **/
- void setName(QString newName, bool renameFile);
-
- unsigned int downloadID() { return m_DownloadID; }
-
- bool isPausedState();
-
- QString currentURL();
- private:
- static unsigned int s_NextDownloadID;
- private:
- DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple<int,int,int,int,int>(0,0,0,0,0)), m_HasData(false) {}
- };
-
-public:
-
- /**
- * @brief constructor
- *
- * @param nexusInterface interface to use to retrieve information from the relevant nexus page
- * @param parent parent object
- **/
- explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent);
-
- ~DownloadManager();
-
- /**
- * @brief determine if a download is currently in progress
- *
- * @return true if there is currently a download in progress
- **/
- bool downloadsInProgress();
-
- /**
- * @brief determine if a download is currently in progress, does not count paused ones.
- *
- * @return true if there is currently a download in progress (that is not paused already).
- **/
- bool downloadsInProgressNoPause();
-
- /**
- * @brief set the output directory to write to
- *
- * @param outputDirectory the new output directory
- **/
- void setOutputDirectory(const QString &outputDirectory);
-
- /**
- * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called
- *
- **/
- static void startDisableDirWatcher();
-
- /**
- * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called
- **/
- static void endDisableDirWatcher();
-
- /**
- * @return current download directory
- **/
- QString getOutputDirectory() const { return m_OutputDirectory; }
-
- /**
- * @brief setPreferredServers set the list of preferred servers
- */
- void setPreferredServers(const std::map<QString, int> &preferredServers);
-
- /**
- * @brief set the list of supported extensions
- * @param extensions list of supported extensions
- */
- void setSupportedExtensions(const QStringList &extensions);
-
- /**
- * @brief sets whether hidden files are to be shown after all
- */
- void setShowHidden(bool showHidden);
-
- void setPluginContainer(PluginContainer *pluginContainer);
-
- /**
- * @brief download from an already open network connection
- *
- * @param reply the network reply to download from
- * @param fileInfo information about the file, like mod id, file id, version, ...
- * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
- **/
- bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo);
-
- /**
- * @brief download from an already open network connection
- *
- * @param reply the network reply to download from
- * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name
- * @param fileInfo information previously retrieved from the nexus network
- * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
- **/
- bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo());
-
- /**
- * @brief start a download using a nxm-link
- *
- * starts a download using a nxm-link. The download manager will first query the nexus
- * page for file information.
- * @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711
- * @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game
- **/
- void addNXMDownload(const QString &url);
-
- /**
- * @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions
- *
- * @return total number of downloads
- **/
- int numTotalDownloads() const;
-
- /**
- * @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet)
- * @return number of pending downloads
- */
- int numPendingDownloads() const;
-
- /**
- * @brief retrieve the info of a pending download
- * @param index index of the pending download (index in the range [0, numPendingDownloads()[)
- * @return pair of modid, fileid
- */
- std::tuple<QString, int, int> getPendingDownload(int index);
-
- /**
- * @brief retrieve the full path to the download specified by index
- *
- * @param index the index to look up
- * @return absolute path of the file
- **/
- QString getFilePath(int index) const;
-
- /**
- * @brief retrieve a descriptive name of the download specified by index
- *
- * @param index index of the file to look up
- * @return display name of the file
- **/
- QString getDisplayName(int index) const;
-
- /**
- * @brief retrieve the filename of the download specified by index
- *
- * @param index index of the file to look up
- * @return name of the file
- **/
- QString getFileName(int index) const;
-
- /**
- * @brief retrieve the file size of the download specified by index
- *
- * @param index index of the file to look up
- * @return size of the file (total size during download)
- */
- qint64 getFileSize(int index) const;
-
- /**
- * @brief retrieve the creation time of the download specified by index
- * @param index index of the file to look up
- * @return size of the file (total size during download)
- */
- QDateTime getFileTime(int index) const;
-
- /**
- * @brief retrieve the current progress of the download specified by index
- *
- * @param index index of the file to look up
- * @return progress of the download in percent (integer)
- **/
- std::pair<int, QString> getProgress(int index) const;
-
- /**
- * @brief retrieve the current state of the download
- *
- * retrieve the current state of the download. A download usually goes through
- * the following states:
- * started -> downloading -> fetching mod info -> fetching file info -> done
- * in case of downloads started via nxm-link, file information is fetched first
- *
- * @param index index of the file to look up
- * @return the download state
- **/
- DownloadState getState(int index) const;
-
- /**
- * @param index index of the file to look up
- * @return true if the nexus information for this download is not complete
- **/
- bool isInfoIncomplete(int index) const;
-
- /**
- * @brief retrieve the nexus mod id of the download specified by index
- *
- * @param index index of the file to look up
- * @return the nexus mod id
- **/
- int getModID(int index) const;
-
- /**
- * @brief retrieve the game name of the downlaod specified by the index
- *
- * @param index index of the file to look up
- * @return the game name
- **/
- QString getGameName(int index) const;
-
- /**
- * @brief determine if the specified file is supposed to be hidden
- * @param index index of the file to look up
- * @return true if the specified file is supposed to be hidden
- */
- bool isHidden(int index) const;
-
- /**
- * @brief retrieve all nexus info of the download specified by index
- *
- * @param index index of the file to look up
- * @return the nexus mod information
- **/
- const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const;
-
- /**
- * @brief mark a download as installed
- *
- * @param index index of the file to mark installed
- */
- void markInstalled(int index);
-
- void markInstalled(QString download);
-
- /**
- * @brief mark a download as uninstalled
- *
- * @param index index of the file to mark uninstalled
- */
- void markUninstalled(int index);
-
- void markUninstalled(QString download);
-
- /**
- * @brief refreshes the list of downloads
- */
- void refreshList();
-
- /**
- * @brief Sort function for download servers
- * @param LHS
- * @param RHS
- * @return
- */
- static bool ServerByPreference(const std::map<QString, int> &preferredServers, const QVariant &LHS, const QVariant &RHS);
-
-
- virtual int startDownloadURLs(const QStringList &urls);
-
- virtual int startDownloadNexusFile(int modID, int fileID);
-
- virtual QString downloadPath(int id);
-
- /**
- * @brief retrieve a download index from the filename
- * @param fileName file to look up
- * @return index of that download or -1 if it wasn't found
- */
- int indexByName(const QString &fileName) const;
-
- void pauseAll();
-
-signals:
-
- void aboutToUpdate();
-
- /**
- * @brief signals that the specified download has changed
- *
- * @param row the row that changed. This corresponds to the download index
- **/
- void update(int row);
-
- /**
- * @brief signals the ui that a message should be displayed
- *
- * @param message the message to display
- **/
- void showMessage(const QString &message);
-
- /**
- * @brief emitted whenever the state of a download changes
- * @param row the row that changed
- * @param state the new state
- */
- void stateChanged(int row, DownloadManager::DownloadState state);
-
- /**
- * @brief emitted whenever a download completes successfully, reporting the download speed for the server used
- */
- void downloadSpeed(const QString &serverName, int bytesPerSecond);
-
- /**
- * @brief emitted whenever a new download is added to the list
- */
- void downloadAdded();
-
-public slots:
-
- /**
- * @brief removes the specified download
- *
- * @param index index of the download to remove
- * @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden.
- **/
- void removeDownload(int index, bool deleteFile);
-
- /**
- * @brief restores the specified download to view (which was previously hidden
- * @param index index of the download to restore
- */
- void restoreDownload(int index);
-
- /**
- * @brief cancel the specified download. This will lead to the corresponding file to be deleted
- *
- * @param index index of the download to cancel
- **/
- void cancelDownload(int index);
-
- void pauseDownload(int index);
-
- void resumeDownload(int index);
-
- void queryInfo(int index);
-
- void visitOnNexus(int index);
-
- void openFile(int index);
-
- void openInDownloadsFolder(int index);
-
- void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
-
- void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
-
- void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
-
- void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
-
- void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString);
-
- void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
-
-private slots:
-
- void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
- void downloadReadyRead();
- void downloadFinished(int index = 0);
- void downloadError(QNetworkReply::NetworkError error);
- void metaDataChanged();
- void directoryChanged(const QString &dirctory);
- void checkDownloadTimeout();
-
-private:
-
- void createMetaFile(DownloadInfo *info);
- DownloadManager::DownloadInfo* getDownloadInfo(QString fileName);
-
-public:
-
- /** Get a unique filename for a download.
- *
- * This allows you multiple versions of download files, useful if the file
- * comes from a web site with no version control
- *
- * @param basename: Name of the file
- *
- * @return Unique(ish) name
- */
- QString getDownloadFileName(const QString &baseName, bool rename = false) const;
-
-private:
-
- void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume);
- void resumeDownloadInt(int index);
-
- /**
- * @brief start a download from a url
- *
- * @param url the url to download from
- * @param fileInfo information previously retrieved from the mod page
- * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
- **/
- bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo);
-
- // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time
- DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const;
-
- void removeFile(int index, bool deleteFile);
-
- void refreshAlphabeticalTranslation();
-
- bool ByName(int LHS, int RHS);
-
- QString getFileNameFromNetworkReply(QNetworkReply *reply);
-
- void setState(DownloadInfo *info, DownloadManager::DownloadState state);
-
- DownloadInfo *downloadInfoByID(unsigned int id);
-
- QDateTime matchDate(const QString &timeString);
-
- void removePending(QString gameName, int modID, int fileID);
-
- static QString getFileTypeString(int fileType);
-
- void writeData(DownloadInfo *info);
-
-private:
-
- static const int AUTOMATIC_RETRIES = 3;
-
-private:
-
- NexusInterface *m_NexusInterface;
-
- QVector<std::tuple<QString, int, int>> m_PendingDownloads;
-
- QVector<DownloadInfo*> m_ActiveDownloads;
-
- QString m_OutputDirectory;
- std::map<QString, int> m_PreferredServers;
- QStringList m_SupportedExtensions;
- std::set<int> m_RequestIDs;
- QVector<int> m_AlphabeticalTranslation;
-
- QFileSystemWatcher m_DirWatcher;
-
- //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files
- //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder.
- //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui.
- static int m_DirWatcherDisabler;
-
-
- std::map<QString, int> m_DownloadFails;
-
- bool m_ShowHidden;
-
- QRegExp m_DateExpression;
-
- MOBase::IPluginGame const *m_ManagedGame;
-
- QTimer m_TimeoutTimer;
-};
-
-
-
-#endif // DOWNLOADMANAGER_H
+/*
+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/>.
+*/
+
+#ifndef DOWNLOADMANAGER_H
+#define DOWNLOADMANAGER_H
+
+#include <idownloadmanager.h>
+#include <modrepositoryfileinfo.h>
+#include <set>
+#include <QObject>
+#include <QUrl>
+#include <QQueue>
+#include <QFile>
+#include <QNetworkReply>
+#include <QTime>
+#include <QTimer>
+#include <QVector>
+#include <QMap>
+#include <QStringList>
+#include <QFileSystemWatcher>
+#include <QSettings>
+
+namespace MOBase { class IPluginGame; }
+
+class NexusInterface;
+class PluginContainer;
+class OrganizerCore;
+
+/*!
+ * \brief manages downloading of files and provides progress information for gui elements
+ **/
+class DownloadManager : public MOBase::IDownloadManager
+{
+ Q_OBJECT
+
+public:
+
+ enum DownloadState {
+ STATE_STARTED = 0,
+ STATE_DOWNLOADING,
+ STATE_CANCELING,
+ STATE_PAUSING,
+ STATE_CANCELED,
+ STATE_PAUSED,
+ STATE_ERROR,
+ STATE_FETCHINGMODINFO,
+ STATE_FETCHINGFILEINFO,
+ STATE_FETCHINGMODINFO_MD5,
+ STATE_NOFETCH,
+ STATE_READY,
+ STATE_INSTALLED,
+ STATE_UNINSTALLED
+ };
+
+private:
+
+ struct DownloadInfo {
+ ~DownloadInfo() { delete m_FileInfo; }
+ unsigned int m_DownloadID;
+ QString m_FileName;
+ QFile m_Output;
+ QNetworkReply *m_Reply;
+ QTime m_StartTime;
+ qint64 m_PreResumeSize;
+ std::pair<int, QString> m_Progress;
+ std::tuple<int, int, int, int, int> m_SpeedDiff;
+ bool m_HasData;
+ DownloadState m_State;
+ int m_CurrentUrl;
+ QStringList m_Urls;
+ qint64 m_ResumePos;
+ qint64 m_TotalSize;
+ QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere
+ QByteArray m_Hash;
+ QStringList m_GamesToQuery;
+ QString m_RemoteFileName;
+
+ int m_Tries;
+ bool m_ReQueried;
+
+ quint32 m_TaskProgressId;
+
+ MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr };
+
+ bool m_Hidden;
+
+ static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs);
+ static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory);
+
+ /**
+ * @brief rename the file
+ * this will change the file name as well as the display name. It will automatically
+ * append .unfinished to the name if this file is still being downloaded
+ * @param newName the new name to setName
+ * @param renameFile if true, the file is assumed to exist and renamed. If the file does not
+ * yet exist, set this to false
+ **/
+ void setName(QString newName, bool renameFile);
+
+ unsigned int downloadID() { return m_DownloadID; }
+
+ bool isPausedState();
+
+ QString currentURL();
+ private:
+ static unsigned int s_NextDownloadID;
+ private:
+ DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple<int,int,int,int,int>(0,0,0,0,0)), m_HasData(false) {}
+ };
+
+public:
+
+ /**
+ * @brief constructor
+ *
+ * @param nexusInterface interface to use to retrieve information from the relevant nexus page
+ * @param parent parent object
+ **/
+ explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent);
+
+ ~DownloadManager();
+
+ /**
+ * @brief determine if a download is currently in progress
+ *
+ * @return true if there is currently a download in progress
+ **/
+ bool downloadsInProgress();
+
+ /**
+ * @brief determine if a download is currently in progress, does not count paused ones.
+ *
+ * @return true if there is currently a download in progress (that is not paused already).
+ **/
+ bool downloadsInProgressNoPause();
+
+ /**
+ * @brief set the output directory to write to
+ *
+ * @param outputDirectory the new output directory
+ **/
+ void setOutputDirectory(const QString &outputDirectory);
+
+ /**
+ * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called
+ *
+ **/
+ static void startDisableDirWatcher();
+
+ /**
+ * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called
+ **/
+ static void endDisableDirWatcher();
+
+ /**
+ * @return current download directory
+ **/
+ QString getOutputDirectory() const { return m_OutputDirectory; }
+
+ /**
+ * @brief setPreferredServers set the list of preferred servers
+ */
+ void setPreferredServers(const std::map<QString, int> &preferredServers);
+
+ /**
+ * @brief set the list of supported extensions
+ * @param extensions list of supported extensions
+ */
+ void setSupportedExtensions(const QStringList &extensions);
+
+ /**
+ * @brief sets whether hidden files are to be shown after all
+ */
+ void setShowHidden(bool showHidden);
+
+ void setPluginContainer(PluginContainer *pluginContainer);
+
+ /**
+ * @brief download from an already open network connection
+ *
+ * @param reply the network reply to download from
+ * @param fileInfo information about the file, like mod id, file id, version, ...
+ * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
+ **/
+ bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo);
+
+ /**
+ * @brief download from an already open network connection
+ *
+ * @param reply the network reply to download from
+ * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name
+ * @param fileInfo information previously retrieved from the nexus network
+ * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
+ **/
+ bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo());
+
+ /**
+ * @brief start a download using a nxm-link
+ *
+ * starts a download using a nxm-link. The download manager will first query the nexus
+ * page for file information.
+ * @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711
+ * @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game
+ **/
+ void addNXMDownload(const QString &url);
+
+ /**
+ * @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions
+ *
+ * @return total number of downloads
+ **/
+ int numTotalDownloads() const;
+
+ /**
+ * @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet)
+ * @return number of pending downloads
+ */
+ int numPendingDownloads() const;
+
+ /**
+ * @brief retrieve the info of a pending download
+ * @param index index of the pending download (index in the range [0, numPendingDownloads()[)
+ * @return pair of modid, fileid
+ */
+ std::tuple<QString, int, int> getPendingDownload(int index);
+
+ /**
+ * @brief retrieve the full path to the download specified by index
+ *
+ * @param index the index to look up
+ * @return absolute path of the file
+ **/
+ QString getFilePath(int index) const;
+
+ /**
+ * @brief retrieve a descriptive name of the download specified by index
+ *
+ * @param index index of the file to look up
+ * @return display name of the file
+ **/
+ QString getDisplayName(int index) const;
+
+ /**
+ * @brief retrieve the filename of the download specified by index
+ *
+ * @param index index of the file to look up
+ * @return name of the file
+ **/
+ QString getFileName(int index) const;
+
+ /**
+ * @brief retrieve the file size of the download specified by index
+ *
+ * @param index index of the file to look up
+ * @return size of the file (total size during download)
+ */
+ qint64 getFileSize(int index) const;
+
+ /**
+ * @brief retrieve the creation time of the download specified by index
+ * @param index index of the file to look up
+ * @return size of the file (total size during download)
+ */
+ QDateTime getFileTime(int index) const;
+
+ /**
+ * @brief retrieve the current progress of the download specified by index
+ *
+ * @param index index of the file to look up
+ * @return progress of the download in percent (integer)
+ **/
+ std::pair<int, QString> getProgress(int index) const;
+
+ /**
+ * @brief retrieve the current state of the download
+ *
+ * retrieve the current state of the download. A download usually goes through
+ * the following states:
+ * started -> downloading -> fetching mod info -> fetching file info -> done
+ * in case of downloads started via nxm-link, file information is fetched first
+ *
+ * @param index index of the file to look up
+ * @return the download state
+ **/
+ DownloadState getState(int index) const;
+
+ /**
+ * @param index index of the file to look up
+ * @return true if the nexus information for this download is not complete
+ **/
+ bool isInfoIncomplete(int index) const;
+
+ /**
+ * @brief retrieve the nexus mod id of the download specified by index
+ *
+ * @param index index of the file to look up
+ * @return the nexus mod id
+ **/
+ int getModID(int index) const;
+
+ /**
+ * @brief retrieve the game name of the downlaod specified by the index
+ *
+ * @param index index of the file to look up
+ * @return the game name
+ **/
+ QString getGameName(int index) const;
+
+ /**
+ * @brief determine if the specified file is supposed to be hidden
+ * @param index index of the file to look up
+ * @return true if the specified file is supposed to be hidden
+ */
+ bool isHidden(int index) const;
+
+ /**
+ * @brief retrieve all nexus info of the download specified by index
+ *
+ * @param index index of the file to look up
+ * @return the nexus mod information
+ **/
+ const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const;
+
+ /**
+ * @brief mark a download as installed
+ *
+ * @param index index of the file to mark installed
+ */
+ void markInstalled(int index);
+
+ void markInstalled(QString download);
+
+ /**
+ * @brief mark a download as uninstalled
+ *
+ * @param index index of the file to mark uninstalled
+ */
+ void markUninstalled(int index);
+
+ void markUninstalled(QString download);
+
+ /**
+ * @brief refreshes the list of downloads
+ */
+ void refreshList();
+
+ /**
+ * @brief Sort function for download servers
+ * @param LHS
+ * @param RHS
+ * @return
+ */
+ static bool ServerByPreference(const std::map<QString, int> &preferredServers, const QVariant &LHS, const QVariant &RHS);
+
+
+ virtual int startDownloadURLs(const QStringList &urls);
+
+ virtual int startDownloadNexusFile(int modID, int fileID);
+
+ virtual QString downloadPath(int id);
+
+ /**
+ * @brief retrieve a download index from the filename
+ * @param fileName file to look up
+ * @return index of that download or -1 if it wasn't found
+ */
+ int indexByName(const QString &fileName) const;
+
+ void pauseAll();
+
+signals:
+
+ void aboutToUpdate();
+
+ /**
+ * @brief signals that the specified download has changed
+ *
+ * @param row the row that changed. This corresponds to the download index
+ **/
+ void update(int row);
+
+ /**
+ * @brief signals the ui that a message should be displayed
+ *
+ * @param message the message to display
+ **/
+ void showMessage(const QString &message);
+
+ /**
+ * @brief emitted whenever the state of a download changes
+ * @param row the row that changed
+ * @param state the new state
+ */
+ void stateChanged(int row, DownloadManager::DownloadState state);
+
+ /**
+ * @brief emitted whenever a download completes successfully, reporting the download speed for the server used
+ */
+ void downloadSpeed(const QString &serverName, int bytesPerSecond);
+
+ /**
+ * @brief emitted whenever a new download is added to the list
+ */
+ void downloadAdded();
+
+public slots:
+
+ /**
+ * @brief removes the specified download
+ *
+ * @param index index of the download to remove
+ * @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden.
+ **/
+ void removeDownload(int index, bool deleteFile);
+
+ /**
+ * @brief restores the specified download to view (which was previously hidden
+ * @param index index of the download to restore
+ */
+ void restoreDownload(int index);
+
+ /**
+ * @brief cancel the specified download. This will lead to the corresponding file to be deleted
+ *
+ * @param index index of the download to cancel
+ **/
+ void cancelDownload(int index);
+
+ void pauseDownload(int index);
+
+ void resumeDownload(int index);
+
+ void queryInfo(int index);
+
+ void queryInfoMd5(int index);
+
+ void visitOnNexus(int index);
+
+ void openFile(int index);
+
+ void openInDownloadsFolder(int index);
+
+ void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+
+ void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+
+ void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
+
+ void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
+
+ void nxmFileInfoFromMd5Available(QString gameName, 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 managedGameChanged(MOBase::IPluginGame const *gamePlugin);
+
+private slots:
+
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void downloadReadyRead();
+ void downloadFinished(int index = 0);
+ void downloadError(QNetworkReply::NetworkError error);
+ void metaDataChanged();
+ void directoryChanged(const QString &dirctory);
+ void checkDownloadTimeout();
+
+private:
+
+ void createMetaFile(DownloadInfo *info);
+ DownloadManager::DownloadInfo* getDownloadInfo(QString fileName);
+
+public:
+
+ /** Get a unique filename for a download.
+ *
+ * This allows you multiple versions of download files, useful if the file
+ * comes from a web site with no version control
+ *
+ * @param basename: Name of the file
+ *
+ * @return Unique(ish) name
+ */
+ QString getDownloadFileName(const QString &baseName, bool rename = false) const;
+
+private:
+
+ void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume);
+ void resumeDownloadInt(int index);
+
+ /**
+ * @brief start a download from a url
+ *
+ * @param url the url to download from
+ * @param fileInfo information previously retrieved from the mod page
+ * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
+ **/
+ bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo);
+
+ // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time
+ DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const;
+
+ void removeFile(int index, bool deleteFile);
+
+ void refreshAlphabeticalTranslation();
+
+ bool ByName(int LHS, int RHS);
+
+ QString getFileNameFromNetworkReply(QNetworkReply *reply);
+
+ void setState(DownloadInfo *info, DownloadManager::DownloadState state);
+
+ DownloadInfo *downloadInfoByID(unsigned int id);
+
+ void removePending(QString gameName, int modID, int fileID);
+
+ static QString getFileTypeString(int fileType);
+
+ void writeData(DownloadInfo *info);
+
+private:
+
+ static const int AUTOMATIC_RETRIES = 3;
+
+private:
+
+ NexusInterface *m_NexusInterface;
+
+ OrganizerCore *m_OrganizerCore;
+
+ QVector<std::tuple<QString, int, int>> m_PendingDownloads;
+
+ QVector<DownloadInfo*> m_ActiveDownloads;
+
+ QString m_OutputDirectory;
+ std::map<QString, int> m_PreferredServers;
+ QStringList m_SupportedExtensions;
+ std::set<int> m_RequestIDs;
+ QVector<int> m_AlphabeticalTranslation;
+
+ QFileSystemWatcher m_DirWatcher;
+
+ //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files
+ //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder.
+ //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui.
+ static int m_DirWatcherDisabler;
+
+
+ std::map<QString, int> m_DownloadFails;
+
+ bool m_ShowHidden;
+
+ MOBase::IPluginGame const *m_ManagedGame;
+
+ QTimer m_TimeoutTimer;
+};
+
+
+
+#endif // DOWNLOADMANAGER_H
diff --git a/src/editexecutablesdialog.cpp b/src/editexecutablesdialog.cpp
index fde6b397..177661ff 100644
--- a/src/editexecutablesdialog.cpp
+++ b/src/editexecutablesdialog.cpp
@@ -25,25 +25,29 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#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, QWidget *parent)
+ Profile *profile, const IPluginGame *game, QWidget *parent)
: TutorableDialog("EditExecutables", parent)
, ui(new Ui::EditExecutablesDialog)
, m_CurrentItem(nullptr)
, m_ExecutablesList(executablesList)
, m_Profile(profile)
+ , m_GamePlugin(game)
{
ui->setupUi(this);
refreshExecutablesWidget();
ui->newFilesModBox->addItems(modList.allMods());
+
+ m_ForcedLibraries = m_Profile->determineForcedLibraries(ui->titleEdit->text());
}
EditExecutablesDialog::~EditExecutablesDialog()
@@ -79,8 +83,33 @@ void EditExecutablesDialog::refreshExecutablesWidget()
void EditExecutablesDialog::on_binaryEdit_textChanged(const QString &name)
{
- QFileInfo fileInfo(name);
- ui->addButton->setEnabled(fileInfo.exists() && fileInfo.isFile());
+ updateButtonStates();
+}
+
+void EditExecutablesDialog::on_workingDirEdit_textChanged(const QString &dir)
+{
+ updateButtonStates();
+}
+
+void EditExecutablesDialog::updateButtonStates()
+{
+ bool enabled = true;
+
+ QString filePath(ui->binaryEdit->text());
+ QFileInfo fileInfo(filePath);
+ if (!fileInfo.exists())
+ enabled = false;
+ if (!fileInfo.isFile())
+ enabled = false;
+
+ QString dirPath(ui->workingDirEdit->text());
+ if (!dirPath.isEmpty()) {
+ QDir dirInfo(dirPath);
+ if (!dirInfo.exists())
+ enabled = false;
+ }
+
+ ui->addButton->setEnabled(enabled);
}
void EditExecutablesDialog::resetInput()
@@ -93,6 +122,7 @@ void EditExecutablesDialog::resetInput()
ui->overwriteAppIDBox->setChecked(false);
ui->useAppIconCheckBox->setChecked(false);
ui->newFilesModCheckBox->setChecked(false);
+ ui->forceLoadCheckBox->setChecked(false);
m_CurrentItem = nullptr;
}
@@ -118,6 +148,10 @@ void EditExecutablesDialog::saveExecutable()
else {
m_Profile->removeSetting("custom_overwrites", ui->titleEdit->text());
}
+
+ m_Profile->removeForcedLibraries(ui->titleEdit->text());
+ m_Profile->storeForcedLibraries(ui->titleEdit->text(), m_ForcedLibraries);
+ m_Profile->setForcedLibrariesEnabled(ui->titleEdit->text(), ui->forceLoadCheckBox->isChecked());
}
@@ -130,6 +164,21 @@ void EditExecutablesDialog::delayedRefresh()
}
+void EditExecutablesDialog::on_forceLoadButton_clicked()
+{
+ ForcedLoadDialog dialog(m_GamePlugin, this);
+ dialog.setValues(m_ForcedLibraries);
+ if (dialog.exec() == QDialog::Accepted) {
+ m_ForcedLibraries = dialog.values();
+ }
+}
+
+void EditExecutablesDialog::on_forceLoadCheckBox_toggled()
+{
+ ui->forceLoadButton->setEnabled(ui->forceLoadCheckBox->isChecked());
+}
+
+
void EditExecutablesDialog::on_addButton_clicked()
{
if (executableChanged()) {
@@ -181,6 +230,10 @@ void EditExecutablesDialog::on_browseButton_clicked()
} else {
ui->binaryEdit->setText(QDir::toNativeSeparators(binaryName));
}
+
+ if (ui->titleEdit->text().isEmpty()) {
+ ui->titleEdit->setText(QFileInfo(binaryName).baseName());
+ }
}
void EditExecutablesDialog::on_browseDirButton_clicked()
@@ -195,6 +248,8 @@ void EditExecutablesDialog::on_removeButton_clicked()
{
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());
}
@@ -231,6 +286,20 @@ bool EditExecutablesDialog::executableChanged()
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();
+
return selectedExecutable.m_Title != ui->titleEdit->text()
|| selectedExecutable.m_Arguments != ui->argumentsEdit->text()
|| selectedExecutable.m_SteamAppID != ui->appIDOverwriteEdit->text()
@@ -238,7 +307,9 @@ bool EditExecutablesDialog::executableChanged()
|| !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();
+ || selectedExecutable.usesOwnIcon() != ui->useAppIconCheckBox->isChecked()
+ || forcedLibrariesDirty
+ ;
} else {
QFileInfo fileInfo(ui->binaryEdit->text());
return !ui->binaryEdit->text().isEmpty()
@@ -247,7 +318,6 @@ bool EditExecutablesDialog::executableChanged()
&& fileInfo.isFile();
}
}
-
void EditExecutablesDialog::on_executablesListBox_itemSelectionChanged()
{
if (ui->executablesListBox->selectedItems().size() == 0) {
@@ -324,13 +394,18 @@ void EditExecutablesDialog::on_executablesListBox_clicked(const QModelIndex &cur
QString customOverwrite = m_Profile->setting("custom_overwrites", selectedExecutable.m_Title).toString();
if (!customOverwrite.isEmpty()) {
index = ui->newFilesModBox->findText(customOverwrite);
- qDebug("find %s -> %d", qPrintable(customOverwrite), index);
+ qDebug("find %s -> %d", qUtf8Printable(customOverwrite), index);
}
ui->newFilesModCheckBox->setChecked(index != -1);
if (index != -1) {
ui->newFilesModBox->setCurrentIndex(index);
}
+
+ m_ForcedLibraries = m_Profile->determineForcedLibraries(ui->titleEdit->text());
+ bool forcedLibraries = m_Profile->forcedLibrariesEnabled(ui->titleEdit->text());
+ ui->forceLoadButton->setEnabled(forcedLibraries);
+ ui->forceLoadCheckBox->setChecked(forcedLibraries);
}
}
diff --git a/src/editexecutablesdialog.h b/src/editexecutablesdialog.h
index 0f3dbaff..bee3cba6 100644
--- a/src/editexecutablesdialog.h
+++ b/src/editexecutablesdialog.h
@@ -25,6 +25,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QTimer>
#include "executableslist.h"
#include "profile.h"
+#include "iplugingame.h"
namespace Ui {
class EditExecutablesDialog;
@@ -52,6 +53,7 @@ public:
explicit EditExecutablesDialog(const ExecutablesList &executablesList,
const ModList &modList,
Profile *profile,
+ const MOBase::IPluginGame *game,
QWidget *parent = 0);
~EditExecutablesDialog();
@@ -72,6 +74,8 @@ private slots:
void on_binaryEdit_textChanged(const QString &arg1);
+ void on_workingDirEdit_textChanged(const QString &arg1);
+
void on_addButton_clicked();
void on_browseButton_clicked();
@@ -92,6 +96,10 @@ private slots:
void on_executablesListBox_clicked(const QModelIndex &index);
+ void on_forceLoadButton_clicked();
+
+ void on_forceLoadCheckBox_toggled();
+
private:
void resetInput();
@@ -100,6 +108,8 @@ private:
bool executableChanged();
+ void updateButtonStates();
+
private:
Ui::EditExecutablesDialog *ui;
@@ -108,6 +118,10 @@ private:
ExecutablesList m_ExecutablesList;
Profile *m_Profile;
+
+ QList<MOBase::ExecutableForcedLoadSetting> m_ForcedLibraries;
+
+ const MOBase::IPluginGame *m_GamePlugin;
};
#endif // EDITEXECUTABLESDIALOG_H
diff --git a/src/editexecutablesdialog.ui b/src/editexecutablesdialog.ui
index bdbb8bd1..4f9223d5 100644
--- a/src/editexecutablesdialog.ui
+++ b/src/editexecutablesdialog.ui
@@ -192,6 +192,43 @@ Right now the only case I know of where this needs to be overwritten is for the
</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>
diff --git a/src/executableslist.cpp b/src/executableslist.cpp
index 21cb6ed4..4b2e380b 100644
--- a/src/executableslist.cpp
+++ b/src/executableslist.cpp
@@ -25,6 +25,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QFileInfo>
#include <QDir>
#include <QDebug>
+#include <QCoreApplication>
+
#include <algorithm>
@@ -53,6 +55,17 @@ void ExecutablesList::init(IPluginGame const *game)
info.steamAppID());
}
}
+ 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());
+ }
+
}
void ExecutablesList::getExecutables(std::vector<Executable>::iterator &begin, std::vector<Executable>::iterator &end)
@@ -75,7 +88,7 @@ const Executable &ExecutablesList::find(const QString &title) const
return exe;
}
}
- throw std::runtime_error(QString("invalid name %1").arg(title).toLocal8Bit().constData());
+ throw std::runtime_error(QString("invalid executable name: %1").arg(title).toLocal8Bit().constData());
}
@@ -86,7 +99,7 @@ Executable &ExecutablesList::find(const QString &title)
return exe;
}
}
- throw std::runtime_error(QString("invalid executable name %1").arg(title).toLocal8Bit().constData());
+ throw std::runtime_error(QString("invalid executable name: %1").arg(title).toLocal8Bit().constData());
}
@@ -139,6 +152,7 @@ void ExecutablesList::updateExecutable(const QString &title,
Executable::Flags flags)
{
QFileInfo file(executableName);
+ QDir dir(workingDirectory);
auto existingExe = findExe(title);
flags &= mask;
@@ -152,8 +166,11 @@ void ExecutablesList::updateExecutable(const QString &title,
// 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_WorkingDirectory = workingDirectory;
existingExe->m_SteamAppID = steamAppID;
}
} else {
diff --git a/src/forcedloaddialog.cpp b/src/forcedloaddialog.cpp
new file mode 100644
index 00000000..ab0bd583
--- /dev/null
+++ b/src/forcedloaddialog.cpp
@@ -0,0 +1,77 @@
+#include "forcedloaddialog.h"
+#include "ui_forcedloaddialog.h"
+
+#include "forcedloaddialogwidget.h"
+
+#include <QComboBox>
+#include <QLabel>
+
+using namespace MOBase;
+
+ForcedLoadDialog::ForcedLoadDialog(const IPluginGame *game, QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::ForcedLoadDialog),
+ m_GamePlugin(game)
+{
+ ui->setupUi(this);
+}
+
+ForcedLoadDialog::~ForcedLoadDialog()
+{
+ delete ui;
+}
+
+void ForcedLoadDialog::setValues(QList<MOBase::ExecutableForcedLoadSetting> &values)
+{
+ ui->tableWidget->clearContents();
+
+ for (int i = 0; i < values.count(); i++) {
+ ForcedLoadDialogWidget *item = new ForcedLoadDialogWidget(m_GamePlugin, this);
+ item->setEnabled(values[i].enabled());
+ item->setProcess(values[i].process());
+ item->setLibraryPath(values[i].library());
+ item->setForced(values[i].forced());
+
+ ui->tableWidget->insertRow(i);
+ ui->tableWidget->setCellWidget(i, 0, item);
+ }
+
+ ui->tableWidget->resizeRowsToContents();
+}
+
+QList<MOBase::ExecutableForcedLoadSetting> ForcedLoadDialog::values()
+{
+ QList<MOBase::ExecutableForcedLoadSetting> results;
+ for (int row = 0; row < ui->tableWidget->rowCount(); row++) {
+ auto widget = (ForcedLoadDialogWidget*)ui->tableWidget->cellWidget(row, 0);
+ results.append(
+ ExecutableForcedLoadSetting(
+ widget->getProcess(),
+ widget->getLibraryPath()
+ ).withEnabled(widget->getEnabled())
+ .withForced(widget->getForced())
+ );
+ }
+ return results;
+}
+
+
+void ForcedLoadDialog::on_addRowButton_clicked()
+{
+ int row = ui->tableWidget->rowCount();
+ ui->tableWidget->insertRow(row);
+ ForcedLoadDialogWidget *item = new ForcedLoadDialogWidget(m_GamePlugin, this);
+ ui->tableWidget->setCellWidget(row, 0, item);
+ ui->tableWidget->resizeRowsToContents();
+}
+
+void ForcedLoadDialog::on_deleteRowButton_clicked()
+{
+ for (auto rowIndex: ui->tableWidget->selectionModel()->selectedRows()) {
+ int row = rowIndex.row();
+ auto widget = (ForcedLoadDialogWidget*)ui->tableWidget->cellWidget(row, 0);
+ if (!widget->getForced()){
+ ui->tableWidget->removeRow(row);
+ }
+ }
+}
diff --git a/src/forcedloaddialog.h b/src/forcedloaddialog.h
new file mode 100644
index 00000000..6a88bccf
--- /dev/null
+++ b/src/forcedloaddialog.h
@@ -0,0 +1,34 @@
+#ifndef FORCEDLOADDIALOG_H
+#define FORCEDLOADDIALOG_H
+
+#include <QDialog>
+#include <QPushButton>
+
+#include "executableinfo.h"
+#include "iplugingame.h"
+
+namespace Ui {
+class ForcedLoadDialog;
+}
+
+class ForcedLoadDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ForcedLoadDialog(const MOBase::IPluginGame *game, QWidget *parent = nullptr);
+ ~ForcedLoadDialog();
+
+ void setValues(QList<MOBase::ExecutableForcedLoadSetting> &values);
+ QList<MOBase::ExecutableForcedLoadSetting> values();
+
+private slots:
+ void on_addRowButton_clicked();
+ void on_deleteRowButton_clicked();
+
+private:
+ Ui::ForcedLoadDialog *ui;
+ const MOBase::IPluginGame *m_GamePlugin;
+};
+
+#endif // FORCEDLOADDIALOG_H
diff --git a/src/forcedloaddialog.ui b/src/forcedloaddialog.ui
new file mode 100644
index 00000000..9c5b2d10
--- /dev/null
+++ b/src/forcedloaddialog.ui
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ForcedLoadDialog</class>
+ <widget class="QDialog" name="ForcedLoadDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>700</width>
+ <height>400</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Forced Load Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTableWidget" name="tableWidget">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="columnCount">
+ <number>1</number>
+ </property>
+ <attribute name="horizontalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <column/>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="addRowButton">
+ <property name="toolTip">
+ <string>Adds a row to the table.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Adds a row to the table.</string>
+ </property>
+ <property name="text">
+ <string>Add Row</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="deleteRowButton">
+ <property name="toolTip">
+ <string>Deletes the selected row from the table.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Deletes the selected row from the table.</string>
+ </property>
+ <property name="text">
+ <string>Delete Row</string>
+ </property>
+ </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>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ForcedLoadDialog</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>ForcedLoadDialog</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/forcedloaddialogwidget.cpp b/src/forcedloaddialogwidget.cpp
new file mode 100644
index 00000000..10069c35
--- /dev/null
+++ b/src/forcedloaddialogwidget.cpp
@@ -0,0 +1,108 @@
+#include "forcedloaddialogwidget.h"
+#include "ui_forcedloaddialogwidget.h"
+
+#include <QFileDialog>
+
+#include "executableinfo.h"
+
+using namespace MOBase;
+
+ForcedLoadDialogWidget::ForcedLoadDialogWidget(const IPluginGame *game, QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::ForcedLoadDialogWidget),
+ m_GamePlugin(game)
+{
+ ui->setupUi(this);
+}
+
+ForcedLoadDialogWidget::~ForcedLoadDialogWidget()
+{
+ delete ui;
+}
+
+bool ForcedLoadDialogWidget::getEnabled()
+{
+ return ui->enabledBox->isChecked();
+}
+
+bool ForcedLoadDialogWidget::getForced()
+{
+ return m_Forced;
+}
+
+QString ForcedLoadDialogWidget::getLibraryPath()
+{
+ return ui->libraryPathEdit->text();
+}
+
+QString ForcedLoadDialogWidget::getProcess()
+{
+ return ui->processEdit->text();
+}
+
+void ForcedLoadDialogWidget::setEnabled(bool enabled)
+{
+ ui->enabledBox->setChecked(enabled);
+}
+
+void ForcedLoadDialogWidget::setForced(bool forced)
+{
+ m_Forced = forced;
+ ui->libraryPathBrowseButton->setEnabled(!forced);
+ ui->libraryPathEdit->setEnabled(!forced);
+ ui->processBrowseButton->setEnabled(!forced);
+ ui->processEdit->setEnabled(!forced);
+}
+
+void ForcedLoadDialogWidget::setLibraryPath(QString &path)
+{
+ ui->libraryPathEdit->setText(path);
+}
+
+void ForcedLoadDialogWidget::setProcess(QString &name)
+{
+ ui->processEdit->setText(name);
+}
+
+void ForcedLoadDialogWidget::on_enabledBox_toggled()
+{
+ // anything to do?
+}
+
+void ForcedLoadDialogWidget::on_libraryPathBrowseButton_clicked()
+{
+ 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()) {
+ QFileInfo fileInfo(result);
+ QString relativePath = gameDir.relativeFilePath(fileInfo.filePath());
+ QString filePath = fileInfo.filePath();
+ if (!relativePath.startsWith("..")) {
+ filePath = relativePath;
+ }
+
+ if (fileInfo.exists()) {
+ ui->libraryPathEdit->setText(filePath);
+ } else {
+ qCritical("%ls does not exist", filePath.toStdWString().c_str());
+ }
+ }
+}
+
+void ForcedLoadDialogWidget::on_processBrowseButton_clicked()
+{
+ QDir gameDir(m_GamePlugin->gameDirectory());
+ QString startPath = gameDir.absolutePath();
+ QString result = QFileDialog::getOpenFileName(nullptr, "Select a process...", startPath, "Executable (*.exe)", nullptr, QFileDialog::ReadOnly);
+ if (!result.isEmpty()) {
+ QFileInfo fileInfo(result);
+ QString fileName = fileInfo.fileName();
+
+ if (fileInfo.exists()) {
+ 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
new file mode 100644
index 00000000..eb7c3f4d
--- /dev/null
+++ b/src/forcedloaddialogwidget.h
@@ -0,0 +1,41 @@
+#ifndef FORCEDLOADDIALOGWIDGET_H
+#define FORCEDLOADDIALOGWIDGET_H
+
+#include <QWidget>
+#include "iplugingame.h"
+
+namespace Ui {
+class ForcedLoadDialogWidget;
+}
+
+class ForcedLoadDialogWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ForcedLoadDialogWidget(const MOBase::IPluginGame *game, QWidget *parent = nullptr);
+ ~ForcedLoadDialogWidget();
+
+ bool getEnabled();
+ bool getForced();
+ QString getLibraryPath();
+ QString getProcess();
+
+ void setEnabled(bool enabled);
+ void setForced(bool forced);
+ void setLibraryPath(QString &path);
+ void setProcess(QString &name);
+
+private slots:
+ void on_enabledBox_toggled();
+ void on_libraryPathBrowseButton_clicked();
+ void on_processBrowseButton_clicked();
+
+private:
+ Ui::ForcedLoadDialogWidget *ui;
+ bool m_Forced;
+ const MOBase::IPluginGame *m_GamePlugin;
+
+};
+
+#endif // FORCEDLOADDIALOGWIDGET_H
diff --git a/src/forcedloaddialogwidget.ui b/src/forcedloaddialogwidget.ui
new file mode 100644
index 00000000..4be215eb
--- /dev/null
+++ b/src/forcedloaddialogwidget.ui
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ForcedLoadDialogWidget</class>
+ <widget class="QWidget" name="ForcedLoadDialogWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>41</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,2,0">
+ <item>
+ <widget class="QCheckBox" name="enabledBox">
+ <property name="toolTip">
+ <string>If checked, the specified library will be force loaded for the specified process.</string>
+ </property>
+ <property name="whatsThis">
+ <string>If checked, the specified library will be force loaded for the specified process.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="processEdit">
+ <property name="toolTip">
+ <string>The name of the process that should be forced to load a library.</string>
+ </property>
+ <property name="whatsThis">
+ <string>The name of the process that should be forced to load a library.</string>
+ </property>
+ <property name="placeholderText">
+ <string>Process name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="processBrowseButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>32</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Browse for a process.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Browse for a process.</string>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="libraryPathEdit">
+ <property name="toolTip">
+ <string>The path to the library to be loaded. This may be a path relative to the managed game's directory or may be an absolute path to somewhere else.</string>
+ </property>
+ <property name="whatsThis">
+ <string>The path to the library to be loaded. This may be a path relative to the managed game's directory or may be an absolute path to somewhere else.</string>
+ </property>
+ <property name="placeholderText">
+ <string>Library to load</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="libraryPathBrowseButton">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Browse for a library.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Browse for a library.</string>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/helper.cpp b/src/helper.cpp
index b7fc866c..59a2d3d1 100644
--- a/src/helper.cpp
+++ b/src/helper.cpp
@@ -26,6 +26,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <Windows.h>
#include <QDir>
+#include <QApplication>
using MOBase::reportError;
@@ -33,7 +34,7 @@ using MOBase::reportError;
namespace Helper {
-static bool helperExec(LPCWSTR moDirectory, LPCWSTR commandLine)
+static bool helperExec(LPCWSTR moDirectory, LPCWSTR commandLine, BOOL async)
{
wchar_t fileName[MAX_PATH];
_snwprintf(fileName, MAX_PATH, L"%ls\\helper.exe", moDirectory);
@@ -51,7 +52,16 @@ static bool helperExec(LPCWSTR moDirectory, LPCWSTR commandLine)
::ShellExecuteExW(&execInfo);
- if ((execInfo.hProcess == 0) || (::WaitForSingleObject(execInfo.hProcess, INFINITE) != WAIT_OBJECT_0)) {
+ if (execInfo.hProcess == 0) {
+ reportError(QObject::tr("helper failed"));
+ return false;
+ }
+
+ if (async) {
+ return true;
+ }
+
+ if (::WaitForSingleObject(execInfo.hProcess, INFINITE) != WAIT_OBJECT_0) {
reportError(QObject::tr("helper failed"));
return false;
}
@@ -76,7 +86,7 @@ bool init(const std::wstring &moPath, const std::wstring &dataPath)
_snwprintf(commandLine, 32768, L"init \"%ls\" \"%ls\"",
dataPath.c_str(), userName);
- bool res = helperExec(moPath.c_str(), commandLine);
+ bool res = helperExec(moPath.c_str(), commandLine, FALSE);
delete [] commandLine;
return res;
@@ -89,7 +99,23 @@ bool backdateBSAs(const std::wstring &moPath, const std::wstring &dataPath)
_snwprintf(commandLine, 32768, L"backdateBSA \"%ls\"",
dataPath.c_str());
- bool res = helperExec(moPath.c_str(), commandLine);
+ bool res = helperExec(moPath.c_str(), commandLine, FALSE);
+ delete [] commandLine;
+
+ return res;
+}
+
+
+bool adminLaunch(const std::wstring &moPath, const std::wstring &moFile, const std::wstring &workingDir)
+{
+ wchar_t *commandLine = new wchar_t[32768];
+ _snwprintf(commandLine, 32768, L"adminLaunch %d \"%ls\" \"%ls\"",
+ ::GetCurrentProcessId(),
+ moFile.c_str(),
+ workingDir.c_str()
+ );
+
+ bool res = helperExec(moPath.c_str(), commandLine, TRUE);
delete [] commandLine;
return res;
diff --git a/src/helper.h b/src/helper.h
index cd4b7883..f6667a84 100644
--- a/src/helper.h
+++ b/src/helper.h
@@ -26,7 +26,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
/**
* @brief Convenience functions to work with the external helper program.
- *
+ *
* The mo_helper program is used to make changes on the system that require administrative
* rights, so that ModOrganizer itself can run without special privileges
**/
@@ -34,7 +34,7 @@ namespace Helper {
/**
* @brief initialise the specified directory for use with mod organizer.
- *
+ *
* This will create all required sub-directories and give the user running ModOrganizer
* write-access
*
@@ -50,6 +50,14 @@ bool init(const std::wstring &moPath, const std::wstring &dataPath);
**/
bool backdateBSAs(const std::wstring &moPath, const std::wstring &dataPath);
+/**
+ * @brief waits for the current process to exit and restarts it as an administrator
+ * @param moPath absolute path to the modOrganizer base directory
+ * @param moFile file name of modOrganizer
+ * @param workingDir current working directory
+ **/
+bool adminLaunch(const std::wstring &moPath, const std::wstring &moFile, const std::wstring &workingDir);
+
}
diff --git a/src/icondelegate.cpp b/src/icondelegate.cpp
index e502dc69..249dae6f 100644
--- a/src/icondelegate.cpp
+++ b/src/icondelegate.cpp
@@ -54,7 +54,7 @@ void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
if (!QPixmapCache::find(fullIconId, &icon)) {
icon = QIcon(iconId).pixmap(iconWidth, iconWidth);
if (icon.isNull()) {
- qWarning("failed to load icon %s", qPrintable(iconId));
+ qWarning("failed to load icon %s", qUtf8Printable(iconId));
}
QPixmapCache::insert(fullIconId, icon);
}
diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp
index 76b1e086..e443a8f2 100644
--- a/src/installationmanager.cpp
+++ b/src/installationmanager.cpp
@@ -80,7 +80,8 @@ static T resolveFunction(QLibrary &lib, const char *name)
InstallationManager::InstallationManager()
: m_ParentWidget(nullptr),
- m_SupportedExtensions({"zip", "rar", "7z", "fomod", "001"}) {
+ m_SupportedExtensions({"zip", "rar", "7z", "fomod", "001"}),
+ m_IsRunning(false) {
QLibrary archiveLib(QCoreApplication::applicationDirPath() +
"\\dlls\\archive.dll");
if (!archiveLib.load()) {
@@ -526,7 +527,7 @@ bool InstallationManager::testOverwrite(GuessedValue<QString> &modName, bool *me
settingsFile.write(originalSettings);
settingsFile.close();
} else {
- qCritical("failed to restore original settings: %s", metaFilename.toUtf8().constData());
+ qCritical("failed to restore original settings: %s", qUtf8Printable(metaFilename));
}
return true;
} else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE) {
@@ -560,7 +561,7 @@ bool InstallationManager::ensureValidModName(GuessedValue<QString> &name) const
bool InstallationManager::doInstall(GuessedValue<QString> &modName, QString gameName, int modID,
const QString &version, const QString &newestVersion,
- int categoryID, const QString &repository)
+ int categoryID, int fileCategoryID, const QString &repository)
{
if (!ensureValidModName(modName)) {
return false;
@@ -575,10 +576,11 @@ bool InstallationManager::doInstall(GuessedValue<QString> &modName, QString game
QString targetDirectory = QDir(m_ModsDirectory + "/" + modName).canonicalPath();
QString targetDirectoryNative = QDir::toNativeSeparators(targetDirectory);
- qDebug("installing to \"%s\"", targetDirectoryNative.toUtf8().constData());
+ qDebug("installing to \"%s\"", qUtf8Printable(targetDirectoryNative));
m_InstallationProgress = new QProgressDialog(m_ParentWidget);
ON_BLOCK_EXIT([this] () {
+ m_InstallationProgress->cancel();
m_InstallationProgress->hide();
m_InstallationProgress->deleteLater();
m_InstallationProgress = nullptr;
@@ -639,10 +641,14 @@ bool InstallationManager::doInstall(GuessedValue<QString> &modName, QString game
if (!settingsFile.contains("category")) {
settingsFile.setValue("category", QString::number(categoryID));
}
+ settingsFile.setValue("nexusFileStatus", fileCategoryID);
settingsFile.setValue("installationFile", m_CurrentFile);
settingsFile.setValue("repository", repository);
settingsFile.setValue("url", m_URL);
+ //cleanup of m_URL or this will persist across installs.
+ m_URL = "";
+
if (!merge) {
// this does not clear the list we have in memory but the mod is going to have to be re-read anyway
// btw.: installedFiles were written with beginWriteArray but we can still clear it with beginGroup. nice
@@ -660,6 +666,11 @@ bool InstallationManager::wasCancelled()
return m_ArchiveHandler->getLastError() == Archive::ERROR_EXTRACT_CANCELLED;
}
+bool InstallationManager::isRunning()
+{
+ return m_IsRunning;
+}
+
void InstallationManager::postInstallCleanup()
{
@@ -701,6 +712,9 @@ bool InstallationManager::install(const QString &fileName,
bool &hasIniTweaks,
int modID)
{
+ m_IsRunning = true;
+ ON_BLOCK_EXIT([this]() { m_IsRunning = false; });
+
QFileInfo fileInfo(fileName);
if (m_SupportedExtensions.find(fileInfo.suffix()) == m_SupportedExtensions.end()) {
reportError(tr("File format \"%1\" not supported").arg(fileInfo.suffix()));
@@ -716,6 +730,7 @@ bool InstallationManager::install(const QString &fileName,
QString version = "";
QString newestVersion = "";
int categoryID = 0;
+ int fileCategoryID = 1;
QString repository = "Nexus";
QString metaName = fileName + ".meta";
@@ -734,6 +749,7 @@ bool InstallationManager::install(const QString &fileName,
metaFile.value("category", 0).toInt());
categoryID = CategoryFactory::instance().getCategoryID(categoryIndex);
repository = metaFile.value("repository", "").toString();
+ fileCategoryID = metaFile.value("fileCategory", 1).toInt();
}
if (version.isEmpty()) {
@@ -758,7 +774,7 @@ bool InstallationManager::install(const QString &fileName,
if (fileInfo.dir() == QDir(m_DownloadsDirectory)) {
m_CurrentFile = fileInfo.fileName();
}
- qDebug("using mod name \"%s\" (id %d) -> %s", modName->toUtf8().constData(), modID, qPrintable(m_CurrentFile));
+ qDebug("using mod name \"%s\" (id %d) -> %s", qUtf8Printable(modName), modID, qUtf8Printable(m_CurrentFile));
//If there's an archive already open, close it. This happens with the bundle
//installer when it uncompresses a split archive, then finds it has a real archive
@@ -770,8 +786,8 @@ bool InstallationManager::install(const QString &fileName,
new MethodCallback<InstallationManager, void, QString *>(this, &InstallationManager::queryPassword));
if (!archiveOpen) {
qDebug("integrated archiver can't open %s: %s (%d)",
- qPrintable(fileName),
- qPrintable(getErrorString(m_ArchiveHandler->getLastError())),
+ qUtf8Printable(fileName),
+ qUtf8Printable(getErrorString(m_ArchiveHandler->getLastError())),
m_ArchiveHandler->getLastError());
}
ON_BLOCK_EXIT(std::bind(&InstallationManager::postInstallCleanup, this));
@@ -811,7 +827,7 @@ bool InstallationManager::install(const QString &fileName,
// the simple installer only prepares the installation, the rest
// works the same for all installers
if (!doInstall(modName, gameName, modID, version, newestVersion, categoryID,
- repository)) {
+ fileCategoryID, repository)) {
installResult = IPluginInstaller::RESULT_FAILED;
}
}
@@ -841,7 +857,7 @@ bool InstallationManager::install(const QString &fileName,
}
} catch (const IncompatibilityException &e) {
qCritical("plugin \"%s\" incompatible: %s",
- qPrintable(installer->name()), e.what());
+ qUtf8Printable(installer->name()), e.what());
}
// act upon the installation result. at this point the files have already been
@@ -867,6 +883,7 @@ bool InstallationManager::install(const QString &fileName,
reportError(tr("None of the available installer plugins were able to handle that archive.\n"
"This is likely due to a corrupted or incompatible download or unrecognized archive format."));
+
return false;
}
diff --git a/src/installationmanager.h b/src/installationmanager.h
index 7c43b4e2..6e4f5925 100644
--- a/src/installationmanager.h
+++ b/src/installationmanager.h
@@ -90,6 +90,11 @@ public:
bool wasCancelled();
/**
+ * @return true if an installation is currently in progress
+ **/
+ bool isRunning();
+
+ /**
* @brief retrieve a string describing the specified error code
*
* @param errorCode an error code as returned by the archiving function
@@ -171,7 +176,7 @@ private:
MOBase::DirectoryTree::Node *getSimpleArchiveBase(MOBase::DirectoryTree *dataTree);
bool doInstall(MOBase::GuessedValue<QString> &modName, QString gameName,
- int modID, const QString &version, const QString &newestVersion, int categoryID, const QString &repository);
+ int modID, const QString &version, const QString &newestVersion, int categoryID, int fileCategoryID, const QString &repository);
//QString generateBackupName(const QString &directoryName) const;
@@ -202,6 +207,8 @@ private:
private:
+ bool m_IsRunning;
+
QWidget *m_ParentWidget;
QString m_ModsDirectory;
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp
index 9066741d..1c65f635 100644
--- a/src/instancemanager.cpp
+++ b/src/instancemanager.cpp
@@ -86,7 +86,7 @@ bool InstanceManager::deleteLocalInstance(const QString &instanceId) const
if (!MOBase::shellDelete(QStringList(instancePath),true))
{
- qWarning("Failed to shell-delete \"%s\" (errorcode %lu), trying regular delete", qPrintable(instancePath), ::GetLastError());
+ qWarning("Failed to shell-delete \"%s\" (errorcode %lu), trying regular delete", qUtf8Printable(instancePath), ::GetLastError());
if (!MOBase::removeDir(instancePath))
{
qWarning("regular delete failed too");
@@ -138,12 +138,13 @@ QString InstanceManager::queryInstanceName(const QStringList &instanceList) cons
dialog.setWindowTitle(QObject::tr("Enter a Name for the new Instance"));
dialog.setLabelText(QObject::tr("Enter a new name or select one from the suggested list: \n"
- "(This is just the name of the Instance, not the game selection)"));
+ "(This is just a name for the Instance and can be whatever you wish,\n"
+ " the actual game selection will happen on the next screen regardless of chosen name)"));
// would be neat if we could take the names from the game plugins but
// the required initialization order requires the ini file to be
// available *before* we load plugins
dialog.setComboBoxItems({ "NewName", "Fallout 4", "SkyrimSE", "Skyrim", "SkyrimVR", "Fallout 3",
- "Fallout NV", "TTW", "FO4VR", "Oblivion", "Morrowind" });
+ "Fallout NV", "TTW", "FO4VR", "Oblivion", "Morrowind", "Enderal" });
dialog.setComboBoxEditable(true);
if (dialog.exec() == QDialog::Rejected) {
diff --git a/src/json.cpp b/src/json.cpp
deleted file mode 100644
index 05a2665a..00000000
--- a/src/json.cpp
+++ /dev/null
@@ -1,522 +0,0 @@
-/**
- * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and vice-versa.
- * Copyright (C) 2011 Eeli Reilin
- *
- * This program 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.
- *
- * 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. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * \file json.cpp
- */
-
-#include "json.h"
-
-namespace QtJson {
- static QString sanitizeString(QString str);
- static QByteArray join(const QList<QByteArray> &list, const QByteArray &sep);
- static QVariant parseValue(const QString &json, int &index, bool &success);
- static QVariant parseObject(const QString &json, int &index, bool &success);
- static QVariant parseArray(const QString &json, int &index, bool &success);
- static QVariant parseString(const QString &json, int &index, bool &success);
- static QVariant parseNumber(const QString &json, int &index);
- static int lastIndexOfNumber(const QString &json, int index);
- static void eatWhitespace(const QString &json, int &index);
- static int lookAhead(const QString &json, int index);
- static int nextToken(const QString &json, int &index);
-
- template<typename T>
- QByteArray serializeMap(const T &map, bool &success) {
- QByteArray str = "{ ";
- QList<QByteArray> pairs;
- for (typename T::const_iterator it = map.begin(), itend = map.end(); it != itend; ++it) {
- QByteArray serializedValue = serialize(it.value());
- if (serializedValue.isNull()) {
- success = false;
- break;
- }
- pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue;
- }
-
- str += join(pairs, ", ");
- str += " }";
- return str;
- }
-
-
- /**
- * parse
- */
- QVariant parse(const QString &json) {
- bool success = true;
- return parse(json, success);
- }
-
- /**
- * parse
- */
- QVariant parse(const QString &json, bool &success) {
- success = true;
-
- // Return an empty QVariant if the JSON data is either null or empty
- if (!json.isNull() || !json.isEmpty()) {
- QString data = json;
- // We'll start from index 0
- int index = 0;
-
- // Parse the first value
- QVariant value = parseValue(data, index, success);
-
- // Return the parsed value
- return value;
- } else {
- // Return the empty QVariant
- return QVariant();
- }
- }
-
- QByteArray serialize(const QVariant &data) {
- bool success = true;
- return serialize(data, success);
- }
-
- QByteArray serialize(const QVariant &data, bool &success) {
- QByteArray str;
- success = true;
-
- if (!data.isValid()) { // invalid or null?
- str = "null";
- } else if ((data.type() == QVariant::List) ||
- (data.type() == QVariant::StringList)) { // variant is a list?
- QList<QByteArray> values;
- const QVariantList list = data.toList();
- Q_FOREACH(const QVariant& v, list) {
- QByteArray serializedValue = serialize(v);
- if (serializedValue.isNull()) {
- success = false;
- break;
- }
- values << serializedValue;
- }
-
- str = "[ " + join( values, ", " ) + " ]";
- } else if (data.type() == QVariant::Hash) { // variant is a hash?
- str = serializeMap<>(data.toHash(), success);
- } else if (data.type() == QVariant::Map) { // variant is a map?
- str = serializeMap<>(data.toMap(), success);
- } else if ((data.type() == QVariant::String) ||
- (data.type() == QVariant::ByteArray)) {// a string or a byte array?
- str = sanitizeString(data.toString()).toUtf8();
- } else if (data.type() == QVariant::Double) { // double?
- double value = data.toDouble();
- if ((value - value) == 0.0) {
- str = QByteArray::number(value, 'g');
- if (!str.contains(".") && ! str.contains("e")) {
- str += ".0";
- }
- } else {
- success = false;
- }
- } else if (data.type() == QVariant::Bool) { // boolean value?
- str = data.toBool() ? "true" : "false";
- } else if (data.type() == QVariant::ULongLong) { // large unsigned number?
- str = QByteArray::number(data.value<qulonglong>());
- } else if (data.canConvert<qlonglong>()) { // any signed number?
- str = QByteArray::number(data.value<qlonglong>());
- } else if (data.canConvert<long>()) { //TODO: this code is never executed
- str = QString::number(data.value<long>()).toUtf8();
- } else if (data.canConvert<QString>()) { // can value be converted to string?
- // this will catch QDate, QDateTime, QUrl, ...
- str = sanitizeString(data.toString()).toUtf8();
- } else {
- success = false;
- }
-
- if (success) {
- return str;
- } else {
- return QByteArray();
- }
- }
-
- QString serializeStr(const QVariant &data) {
- return QString::fromUtf8(serialize(data));
- }
-
- QString serializeStr(const QVariant &data, bool &success) {
- return QString::fromUtf8(serialize(data, success));
- }
-
-
- /**
- * \enum JsonToken
- */
- enum JsonToken {
- JsonTokenNone = 0,
- JsonTokenCurlyOpen = 1,
- JsonTokenCurlyClose = 2,
- JsonTokenSquaredOpen = 3,
- JsonTokenSquaredClose = 4,
- JsonTokenColon = 5,
- JsonTokenComma = 6,
- JsonTokenString = 7,
- JsonTokenNumber = 8,
- JsonTokenTrue = 9,
- JsonTokenFalse = 10,
- JsonTokenNull = 11
- };
-
- static QString sanitizeString(QString str) {
- str.replace(QLatin1String("\\"), QLatin1String("\\\\"));
- str.replace(QLatin1String("\""), QLatin1String("\\\""));
- str.replace(QLatin1String("\b"), QLatin1String("\\b"));
- str.replace(QLatin1String("\f"), QLatin1String("\\f"));
- str.replace(QLatin1String("\n"), QLatin1String("\\n"));
- str.replace(QLatin1String("\r"), QLatin1String("\\r"));
- str.replace(QLatin1String("\t"), QLatin1String("\\t"));
- return QString(QLatin1String("\"%1\"")).arg(str);
- }
-
- static QByteArray join(const QList<QByteArray> &list, const QByteArray &sep) {
- QByteArray res;
- Q_FOREACH(const QByteArray &i, list) {
- if (!res.isEmpty()) {
- res += sep;
- }
- res += i;
- }
- return res;
- }
-
- /**
- * parseValue
- */
- static QVariant parseValue(const QString &json, int &index, bool &success) {
- // Determine what kind of data we should parse by
- // checking out the upcoming token
- switch(lookAhead(json, index)) {
- case JsonTokenString:
- return parseString(json, index, success);
- case JsonTokenNumber:
- return parseNumber(json, index);
- case JsonTokenCurlyOpen:
- return parseObject(json, index, success);
- case JsonTokenSquaredOpen:
- return parseArray(json, index, success);
- case JsonTokenTrue:
- nextToken(json, index);
- return QVariant(true);
- case JsonTokenFalse:
- nextToken(json, index);
- return QVariant(false);
- case JsonTokenNull:
- nextToken(json, index);
- return QVariant();
- case JsonTokenNone:
- break;
- }
-
- // If there were no tokens, flag the failure and return an empty QVariant
- success = false;
- return QVariant();
- }
-
- /**
- * parseObject
- */
- static QVariant parseObject(const QString &json, int &index, bool &success) {
- QVariantMap map;
- int token;
-
- // Get rid of the whitespace and increment index
- nextToken(json, index);
-
- // Loop through all of the key/value pairs of the object
- bool done = false;
- while (!done) {
- // Get the upcoming token
- token = lookAhead(json, index);
-
- if (token == JsonTokenNone) {
- success = false;
- return QVariantMap();
- } else if (token == JsonTokenComma) {
- nextToken(json, index);
- } else if (token == JsonTokenCurlyClose) {
- nextToken(json, index);
- return map;
- } else {
- // Parse the key/value pair's name
- QString name = parseString(json, index, success).toString();
-
- if (!success) {
- return QVariantMap();
- }
-
- // Get the next token
- token = nextToken(json, index);
-
- // If the next token is not a colon, flag the failure
- // return an empty QVariant
- if (token != JsonTokenColon) {
- success = false;
- return QVariant(QVariantMap());
- }
-
- // Parse the key/value pair's value
- QVariant value = parseValue(json, index, success);
-
- if (!success) {
- return QVariantMap();
- }
-
- // Assign the value to the key in the map
- map[name] = value;
- }
- }
-
- // Return the map successfully
- return QVariant(map);
- }
-
- /**
- * parseArray
- */
- static QVariant parseArray(const QString &json, int &index, bool &success) {
- QVariantList list;
-
- nextToken(json, index);
-
- bool done = false;
- while(!done) {
- int token = lookAhead(json, index);
-
- if (token == JsonTokenNone) {
- success = false;
- return QVariantList();
- } else if (token == JsonTokenComma) {
- nextToken(json, index);
- } else if (token == JsonTokenSquaredClose) {
- nextToken(json, index);
- break;
- } else {
- QVariant value = parseValue(json, index, success);
- if (!success) {
- return QVariantList();
- }
- list.push_back(value);
- }
- }
-
- return QVariant(list);
- }
-
- /**
- * parseString
- */
- static QVariant parseString(const QString &json, int &index, bool &success) {
- QString s;
- QChar c;
-
- eatWhitespace(json, index);
-
- c = json[index++];
-
- bool complete = false;
- while(!complete) {
- if (index == json.size()) {
- break;
- }
-
- c = json[index++];
-
- if (c == '\"') {
- complete = true;
- break;
- } else if (c == '\\') {
- if (index == json.size()) {
- break;
- }
-
- c = json[index++];
-
- if (c == '\"') {
- s.append('\"');
- } else if (c == '\\') {
- s.append('\\');
- } else if (c == '/') {
- s.append('/');
- } else if (c == 'b') {
- s.append('\b');
- } else if (c == 'f') {
- s.append('\f');
- } else if (c == 'n') {
- s.append('\n');
- } else if (c == 'r') {
- s.append('\r');
- } else if (c == 't') {
- s.append('\t');
- } else if (c == 'u') {
- int remainingLength = json.size() - index;
- if (remainingLength >= 4) {
- QString unicodeStr = json.mid(index, 4);
-
- int symbol = unicodeStr.toInt(0, 16);
-
- s.append(QChar(symbol));
-
- index += 4;
- } else {
- break;
- }
- }
- } else {
- s.append(c);
- }
- }
-
- if (!complete) {
- success = false;
- return QVariant();
- }
-
- return QVariant(s);
- }
-
- /**
- * parseNumber
- */
- static QVariant parseNumber(const QString &json, int &index) {
- eatWhitespace(json, index);
-
- int lastIndex = lastIndexOfNumber(json, index);
- int charLength = (lastIndex - index) + 1;
- QString numberStr;
-
- numberStr = json.mid(index, charLength);
-
- index = lastIndex + 1;
- bool ok;
-
- if (numberStr.contains('.')) {
- return QVariant(numberStr.toDouble(nullptr));
- } else if (numberStr.startsWith('-')) {
- int i = numberStr.toInt(&ok);
- if (!ok) {
- qlonglong ll = numberStr.toLongLong(&ok);
- return ok ? ll : QVariant(numberStr);
- }
- return i;
- } else {
- uint u = numberStr.toUInt(&ok);
- if (!ok) {
- qulonglong ull = numberStr.toULongLong(&ok);
- return ok ? ull : QVariant(numberStr);
- }
- return u;
- }
- }
-
- /**
- * lastIndexOfNumber
- */
- static int lastIndexOfNumber(const QString &json, int index) {
- int lastIndex;
-
- for(lastIndex = index; lastIndex < json.size(); lastIndex++) {
- if (QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) {
- break;
- }
- }
-
- return lastIndex -1;
- }
-
- /**
- * eatWhitespace
- */
- static void eatWhitespace(const QString &json, int &index) {
- for(; index < json.size(); index++) {
- if (QString(" \t\n\r").indexOf(json[index]) == -1) {
- break;
- }
- }
- }
-
- /**
- * lookAhead
- */
- static int lookAhead(const QString &json, int index) {
- int saveIndex = index;
- return nextToken(json, saveIndex);
- }
-
- /**
- * nextToken
- */
- static int nextToken(const QString &json, int &index) {
- eatWhitespace(json, index);
-
- if (index == json.size()) {
- return JsonTokenNone;
- }
-
- QChar c = json[index];
- index++;
- switch(c.toLatin1()) {
- case '{': return JsonTokenCurlyOpen;
- case '}': return JsonTokenCurlyClose;
- case '[': return JsonTokenSquaredOpen;
- case ']': return JsonTokenSquaredClose;
- case ',': return JsonTokenComma;
- case '"': return JsonTokenString;
- case '0': case '1': case '2': case '3': case '4':
- case '5': case '6': case '7': case '8': case '9':
- case '-': return JsonTokenNumber;
- case ':': return JsonTokenColon;
- }
- index--; // ^ WTF?
-
- int remainingLength = json.size() - index;
-
- // True
- if (remainingLength >= 4) {
- if (json[index] == 't' && json[index + 1] == 'r' &&
- json[index + 2] == 'u' && json[index + 3] == 'e') {
- index += 4;
- return JsonTokenTrue;
- }
- }
-
- // False
- if (remainingLength >= 5) {
- if (json[index] == 'f' && json[index + 1] == 'a' &&
- json[index + 2] == 'l' && json[index + 3] == 's' &&
- json[index + 4] == 'e') {
- index += 5;
- return JsonTokenFalse;
- }
- }
-
- // Null
- if (remainingLength >= 4) {
- if (json[index] == 'n' && json[index + 1] == 'u' &&
- json[index + 2] == 'l' && json[index + 3] == 'l') {
- index += 4;
- return JsonTokenNull;
- }
- }
-
- return JsonTokenNone;
- }
-} //end namespace
diff --git a/src/json.h b/src/json.h
deleted file mode 100644
index 57842e37..00000000
--- a/src/json.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and vice-versa.
- * Copyright (C) 2011 Eeli Reilin
- *
- * This program 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.
- *
- * 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. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * \file json.h
- */
-
-#ifndef JSON_H
-#define JSON_H
-
-#include <QVariant>
-#include <QString>
-
-
-/**
- * \namespace QtJson
- * \brief A JSON data parser
- *
- * Json parses a JSON data into a QVariant hierarchy.
- */
-namespace QtJson {
- typedef QVariantMap JsonObject;
- typedef QVariantList JsonArray;
-
- /**
- * Parse a JSON string
- *
- * \param json The JSON data
- */
- QVariant parse(const QString &json);
-
- /**
- * Parse a JSON string
- *
- * \param json The JSON data
- * \param success The success of the parsing
- */
- QVariant parse(const QString &json, bool &success);
-
- /**
- * This method generates a textual JSON representation
- *
- * \param data The JSON data generated by the parser.
- *
- * \return QByteArray Textual JSON representation in UTF-8
- */
- QByteArray serialize(const QVariant &data);
-
- /**
- * This method generates a textual JSON representation
- *
- * \param data The JSON data generated by the parser.
- * \param success The success of the serialization
- *
- * \return QByteArray Textual JSON representation in UTF-8
- */
- QByteArray serialize(const QVariant &data, bool &success);
-
- /**
- * This method generates a textual JSON representation
- *
- * \param data The JSON data generated by the parser.
- *
- * \return QString Textual JSON representation
- */
- QString serializeStr(const QVariant &data);
-
- /**
- * This method generates a textual JSON representation
- *
- * \param data The JSON data generated by the parser.
- * \param success The success of the serialization
- *
- * \return QString Textual JSON representation
- */
- QString serializeStr(const QVariant &data, bool &success);
-}
-
-#endif //JSON_H
diff --git a/src/lcdnumber.cpp b/src/lcdnumber.cpp
new file mode 100644
index 00000000..3191b434
--- /dev/null
+++ b/src/lcdnumber.cpp
@@ -0,0 +1,37 @@
+/*
+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 "lcdnumber.h"
+
+#include <QToolTip>
+#include <QTimer>
+
+LCDNumber::LCDNumber(QWidget *parent)
+ : QLCDNumber(parent)
+{
+}
+
+void LCDNumber::mousePressEvent(QMouseEvent *event)
+{
+ m_toolTipPosition = mapToGlobal(event->pos());
+ QTimer::singleShot(100, this, SLOT(showToolTip()));
+}
+
+void LCDNumber::showToolTip()
+{
+ QToolTip::showText(m_toolTipPosition, toolTip());
+}
diff --git a/src/lcdnumber.h b/src/lcdnumber.h
new file mode 100644
index 00000000..2f8e0454
--- /dev/null
+++ b/src/lcdnumber.h
@@ -0,0 +1,35 @@
+/*
+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 <QLCDNumber>
+#include <QMouseEvent>
+#include <QWidget>
+
+class LCDNumber : public QLCDNumber
+{
+ Q_OBJECT
+
+public:
+ LCDNumber(QWidget *parent = nullptr);
+ void mousePressEvent(QMouseEvent *event);
+
+public slots:
+ void showToolTip();
+
+private:
+ QPoint m_toolTipPosition;
+};
diff --git a/src/loadmechanism.cpp b/src/loadmechanism.cpp
index ebbb5de3..8f0529ce 100644
--- a/src/loadmechanism.cpp
+++ b/src/loadmechanism.cpp
@@ -100,7 +100,7 @@ bool LoadMechanism::hashIdentical(const QString &fileNameLHS, const QString &fil
{
QFile fileLHS(fileNameLHS);
if (!fileLHS.open(QIODevice::ReadOnly)) {
- throw MyException(QObject::tr("%1 not found").arg(fileNameLHS));
+ throw MyException(QObject::tr("file not found: %1").arg(qUtf8Printable(fileNameLHS)));
}
QByteArray dataLHS = fileLHS.readAll();
QByteArray hashLHS = QCryptographicHash::hash(dataLHS, QCryptographicHash::Md5);
@@ -109,7 +109,7 @@ bool LoadMechanism::hashIdentical(const QString &fileNameLHS, const QString &fil
QFile fileRHS(fileNameRHS);
if (!fileRHS.open(QIODevice::ReadOnly)) {
- throw MyException(QObject::tr("%1 not found").arg(fileNameRHS));
+ throw MyException(QObject::tr("file not found: %1").arg(qUtf8Printable(fileNameRHS)));
}
QByteArray dataRHS = fileRHS.readAll();
QByteArray hashRHS = QCryptographicHash::hash(dataRHS, QCryptographicHash::Md5);
diff --git a/src/logbuffer.cpp b/src/logbuffer.cpp
index 522ce3c8..7314624f 100644
--- a/src/logbuffer.cpp
+++ b/src/logbuffer.cpp
@@ -51,21 +51,24 @@ LogBuffer::~LogBuffer()
void LogBuffer::logMessage(QtMsgType type, const QString &message)
{
if (type >= m_MinMsgType) {
- Message msg = {type, QTime::currentTime(), message};
- if (m_NumMessages < m_Messages.size()) {
- beginInsertRows(QModelIndex(), static_cast<int>(m_NumMessages),
- static_cast<int>(m_NumMessages) + 1);
- }
- m_Messages.at(m_NumMessages % m_Messages.size()) = msg;
- if (m_NumMessages < m_Messages.size()) {
- endInsertRows();
- } else {
- emit dataChanged(createIndex(0, 0),
- createIndex(static_cast<int>(m_Messages.size()), 0));
- }
- ++m_NumMessages;
- if (type >= QtCriticalMsg) {
- write();
+ QStringList messagelist = message.split("\n");
+ for (auto split_message : messagelist) {
+ Message msg = {type, QTime::currentTime(), split_message};
+ if (m_NumMessages < m_Messages.size()) {
+ beginInsertRows(QModelIndex(), static_cast<int>(m_NumMessages),
+ static_cast<int>(m_NumMessages) + 1);
+ }
+ m_Messages.at(m_NumMessages % m_Messages.size()) = msg;
+ if (m_NumMessages < m_Messages.size()) {
+ endInsertRows();
+ } else {
+ emit dataChanged(createIndex(0, 0),
+ createIndex(static_cast<int>(m_Messages.size()), 0));
+ }
+ ++m_NumMessages;
+ if (type >= QtCriticalMsg) {
+ write();
+ }
}
}
}
@@ -111,6 +114,8 @@ char LogBuffer::msgTypeID(QtMsgType type)
switch (type) {
case QtDebugMsg:
return 'D';
+ case QtInfoMsg:
+ return 'I';
case QtWarningMsg:
return 'W';
case QtCriticalMsg:
@@ -127,7 +132,7 @@ void LogBuffer::log(QtMsgType type, const QMessageLogContext &context,
{
// QMutexLocker doesn't support timeout...
if (!s_Mutex.tryLock(100)) {
- fprintf(stderr, "failed to log: %s", qPrintable(message));
+ fprintf(stderr, "failed to log: %s", qUtf8Printable(message));
return;
}
ON_BLOCK_EXIT([]() { s_Mutex.unlock(); });
@@ -137,17 +142,17 @@ void LogBuffer::log(QtMsgType type, const QMessageLogContext &context,
}
if (type == QtDebugMsg) {
- fprintf(stdout, "%s [%c] %s\n", qPrintable(QTime::currentTime().toString()),
- msgTypeID(type), qPrintable(message));
+ fprintf(stdout, "%s [%c] %s\n", qUtf8Printable(QTime::currentTime().toString()),
+ msgTypeID(type), qUtf8Printable(message));
} else {
if (context.line != 0) {
fprintf(stdout, "%s [%c] (%s:%u) %s\n",
- qPrintable(QTime::currentTime().toString()), msgTypeID(type),
- context.file, context.line, qPrintable(message));
+ qUtf8Printable(QTime::currentTime().toString()), msgTypeID(type),
+ context.file, context.line, qUtf8Printable(message));
} else {
fprintf(stdout, "%s [%c] %s\n",
- qPrintable(QTime::currentTime().toString()), msgTypeID(type),
- qPrintable(message));
+ qUtf8Printable(QTime::currentTime().toString()), msgTypeID(type),
+ qUtf8Printable(message));
}
}
fflush(stdout);
@@ -200,6 +205,7 @@ QVariant LogBuffer::data(const QModelIndex &index, int role) const
if (index.column() == 1) {
switch (m_Messages[msgIndex].type) {
case QtDebugMsg:
+ case QtInfoMsg:
return QIcon(":/MO/gui/information");
case QtWarningMsg:
return QIcon(":/MO/gui/warning");
@@ -215,6 +221,8 @@ QVariant LogBuffer::data(const QModelIndex &index, int role) const
switch (m_Messages[msgIndex].type) {
case QtDebugMsg:
return "D";
+ case QtInfoMsg:
+ return "I";
case QtWarningMsg:
return "W";
case QtCriticalMsg:
diff --git a/src/main.cpp b/src/main.cpp
index 4d2bb7ed..c012b037 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,720 +1,724 @@
-/*
-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/>.
-*/
-
-
-#ifdef LEAK_CHECK_WITH_VLD
-#include <wchar.h>
-#include <vld.h>
-#endif // LEAK_CHECK_WITH_VLD
-
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <DbgHelp.h>
-
-#include <appconfig.h>
-#include <utility.h>
-#include <scopeguard.h>
-#include "mainwindow.h"
-#include <report.h>
-#include "modlist.h"
-#include "profile.h"
-#include "spawn.h"
-#include "executableslist.h"
-#include "singleinstance.h"
-#include "utility.h"
-#include "helper.h"
-#include "logbuffer.h"
-#include "selectiondialog.h"
-#include "moapplication.h"
-#include "tutorialmanager.h"
-#include "nxmaccessmanager.h"
-#include "instancemanager.h"
-#include "moshortcut.h"
-#include "organizercore.h"
-
-#include <eh.h>
-#include <windows_error.h>
-#include <usvfs.h>
-
-#include <QApplication>
-#include <QPushButton>
-#include <QListWidget>
-#include <QComboBox>
-#include <QCheckBox>
-#include <QDir>
-#include <QFileInfo>
-#include <QSettings>
-#include <QWhatsThis>
-#include <QToolBar>
-#include <QFileDialog>
-#include <QDesktopServices>
-#include <QMessageBox>
-#include <QSharedMemory>
-#include <QBuffer>
-#include <QSplashScreen>
-#include <QDirIterator>
-#include <QDesktopServices>
-#include <QLibraryInfo>
-#include <QSslSocket>
-#include <QtPlatformHeaders/QWindowsWindowFunctions>
-
-#include <boost/scoped_array.hpp>
-
-#include <ShellAPI.h>
-
-#include <cstdarg>
-#include <iostream>
-#include <sstream>
-#include <stdexcept>
-
-
-#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"")
-
-
-using namespace MOBase;
-using namespace MOShared;
-
-bool createAndMakeWritable(const std::wstring &subPath) {
- QString const dataPath = qApp->property("dataPath").toString();
- QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
-
- if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) {
- QMessageBox::critical(nullptr, QObject::tr("Error"),
- QObject::tr("Failed to create \"%1\". Your user "
- "account probably lacks permission.")
- .arg(fullPath));
- return false;
- } else {
- return true;
- }
-}
-
-bool bootstrap()
-{
- // remove the temporary backup directory in case we're restarting after an update
- QString backupDirectory = qApp->applicationDirPath() + "/update_backup";
- if (QDir(backupDirectory).exists()) {
- shellDelete(QStringList(backupDirectory));
- }
-
- // cycle logfile
- removeOldFiles(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()),
- "usvfs*.log", 5, QDir::Name);
-
- if (!createAndMakeWritable(AppConfig::logPath())) {
- return false;
- }
-
- return true;
-}
-
-LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr;
-
-static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs)
-{
- const std::wstring& dumpPath = OrganizerCore::crashDumpsPath();
- int dumpRes =
- CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str());
- if (!dumpRes)
- qCritical("ModOrganizer has crashed, crash dump created.");
- else
- qCritical("ModOrganizer has crashed, CreateMiniDump failed (%d, error %lu).", dumpRes, GetLastError());
-
- if (prevUnhandledExceptionFilter)
- return prevUnhandledExceptionFilter(exceptionPtrs);
- else
- return EXCEPTION_CONTINUE_SEARCH;
-}
-
-// Parses the first parseArgCount arguments of the current process command line and returns
-// them in parsedArgs, the rest of the command line is returned untouched.
-LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector<std::wstring>& parsedArgs)
-{
- LPCWSTR cmd = GetCommandLineW();
- LPCWSTR arg = nullptr; // to skip executable name
- for (; parseArgCount >= 0 && *cmd; ++cmd)
- {
- if (*cmd == '"') {
- int escaped = 0;
- for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd)
- escaped = *cmd == '\\' ? escaped + 1 : 0;
- }
- if (*cmd == ' ') {
- if (arg)
- if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"')
- parsedArgs.push_back(std::wstring(arg+1, cmd-1));
- else
- parsedArgs.push_back(std::wstring(arg, cmd));
- arg = cmd + 1;
- --parseArgCount;
- }
- }
- return cmd;
-}
-
-static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) {
- PROCESS_INFORMATION pi{ 0 };
- STARTUPINFO si{ 0 };
- si.cb = sizeof(si);
- std::wstring commandLineCopy = commandLine;
-
- if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) {
- // A bit of a problem where to log the error message here, at least this way you can get the message
- // using a either DebugView or a live debugger:
- std::wostringstream ost;
- ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError();
- OutputDebugStringW(ost.str().c_str());
- return -1;
- }
-
- WaitForSingleObject(pi.hProcess, INFINITE);
-
- DWORD exitCode = (DWORD)-1;
- ::GetExitCodeProcess(pi.hProcess, &exitCode);
- CloseHandle(pi.hThread);
- CloseHandle(pi.hProcess);
- return static_cast<int>(exitCode);
-}
-
-static DWORD WaitForProcess() {
-
-}
-
-static bool HaveWriteAccess(const std::wstring &path)
-{
- bool writable = false;
-
- const static SECURITY_INFORMATION requestedFileInformation = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
-
- DWORD length = 0;
- if (!::GetFileSecurityW(path.c_str(), requestedFileInformation, nullptr, 0UL, &length)
- && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)) {
- std::string tempBuffer;
- tempBuffer.reserve(length);
- PSECURITY_DESCRIPTOR security = (PSECURITY_DESCRIPTOR)tempBuffer.data();
- if (security
- && ::GetFileSecurity(path.c_str(), requestedFileInformation, security, length, &length)) {
- HANDLE token = nullptr;
- const static DWORD tokenDesiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ;
- if (!::OpenThreadToken(::GetCurrentThread(), tokenDesiredAccess, TRUE, &token)) {
- if (!::OpenProcessToken(::GetCurrentProcess(), tokenDesiredAccess, &token)) {
- throw std::runtime_error("Unable to get any thread or process token");
- }
- }
-
- HANDLE impersonatedToken = nullptr;
- if (::DuplicateToken(token, SecurityImpersonation, &impersonatedToken)) {
- GENERIC_MAPPING mapping = { 0xFFFFFFFF };
- mapping.GenericRead = FILE_GENERIC_READ;
- mapping.GenericWrite = FILE_GENERIC_WRITE;
- mapping.GenericExecute = FILE_GENERIC_EXECUTE;
- mapping.GenericAll = FILE_ALL_ACCESS;
-
- DWORD genericAccessRights = FILE_GENERIC_WRITE;
- ::MapGenericMask(&genericAccessRights, &mapping);
-
- PRIVILEGE_SET privileges = { 0 };
- DWORD grantedAccess = 0;
- DWORD privilegesLength = sizeof(privileges);
- BOOL result = 0;
- if (::AccessCheck(security, impersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result)) {
- writable = result != 0;
- }
- ::CloseHandle(impersonatedToken);
- }
-
- ::CloseHandle(token);
- }
- }
- return writable;
-}
-
-
-QString determineProfile(QStringList &arguments, const QSettings &settings)
-{
- QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray());
- { // see if there is a profile on the command line
- int profileIndex = arguments.indexOf("-p", 1);
- if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) {
- qDebug("profile overwritten on command line");
- selectedProfileName = arguments.at(profileIndex + 1);
- }
- arguments.removeAt(profileIndex);
- arguments.removeAt(profileIndex);
- }
- if (selectedProfileName.isEmpty()) {
- qDebug("no configured profile");
- selectedProfileName = "Default";
- } else {
- qDebug("configured profile: %s", qPrintable(selectedProfileName));
- }
-
- return selectedProfileName;
-}
-
-MOBase::IPluginGame *selectGame(QSettings &settings, QDir const &gamePath, MOBase::IPluginGame *game)
-{
- settings.setValue("gameName", game->gameName());
- //Sadly, hookdll needs gamePath in order to run. So following code block is
- //commented out
- /*if (gamePath == game->gameDirectory()) {
- settings.remove("gamePath");
- } else*/ {
- QString gameDir = gamePath.absolutePath();
- game->setGamePath(gameDir);
- settings.setValue("gamePath", QDir::toNativeSeparators(gameDir).toUtf8().constData());
- }
- return game; //Woot
-}
-
-
-MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &settings, PluginContainer const &plugins)
-{
- //Determine what game we are running where. Be very paranoid in case the
- //user has done something odd.
- //If the game name has been set up, use that.
- QString gameName = settings.value("gameName", "").toString();
- if (!gameName.isEmpty()) {
- MOBase::IPluginGame *game = plugins.managedGame(gameName);
- if (game == nullptr) {
- reportError(QObject::tr("Plugin to handle %1 no longer installed").arg(gameName));
- return nullptr;
- }
- QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray());
- if (gamePath == "") {
- gamePath = game->gameDirectory().absolutePath();
- }
- QDir gameDir(gamePath);
- if (game->looksValid(gameDir)) {
- return selectGame(settings, gameDir, game);
- }
- }
-
- //gameName wasn't set, or otherwise can't be found. Try looking through all
- //the plugins using the gamePath
- QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray());
- if (!gamePath.isEmpty()) {
- QDir gameDir(gamePath);
- //Look to see if one of the installed games binary file exists in the current
- //game directory.
- for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
- if (game->looksValid(gameDir)) {
- return selectGame(settings, gameDir, game);
- }
- }
- }
-
- //The following code would try to determine the right game to mange but it would usually find the wrong one
- //so it was commented out.
- /*
- //OK, we are in a new setup or existing info is useless.
- //See if MO has been installed inside a game directory
- for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
- if (game->isInstalled() && moPath.startsWith(game->gameDirectory().absolutePath())) {
- //Found it.
- return selectGame(settings, game->gameDirectory(), game);
- }
- }
-
- //Try walking up the directory tree to see if MO has been installed inside a game
- {
- QDir gameDir(moPath);
- do {
- //Look to see if one of the installed games binary file exists in the current
- //directory.
- for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
- if (game->looksValid(gameDir)) {
- return selectGame(settings, gameDir, game);
- }
- }
- //OK, chop off the last directory and try again
- } while (gameDir.cdUp());
- }
- */
-
- //Then try a selection dialogue.
- if (!gamePath.isEmpty() || !gameName.isEmpty()) {
- reportError(QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\".").
- arg(gameName).arg(gamePath));
- }
-
- SelectionDialog selection(QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32));
-
- for (IPluginGame *game : plugins.plugins<IPluginGame>()) {
- if (game->isInstalled()) {
- QString path = game->gameDirectory().absolutePath();
- selection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game));
- }
- }
-
- selection.addChoice(QString("Browse..."), QString(), QVariant::fromValue(static_cast<IPluginGame *>(nullptr)));
-
- while (selection.exec() != QDialog::Rejected) {
- IPluginGame * game = selection.getChoiceData().value<IPluginGame *>();
- if (game != nullptr) {
- return selectGame(settings, game->gameDirectory(), game);
- }
-
- gamePath = QFileDialog::getExistingDirectory(
- nullptr, QObject::tr("Please select the game to manage"), QString(),
- QFileDialog::ShowDirsOnly);
-
- if (!gamePath.isEmpty()) {
- QDir gameDir(gamePath);
- for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
- if (game->looksValid(gameDir)) {
- return selectGame(settings, gameDir, game);
- }
- }
- reportError(QObject::tr("No game identified in \"%1\". The directory is required to contain "
- "the game binary.").arg(gamePath));
- }
- }
-
- return nullptr;
-}
-
-
-// extend path to include dll directory so plugins don't need a manifest
-// (using AddDllDirectory would be an alternative to this but it seems fairly
-// complicated esp.
-// since it isn't easily accessible on Windows < 8
-// SetDllDirectory replaces other search directories and this seems to
-// propagate to child processes)
-void setupPath()
-{
- static const int BUFSIZE = 4096;
-
- qDebug("MO at: %s", qPrintable(QDir::toNativeSeparators(
- QCoreApplication::applicationDirPath())));
-
- QCoreApplication::setLibraryPaths(QStringList(QCoreApplication::applicationDirPath() + "/dlls") + QCoreApplication::libraryPaths());
-
- boost::scoped_array<TCHAR> oldPath(new TCHAR[BUFSIZE]);
- DWORD offset = ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), BUFSIZE);
- if (offset > BUFSIZE) {
- oldPath.reset(new TCHAR[offset]);
- ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), offset);
- }
-
- std::wstring newPath(ToWString(QDir::toNativeSeparators(
- QCoreApplication::applicationDirPath())) + L"\\dlls");
- newPath += L";";
- newPath += oldPath.get();
-
- ::SetEnvironmentVariableW(L"PATH", newPath.c_str());
-}
-
-static void preloadSsl()
-{
- 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", qPrintable(libeay32));
- else if (!LoadLibraryW(libeay32.toStdWString().c_str()))
- qWarning("failed to load: %s, %d", qPrintable(libeay32), GetLastError());
- }
- 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", qPrintable(ssleay32));
- else if (!LoadLibraryW(ssleay32.toStdWString().c_str()))
- qWarning("failed to load: %s, %d", qPrintable(ssleay32), GetLastError());
- }
-}
-
-static QString getVersionDisplayString()
-{
- return createVersionInfo().displayString();
-}
-
-int runApplication(MOApplication &application, SingleInstance &instance,
- const QString &splashPath)
-{
-
- qDebug("Starting Mod Organizer version %s revision %s", qPrintable(getVersionDisplayString()),
-#if defined(HGID)
- HGID
-#elif defined(GITID)
- GITID
-#else
- "unknown"
-#endif
- );
-
-#if !defined(QT_NO_SSL)
- preloadSsl();
- qDebug("ssl support: %d", QSslSocket::supportsSsl());
-#else
- qDebug("non-ssl build");
-#endif
-
- QString dataPath = application.property("dataPath").toString();
- qDebug("data path: %s", qPrintable(dataPath));
-
- if (!bootstrap()) {
- reportError("failed to set up data paths");
- return 1;
- }
-
- QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow);
-
- QStringList arguments = application.arguments();
-
- try {
- qDebug("Working directory: %s", qPrintable(QDir::toNativeSeparators(QDir::currentPath())));
-
- QSettings settings(dataPath + "/"
- + QString::fromStdWString(AppConfig::iniFileName()),
- QSettings::IniFormat);
-
- // global crashDumpType sits in OrganizerCore to make a bit less ugly to update it when the settings are changed during runtime
- OrganizerCore::setGlobalCrashDumpsType(settings.value("Settings/crash_dumps_type", static_cast<int>(CrashDumpsType::Mini)).toInt());
-
- qDebug("Loaded settings:");
- settings.beginGroup("Settings");
- for (auto k : settings.allKeys())
- if (!k.contains("username") && !k.contains("password"))
- qDebug(" %s=%s", k.toUtf8().data(), settings.value(k).toString().toUtf8().data());
- settings.endGroup();
-
-
- qDebug("initializing core");
- OrganizerCore organizer(settings);
- if (!organizer.bootstrap()) {
- reportError("failed to set up data paths");
- return 1;
- }
- qDebug("initialize plugins");
- PluginContainer pluginContainer(&organizer);
- pluginContainer.loadPlugins();
-
- MOBase::IPluginGame *game = determineCurrentGame(
- application.applicationDirPath(), settings, pluginContainer);
- if (game == nullptr) {
- return 1;
- }
- if (splashPath.startsWith(':')) {
- // currently using MO splash, see if the plugin contains one
- QString pluginSplash
- = QString(":/%1/splash").arg(game->gameShortName());
- QImage image(pluginSplash);
- if (!image.isNull()) {
- image.save(dataPath + "/splash.png");
- } else {
- qDebug("no plugin splash");
- }
- }
-
- organizer.setManagedGame(game);
- organizer.createDefaultProfile();
-
- if (!settings.contains("game_edition")) {
- QStringList editions = game->gameVariants();
- if (editions.size() > 1) {
- SelectionDialog selection(
- QObject::tr("Please select the game edition you have (MO can't "
- "start the game correctly if this is set "
- "incorrectly!)"),
- nullptr);
- selection.setWindowFlag(Qt::WindowStaysOnTopHint, true);
- int index = 0;
- for (const QString &edition : editions) {
- selection.addChoice(edition, "", index++);
- }
- if (selection.exec() == QDialog::Rejected) {
- return 1;
- } else {
- settings.setValue("game_edition", selection.getChoiceString());
- }
- }
- }
- game->setGameVariant(settings.value("game_edition").toString());
-
- qDebug("managing game at %s", qPrintable(QDir::toNativeSeparators(
- game->gameDirectory().absolutePath())));
-
- organizer.updateExecutablesList(settings);
-
- QString selectedProfileName = determineProfile(arguments, settings);
- organizer.setCurrentProfile(selectedProfileName);
-
- // if we have a command line parameter, it is either a nxm link or
- // a binary to start
- if (arguments.size() > 1) {
- if (MOShortcut shortcut{ arguments.at(1) }) {
- if (shortcut.hasExecutable()) {
- try {
- organizer.runShortcut(shortcut);
- return 0;
- }
- catch (const std::exception &e) {
- reportError(
- QObject::tr("failed to start shortcut: %1").arg(e.what()));
- return 1;
- }
- }
- }
- else if (OrganizerCore::isNxmLink(arguments.at(1))) {
- qDebug("starting download from command line: %s",
- qPrintable(arguments.at(1)));
- organizer.externalMessage(arguments.at(1));
- }
- else {
- QString exeName = arguments.at(1);
- qDebug("starting %s from command line", qPrintable(exeName));
- arguments.removeFirst(); // remove application name (ModOrganizer.exe)
- arguments.removeFirst(); // remove binary name
- // pass the remaining parameters to the binary
- try {
- organizer.startApplication(exeName, arguments, QString(), QString());
- return 0;
- }
- catch (const std::exception &e) {
- reportError(
- QObject::tr("failed to start application: %1").arg(e.what()));
- return 1;
- }
- }
- }
-
- QPixmap pixmap(splashPath);
- QSplashScreen splash(pixmap);
- splash.show();
- splash.activateWindow();
-
- NexusInterface::instance(&pluginContainer)->getAccessManager()->startLoginCheck();
-
- qDebug("initializing tutorials");
- TutorialManager::init(
- qApp->applicationDirPath() + "/"
- + QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
- &organizer);
-
- if (!application.setStyleFile(settings.value("Settings/style", "").toString())) {
- // disable invalid stylesheet
- settings.setValue("Settings/style", "");
- }
-
- int res = 1;
- { // scope to control lifetime of mainwindow
- // set up main window and its data structures
- MainWindow mainWindow(settings, organizer, pluginContainer);
-
- QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
- SLOT(setStyleFile(QString)));
- QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer,
- SLOT(externalMessage(QString)));
-
- mainWindow.processUpdates();
- mainWindow.readSettings();
-
- qDebug("displaying main window");
- mainWindow.show();
- mainWindow.activateWindow();
-
- splash.finish(&mainWindow);
- return application.exec();
- }
- } catch (const std::exception &e) {
- reportError(e.what());
- return 1;
- }
-}
-
-
-int main(int argc, char *argv[])
-{
- //Should allow for better scaling of ui with higher resolution displays
- QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
-
- if (argc >= 4) {
- std::vector<std::wstring> arg;
- auto args = UntouchedCommandLineArguments(2, arg);
- if (arg[0] == L"launch")
- return SpawnWaitProcess(arg[1].c_str(), args);
- }
-
- MOApplication application(argc, argv);
- QStringList arguments = application.arguments();
-
- setupPath();
-
- bool forcePrimary = false;
- if (arguments.contains("update")) {
- arguments.removeAll("update");
- forcePrimary = true;
- }
-
- MOShortcut moshortcut{ arguments.size() > 1 ? arguments.at(1) : "" };
-
- SingleInstance instance(forcePrimary);
- if (!instance.primaryInstance()) {
- if (moshortcut ||
- arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1)))
- {
- qDebug("not primary instance, sending shortcut/download message");
- instance.sendMessage(arguments.at(1));
- return 0;
- } else if (arguments.size() == 1) {
- QMessageBox::information(
- nullptr, QObject::tr("Mod Organizer"),
- QObject::tr("An instance of Mod Organizer is already running"));
- return 0;
- }
- } // we continue for the primary instance OR if MO was called with parameters
-
- do {
- QString dataPath;
-
- try {
- InstanceManager& instanceManager = InstanceManager::instance();
- if (moshortcut && moshortcut.hasInstance())
- instanceManager.overrideInstance(moshortcut.instance());
- dataPath = instanceManager.determineDataPath();
- } catch (const std::exception &e) {
- if (strcmp(e.what(),"Canceled"))
- QMessageBox::critical(nullptr, QObject::tr("Failed to set up instance"), e.what());
- return 1;
- }
- application.setProperty("dataPath", dataPath);
-
- // initialize dump collection only after "dataPath" since the crashes are stored under it
- prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
-
- LogBuffer::init(1000, QtDebugMsg, qApp->property("dataPath").toString() + "/logs/mo_interface.log");
-
- QString splash = dataPath + "/splash.png";
- if (!QFile::exists(dataPath + "/splash.png")) {
- splash = ":/MO/gui/splash";
- }
-
- int result = runApplication(application, instance, splash);
- if (result != INT_MAX) {
- return result;
- }
- argc = 1;
- moshortcut = MOShortcut("");
- } while (true);
-}
+/*
+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/>.
+*/
+
+
+#ifdef LEAK_CHECK_WITH_VLD
+#include <wchar.h>
+#include <vld.h>
+#endif // LEAK_CHECK_WITH_VLD
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <DbgHelp.h>
+
+#include <appconfig.h>
+#include <utility.h>
+#include <scopeguard.h>
+#include "mainwindow.h"
+#include <report.h>
+#include "modlist.h"
+#include "profile.h"
+#include "spawn.h"
+#include "executableslist.h"
+#include "singleinstance.h"
+#include "utility.h"
+#include "helper.h"
+#include "logbuffer.h"
+#include "selectiondialog.h"
+#include "moapplication.h"
+#include "tutorialmanager.h"
+#include "nxmaccessmanager.h"
+#include "instancemanager.h"
+#include "moshortcut.h"
+#include "organizercore.h"
+
+#include <eh.h>
+#include <windows_error.h>
+#include <usvfs.h>
+
+#include <QApplication>
+#include <QPushButton>
+#include <QListWidget>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QDir>
+#include <QFileInfo>
+#include <QSettings>
+#include <QWhatsThis>
+#include <QToolBar>
+#include <QFileDialog>
+#include <QDesktopServices>
+#include <QMessageBox>
+#include <QSharedMemory>
+#include <QBuffer>
+#include <QSplashScreen>
+#include <QDirIterator>
+#include <QDesktopServices>
+#include <QLibraryInfo>
+#include <QSslSocket>
+#include <QtPlatformHeaders/QWindowsWindowFunctions>
+
+#include <boost/scoped_array.hpp>
+
+#include <ShellAPI.h>
+
+#include <cstdarg>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+
+
+#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"")
+
+
+using namespace MOBase;
+using namespace MOShared;
+
+bool createAndMakeWritable(const std::wstring &subPath) {
+ QString const dataPath = qApp->property("dataPath").toString();
+ QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
+
+ if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) {
+ QMessageBox::critical(nullptr, QObject::tr("Error"),
+ QObject::tr("Failed to create \"%1\". Your user "
+ "account probably lacks permission.")
+ .arg(fullPath));
+ return false;
+ } else {
+ return true;
+ }
+}
+
+bool bootstrap()
+{
+ // remove the temporary backup directory in case we're restarting after an update
+ QString backupDirectory = qApp->applicationDirPath() + "/update_backup";
+ if (QDir(backupDirectory).exists()) {
+ shellDelete(QStringList(backupDirectory));
+ }
+
+ // cycle logfile
+ removeOldFiles(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()),
+ "usvfs*.log", 5, QDir::Name);
+
+ if (!createAndMakeWritable(AppConfig::logPath())) {
+ return false;
+ }
+
+ return true;
+}
+
+LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr;
+
+static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs)
+{
+ const std::wstring& dumpPath = OrganizerCore::crashDumpsPath();
+ int dumpRes =
+ CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str());
+ if (!dumpRes)
+ qCritical("ModOrganizer has crashed, crash dump created.");
+ else
+ qCritical("ModOrganizer has crashed, CreateMiniDump failed (%d, error %lu).", dumpRes, GetLastError());
+
+ if (prevUnhandledExceptionFilter)
+ return prevUnhandledExceptionFilter(exceptionPtrs);
+ else
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+
+// Parses the first parseArgCount arguments of the current process command line and returns
+// them in parsedArgs, the rest of the command line is returned untouched.
+LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector<std::wstring>& parsedArgs)
+{
+ LPCWSTR cmd = GetCommandLineW();
+ LPCWSTR arg = nullptr; // to skip executable name
+ for (; parseArgCount >= 0 && *cmd; ++cmd)
+ {
+ if (*cmd == '"') {
+ int escaped = 0;
+ for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd)
+ escaped = *cmd == '\\' ? escaped + 1 : 0;
+ }
+ if (*cmd == ' ') {
+ if (arg)
+ if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"')
+ parsedArgs.push_back(std::wstring(arg+1, cmd-1));
+ else
+ parsedArgs.push_back(std::wstring(arg, cmd));
+ arg = cmd + 1;
+ --parseArgCount;
+ }
+ }
+ return cmd;
+}
+
+static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) {
+ PROCESS_INFORMATION pi{ 0 };
+ STARTUPINFO si{ 0 };
+ si.cb = sizeof(si);
+ std::wstring commandLineCopy = commandLine;
+
+ if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) {
+ // A bit of a problem where to log the error message here, at least this way you can get the message
+ // using a either DebugView or a live debugger:
+ std::wostringstream ost;
+ ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError();
+ OutputDebugStringW(ost.str().c_str());
+ return -1;
+ }
+
+ WaitForSingleObject(pi.hProcess, INFINITE);
+
+ DWORD exitCode = (DWORD)-1;
+ ::GetExitCodeProcess(pi.hProcess, &exitCode);
+ CloseHandle(pi.hThread);
+ CloseHandle(pi.hProcess);
+ return static_cast<int>(exitCode);
+}
+
+static DWORD WaitForProcess() {
+
+}
+
+static bool HaveWriteAccess(const std::wstring &path)
+{
+ bool writable = false;
+
+ const static SECURITY_INFORMATION requestedFileInformation = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
+
+ DWORD length = 0;
+ if (!::GetFileSecurityW(path.c_str(), requestedFileInformation, nullptr, 0UL, &length)
+ && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)) {
+ std::string tempBuffer;
+ tempBuffer.reserve(length);
+ PSECURITY_DESCRIPTOR security = (PSECURITY_DESCRIPTOR)tempBuffer.data();
+ if (security
+ && ::GetFileSecurity(path.c_str(), requestedFileInformation, security, length, &length)) {
+ HANDLE token = nullptr;
+ const static DWORD tokenDesiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ;
+ if (!::OpenThreadToken(::GetCurrentThread(), tokenDesiredAccess, TRUE, &token)) {
+ if (!::OpenProcessToken(::GetCurrentProcess(), tokenDesiredAccess, &token)) {
+ throw std::runtime_error("Unable to get any thread or process token");
+ }
+ }
+
+ HANDLE impersonatedToken = nullptr;
+ if (::DuplicateToken(token, SecurityImpersonation, &impersonatedToken)) {
+ GENERIC_MAPPING mapping = { 0xFFFFFFFF };
+ mapping.GenericRead = FILE_GENERIC_READ;
+ mapping.GenericWrite = FILE_GENERIC_WRITE;
+ mapping.GenericExecute = FILE_GENERIC_EXECUTE;
+ mapping.GenericAll = FILE_ALL_ACCESS;
+
+ DWORD genericAccessRights = FILE_GENERIC_WRITE;
+ ::MapGenericMask(&genericAccessRights, &mapping);
+
+ PRIVILEGE_SET privileges = { 0 };
+ DWORD grantedAccess = 0;
+ DWORD privilegesLength = sizeof(privileges);
+ BOOL result = 0;
+ if (::AccessCheck(security, impersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result)) {
+ writable = result != 0;
+ }
+ ::CloseHandle(impersonatedToken);
+ }
+
+ ::CloseHandle(token);
+ }
+ }
+ return writable;
+}
+
+
+QString determineProfile(QStringList &arguments, const QSettings &settings)
+{
+ QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray());
+ { // see if there is a profile on the command line
+ int profileIndex = arguments.indexOf("-p", 1);
+ if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) {
+ qDebug("profile overwritten on command line");
+ selectedProfileName = arguments.at(profileIndex + 1);
+ }
+ arguments.removeAt(profileIndex);
+ arguments.removeAt(profileIndex);
+ }
+ if (selectedProfileName.isEmpty()) {
+ qDebug("no configured profile");
+ selectedProfileName = "Default";
+ } else {
+ qDebug("configured profile: %s", qUtf8Printable(selectedProfileName));
+ }
+
+ return selectedProfileName;
+}
+
+MOBase::IPluginGame *selectGame(QSettings &settings, QDir const &gamePath, MOBase::IPluginGame *game)
+{
+ settings.setValue("gameName", game->gameName());
+ //Sadly, hookdll needs gamePath in order to run. So following code block is
+ //commented out
+ /*if (gamePath == game->gameDirectory()) {
+ settings.remove("gamePath");
+ } else*/ {
+ QString gameDir = gamePath.absolutePath();
+ game->setGamePath(gameDir);
+ settings.setValue("gamePath", QDir::toNativeSeparators(gameDir).toUtf8().constData());
+ }
+ return game; //Woot
+}
+
+
+MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &settings, PluginContainer const &plugins)
+{
+ //Determine what game we are running where. Be very paranoid in case the
+ //user has done something odd.
+
+ //If the game name has been set up, try to use that.
+ QString gameName = settings.value("gameName", "").toString();
+ bool gameConfigured = !gameName.isEmpty();
+ if (gameConfigured) {
+ MOBase::IPluginGame *game = plugins.managedGame(gameName);
+ if (game == nullptr) {
+ reportError(QObject::tr("Plugin to handle %1 no longer installed").arg(gameName));
+ return nullptr;
+ }
+ QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray());
+ if (gamePath == "") {
+ gamePath = game->gameDirectory().absolutePath();
+ }
+ QDir gameDir(gamePath);
+ if (game->looksValid(gameDir)) {
+ return selectGame(settings, gameDir, game);
+ }
+ }
+
+ //If we've made it this far and the instance is already configured for a game, something has gone wrong.
+ //Tell the user about it.
+ if (gameConfigured) {
+ QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray());
+ reportError(QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\".").
+ arg(gameName).arg(gamePath));
+ }
+
+ SelectionDialog selection(gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName)
+ : QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32));
+
+ for (IPluginGame *game : plugins.plugins<IPluginGame>()) {
+ //If a game is already configured, skip any plugins that are not for that game
+ if (gameConfigured && gameName.compare(game->gameName(), Qt::CaseInsensitive) != 0)
+ continue;
+
+ //Only add games that are installed
+ if (game->isInstalled()) {
+ QString path = game->gameDirectory().absolutePath();
+ selection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game));
+ }
+ }
+
+ selection.addChoice(QString("Browse..."), QString(), QVariant::fromValue(static_cast<IPluginGame *>(nullptr)));
+
+ while (selection.exec() != QDialog::Rejected) {
+ IPluginGame * game = selection.getChoiceData().value<IPluginGame *>();
+ 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)
+ : QObject::tr("Please select the game to manage"),
+ QString(), QFileDialog::ShowDirsOnly);
+ if (!gamePath.isEmpty()) {
+ QDir gameDir(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
+ if (gameConfigured && gameName.compare(game->gameName(), Qt::CaseInsensitive) != 0)
+ continue;
+
+ //Only try plugins that look valid for this directory
+ if (game->looksValid(gameDir)) {
+ possibleGames.append(game);
+ }
+ }
+ if (possibleGames.count() > 1) {
+ SelectionDialog browseSelection(gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName)
+ : QObject::tr("Please select the game to manage"),
+ nullptr, QSize(32, 32));
+ for (IPluginGame *game : possibleGames) {
+ browseSelection.addChoice(game->gameIcon(), game->gameName(), gamePath, QVariant::fromValue(game));
+ }
+ if (browseSelection.exec() == QDialog::Accepted) {
+ return selectGame(settings, gameDir, browseSelection.getChoiceData().value<IPluginGame *>());
+ } else {
+ reportError(gameConfigured ? QObject::tr("Canceled finding %1 in \"%2\".").arg(gameName).arg(gamePath)
+ : QObject::tr("Canceled finding game in \"%1\".").arg(gamePath));
+ }
+ } 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));
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+
+// extend path to include dll directory so plugins don't need a manifest
+// (using AddDllDirectory would be an alternative to this but it seems fairly
+// complicated esp.
+// since it isn't easily accessible on Windows < 8
+// SetDllDirectory replaces other search directories and this seems to
+// propagate to child processes)
+void setupPath()
+{
+ static const int BUFSIZE = 4096;
+
+ qDebug("MO at: %s", qUtf8Printable(QDir::toNativeSeparators(
+ QCoreApplication::applicationDirPath())));
+
+ QCoreApplication::setLibraryPaths(QStringList(QCoreApplication::applicationDirPath() + "/dlls") + QCoreApplication::libraryPaths());
+
+ boost::scoped_array<TCHAR> oldPath(new TCHAR[BUFSIZE]);
+ DWORD offset = ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), BUFSIZE);
+ if (offset > BUFSIZE) {
+ oldPath.reset(new TCHAR[offset]);
+ ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), offset);
+ }
+
+ std::wstring newPath(ToWString(QDir::toNativeSeparators(
+ QCoreApplication::applicationDirPath())) + L"\\dlls");
+ newPath += L";";
+ newPath += oldPath.get();
+
+ ::SetEnvironmentVariableW(L"PATH", newPath.c_str());
+}
+
+static void preloadSsl()
+{
+ 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());
+ }
+ 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());
+ }
+}
+
+static QString getVersionDisplayString()
+{
+ return createVersionInfo().displayString(3);
+}
+
+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
+ );
+
+#if !defined(QT_NO_SSL)
+ preloadSsl();
+ qDebug("ssl support: %d", QSslSocket::supportsSsl());
+#else
+ qDebug("non-ssl build");
+#endif
+
+ QString dataPath = application.property("dataPath").toString();
+ qDebug("data path: %s", qUtf8Printable(dataPath));
+
+ if (!bootstrap()) {
+ reportError("failed to set up data paths");
+ return 1;
+ }
+
+ QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow);
+
+ QStringList arguments = application.arguments();
+
+ try {
+ qDebug("Working directory: %s", qUtf8Printable(QDir::toNativeSeparators(QDir::currentPath())));
+
+ QSettings settings(dataPath + "/"
+ + QString::fromStdWString(AppConfig::iniFileName()),
+ QSettings::IniFormat);
+
+ // global crashDumpType sits in OrganizerCore to make a bit less ugly to update it when the settings are changed during runtime
+ OrganizerCore::setGlobalCrashDumpsType(settings.value("Settings/crash_dumps_type", static_cast<int>(CrashDumpsType::Mini)).toInt());
+
+ qDebug("Loaded settings:");
+ settings.beginGroup("Settings");
+ for (auto k : settings.allKeys())
+ if (!k.contains("username") && !k.contains("password") && !k.contains("nexus_api_key"))
+ qDebug(" %s=%s", k.toUtf8().data(), settings.value(k).toString().toUtf8().data());
+ settings.endGroup();
+
+
+ qDebug("initializing core");
+ OrganizerCore organizer(settings);
+ if (!organizer.bootstrap()) {
+ reportError("failed to set up data paths");
+ return 1;
+ }
+ qDebug("initialize plugins");
+ PluginContainer pluginContainer(&organizer);
+ pluginContainer.loadPlugins();
+
+ MOBase::IPluginGame *game = determineCurrentGame(
+ application.applicationDirPath(), settings, pluginContainer);
+ if (game == nullptr) {
+ InstanceManager &instance = InstanceManager::instance();
+ QString instanceName = instance.currentInstance();
+ if (instanceName.compare("Portable", Qt::CaseInsensitive) != 0) {
+ instance.clearCurrentInstance();
+ return INT_MAX;
+ }
+ return 1;
+ }
+ if (splashPath.startsWith(':')) {
+ // currently using MO splash, see if the plugin contains one
+ QString pluginSplash
+ = QString(":/%1/splash").arg(game->gameShortName());
+ QImage image(pluginSplash);
+ if (!image.isNull()) {
+ image.save(dataPath + "/splash.png");
+ } else {
+ qDebug("no plugin splash");
+ }
+ }
+
+ organizer.setManagedGame(game);
+ organizer.createDefaultProfile();
+
+ if (!settings.contains("game_edition")) {
+ QStringList editions = game->gameVariants();
+ if (editions.size() > 1) {
+ SelectionDialog selection(
+ QObject::tr("Please select the game edition you have (MO can't "
+ "start the game correctly if this is set "
+ "incorrectly!)"),
+ nullptr);
+ selection.setWindowFlag(Qt::WindowStaysOnTopHint, true);
+ int index = 0;
+ for (const QString &edition : editions) {
+ selection.addChoice(edition, "", index++);
+ }
+ if (selection.exec() == QDialog::Rejected) {
+ return 1;
+ } else {
+ settings.setValue("game_edition", selection.getChoiceString());
+ }
+ }
+ }
+ game->setGameVariant(settings.value("game_edition").toString());
+
+ qDebug("managing game at %s", qUtf8Printable(QDir::toNativeSeparators(
+ game->gameDirectory().absolutePath())));
+
+ organizer.updateExecutablesList(settings);
+
+ QString selectedProfileName = determineProfile(arguments, settings);
+ organizer.setCurrentProfile(selectedProfileName);
+
+ // if we have a command line parameter, it is either a nxm link or
+ // a binary to start
+ if (arguments.size() > 1) {
+ if (MOShortcut shortcut{ arguments.at(1) }) {
+ if (shortcut.hasExecutable()) {
+ try {
+ organizer.runShortcut(shortcut);
+ return 0;
+ }
+ catch (const std::exception &e) {
+ reportError(
+ QObject::tr("failed to start shortcut: %1").arg(e.what()));
+ return 1;
+ }
+ }
+ }
+ else if (OrganizerCore::isNxmLink(arguments.at(1))) {
+ qDebug("starting download from command line: %s",
+ qUtf8Printable(arguments.at(1)));
+ organizer.externalMessage(arguments.at(1));
+ }
+ else {
+ QString exeName = arguments.at(1);
+ qDebug("starting %s from command line", qUtf8Printable(exeName));
+ arguments.removeFirst(); // remove application name (ModOrganizer.exe)
+ arguments.removeFirst(); // remove binary name
+ // pass the remaining parameters to the binary
+ try {
+ organizer.startApplication(exeName, arguments, QString(), QString());
+ return 0;
+ }
+ catch (const std::exception &e) {
+ reportError(
+ QObject::tr("failed to start application: %1").arg(e.what()));
+ return 1;
+ }
+ }
+ }
+
+ QPixmap pixmap(splashPath);
+ QSplashScreen splash(pixmap);
+ splash.show();
+ splash.activateWindow();
+
+ QString apiKey;
+ if (organizer.settings().getNexusApiKey(apiKey)) {
+ NexusInterface::instance(&pluginContainer)->getAccessManager()->apiCheck(apiKey);
+ }
+
+ qDebug("initializing tutorials");
+ TutorialManager::init(
+ qApp->applicationDirPath() + "/"
+ + QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
+ &organizer);
+
+ if (!application.setStyleFile(settings.value("Settings/style", "").toString())) {
+ // disable invalid stylesheet
+ settings.setValue("Settings/style", "");
+ }
+
+ int res = 1;
+ { // scope to control lifetime of mainwindow
+ // set up main window and its data structures
+ MainWindow mainWindow(settings, organizer, pluginContainer);
+
+ QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
+ SLOT(setStyleFile(QString)));
+ QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer,
+ SLOT(externalMessage(QString)));
+
+ mainWindow.processUpdates();
+ mainWindow.readSettings();
+
+ qDebug("displaying main window");
+ mainWindow.show();
+ mainWindow.activateWindow();
+
+ splash.finish(&mainWindow);
+ return application.exec();
+ }
+ } catch (const std::exception &e) {
+ reportError(e.what());
+ return 1;
+ }
+}
+
+
+int main(int argc, char *argv[])
+{
+ //Make sure the configured temp folder exists
+ QDir tempDir = QDir::temp();
+ if (!tempDir.exists())
+ tempDir.root().mkpath(tempDir.canonicalPath());
+
+ //Should allow for better scaling of ui with higher resolution displays
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+ if (argc >= 4) {
+ std::vector<std::wstring> arg;
+ auto args = UntouchedCommandLineArguments(2, arg);
+ if (arg[0] == L"launch")
+ return SpawnWaitProcess(arg[1].c_str(), args);
+ }
+
+ MOApplication application(argc, argv);
+ QStringList arguments = application.arguments();
+
+ setupPath();
+
+ bool forcePrimary = false;
+ if (arguments.contains("update")) {
+ arguments.removeAll("update");
+ forcePrimary = true;
+ }
+
+ MOShortcut moshortcut{ arguments.size() > 1 ? arguments.at(1) : "" };
+
+ SingleInstance instance(forcePrimary);
+ if (!instance.primaryInstance()) {
+ if (moshortcut ||
+ arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1)))
+ {
+ qDebug("not primary instance, sending shortcut/download message");
+ instance.sendMessage(arguments.at(1));
+ return 0;
+ } else if (arguments.size() == 1) {
+ QMessageBox::information(
+ nullptr, QObject::tr("Mod Organizer"),
+ QObject::tr("An instance of Mod Organizer is already running"));
+ return 0;
+ }
+ } // we continue for the primary instance OR if MO was called with parameters
+
+ do {
+ QString dataPath;
+
+ try {
+ InstanceManager& instanceManager = InstanceManager::instance();
+ if (moshortcut && moshortcut.hasInstance())
+ instanceManager.overrideInstance(moshortcut.instance());
+ dataPath = instanceManager.determineDataPath();
+ } catch (const std::exception &e) {
+ if (strcmp(e.what(),"Canceled"))
+ QMessageBox::critical(nullptr, QObject::tr("Failed to set up instance"), e.what());
+ return 1;
+ }
+ application.setProperty("dataPath", dataPath);
+
+ // initialize dump collection only after "dataPath" since the crashes are stored under it
+ prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
+
+ LogBuffer::init(1000000, QtDebugMsg, qApp->property("dataPath").toString() + "/logs/mo_interface.log");
+
+ QString splash = dataPath + "/splash.png";
+ if (!QFile::exists(dataPath + "/splash.png")) {
+ splash = ":/MO/gui/splash";
+ }
+
+ int result = runApplication(application, instance, splash);
+ if (result != INT_MAX) {
+ return result;
+ }
+ argc = 1;
+ moshortcut = MOShortcut("");
+ } while (true);
+}
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 954c88c6..389e65ca 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,5995 +1,6940 @@
-/*
-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 "mainwindow.h"
-#include "ui_mainwindow.h"
-
-#include "directoryentry.h"
-#include "directoryrefresher.h"
-#include "executableinfo.h"
-#include "executableslist.h"
-#include "guessedvalue.h"
-#include "imodinterface.h"
-#include "iplugingame.h"
-#include "iplugindiagnose.h"
-#include "isavegame.h"
-#include "isavegameinfowidget.h"
-#include "nexusinterface.h"
-#include "organizercore.h"
-#include "pluginlistsortproxy.h"
-#include "previewgenerator.h"
-#include "serverinfo.h"
-#include "savegameinfo.h"
-#include "spawn.h"
-#include "versioninfo.h"
-#include "instancemanager.h"
-
-#include "report.h"
-#include "modlist.h"
-#include "modlistsortproxy.h"
-#include "qtgroupingproxy.h"
-#include "profile.h"
-#include "pluginlist.h"
-#include "profilesdialog.h"
-#include "editexecutablesdialog.h"
-#include "categories.h"
-#include "categoriesdialog.h"
-#include "modinfodialog.h"
-#include "overwriteinfodialog.h"
-#include "activatemodsdialog.h"
-#include "downloadlist.h"
-#include "downloadlistwidget.h"
-#include "downloadlistwidgetcompact.h"
-#include "messagedialog.h"
-#include "installationmanager.h"
-#include "lockeddialog.h"
-#include "waitingonclosedialog.h"
-#include "logbuffer.h"
-#include "downloadlistsortproxy.h"
-#include "motddialog.h"
-#include "filedialogmemory.h"
-#include "tutorialmanager.h"
-#include "modflagicondelegate.h"
-#include "genericicondelegate.h"
-#include "selectiondialog.h"
-#include "csvbuilder.h"
-#include "savetextasdialog.h"
-#include "problemsdialog.h"
-#include "previewdialog.h"
-#include "browserdialog.h"
-#include "aboutdialog.h"
-#include <safewritefile.h>
-#include "nxmaccessmanager.h"
-#include "appconfig.h"
-#include "eventfilter.h"
-#include <utility.h>
-#include <dataarchives.h>
-#include <bsainvalidation.h>
-#include <taskprogressmanager.h>
-#include <scopeguard.h>
-#include <usvfs.h>
-#include "localsavegames.h"
-#include "listdialog.h"
-
-#include <QAbstractItemDelegate>
-#include <QAbstractProxyModel>
-#include <QAction>
-#include <QApplication>
-#include <QButtonGroup>
-#include <QBuffer>
-#include <QCheckBox>
-#include <QClipboard>
-#include <QCloseEvent>
-#include <QCoreApplication>
-#include <QCursor>
-#include <QDebug>
-#include <QDesktopWidget>
-#include <QDesktopServices>
-#include <QDialog>
-#include <QDirIterator>
-#include <QDragEnterEvent>
-#include <QDropEvent>
-#include <QEvent>
-#include <QFileDialog>
-#include <QFIleIconProvider>
-#include <QFont>
-#include <QFuture>
-#include <QHash>
-#include <QIODevice>
-#include <QIcon>
-#include <QInputDialog>
-#include <QItemSelection>
-#include <QItemSelectionModel>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonValueRef>
-#include <QLineEdit>
-#include <QListWidgetItem>
-#include <QMenu>
-#include <QMessageBox>
-#include <QMimeData>
-#include <QModelIndex>
-#include <QNetworkProxyFactory>
-#include <QPainter>
-#include <QPixmap>
-#include <QPoint>
-#include <QProcess>
-#include <QProgressDialog>
-#include <QDialogButtonBox>
-#include <QPushButton>
-#include <QRadioButton>
-#include <QRect>
-#include <QRegExp>
-#include <QResizeEvent>
-#include <QSettings>
-#include <QScopedPointer>
-#include <QSizePolicy>
-#include <QSize>
-#include <QTime>
-#include <QTimer>
-#include <QToolButton>
-#include <QToolTip>
-#include <QTranslator>
-#include <QTreeWidget>
-#include <QTreeWidgetItemIterator>
-#include <QUrl>
-#include <QVariantList>
-#include <QVersionNumber>
-#include <QWhatsThis>
-#include <QWidgetAction>
-#include <QWebEngineProfile>
-#include <QShortcut>
-#include <QColorDialog>
-#include <QColor>
-
-#include <QDebug>
-#include <QtGlobal>
-
-#ifndef Q_MOC_RUN
-#include <boost/thread.hpp>
-#include <boost/algorithm/string.hpp>
-#include <boost/bind.hpp>
-#include <boost/assign.hpp>
-#include <boost/range/adaptor/reversed.hpp>
-#endif
-
-#include <shlobj.h>
-
-#include <limits.h>
-#include <exception>
-#include <functional>
-#include <map>
-#include <regex>
-#include <stdexcept>
-#include <sstream>
-#include <utility>
-
-#ifdef TEST_MODELS
-#include "modeltest.h"
-#endif // TEST_MODELS
-
-#pragma warning( disable : 4428 )
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-MainWindow::MainWindow(QSettings &initSettings
- , OrganizerCore &organizerCore
- , PluginContainer &pluginContainer
- , QWidget *parent)
- : QMainWindow(parent)
- , ui(new Ui::MainWindow)
- , m_WasVisible(false)
- , m_Tutorial(this, "MainWindow")
- , m_OldProfileIndex(-1)
- , m_ModListGroupingProxy(nullptr)
- , m_ModListSortProxy(nullptr)
- , m_OldExecutableIndex(-1)
- , m_CategoryFactory(CategoryFactory::instance())
- , m_ContextItem(nullptr)
- , m_ContextAction(nullptr)
- , m_ContextRow(-1)
- , m_CurrentSaveView(nullptr)
- , m_OrganizerCore(organizerCore)
- , m_PluginContainer(pluginContainer)
- , m_DidUpdateMasterList(false)
- , m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this))
-{
- 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);
-
- languageChange(m_OrganizerCore.settings().language());
-
- m_CategoryFactory.loadCategories();
-
- ui->logList->setModel(LogBuffer::instance());
- ui->logList->setColumnWidth(0, 100);
- ui->logList->setAutoScroll(true);
- ui->logList->scrollToBottom();
- ui->logList->addAction(ui->actionCopy_Log_to_Clipboard);
- int splitterSize = this->size().height(); // actually total window size, but the splitter doesn't seem to return the true value
- ui->topLevelSplitter->setSizes(QList<int>() << splitterSize - 100 << 100);
- connect(ui->logList->model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
- ui->logList, SLOT(scrollToBottom()));
- 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();
-
- ui->actionEndorseMO->setVisible(false);
-
- 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();
-
- 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
- m_ModListSortProxy = m_OrganizerCore.createModListProxyModel();
-
- ui->modList->setModel(m_ModListSortProxy);
-
- GenericIconDelegate *contentDelegate = new GenericIconDelegate(ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150);
- connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), contentDelegate, SLOT(columnResized(int,int,int)));
- ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
- ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, new ModFlagIconDelegate(ui->modList));
- ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate);
- ui->modList->header()->installEventFilter(m_OrganizerCore.modList());
-
- bool modListAdjusted = registerWidgetState(ui->modList->objectName(), ui->modList->header(), "mod_list_state");
-
- if (modListAdjusted) {
- // hack: force the resize-signal to be triggered because restoreState doesn't seem to do that
- int sectionSize = ui->modList->header()->sectionSize(ModList::COL_CONTENT);
- ui->modList->header()->resizeSection(ModList::COL_CONTENT, sectionSize + 1);
- ui->modList->header()->resizeSection(ModList::COL_CONTENT, sectionSize);
- } else {
- // hide these columns by default
- ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true);
- ui->modList->header()->setSectionHidden(ModList::COL_MODID, true);
- ui->modList->header()->setSectionHidden(ModList::COL_GAME, true);
- ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
- ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
- }
-
- ui->modList->header()->setSectionHidden(ModList::COL_NAME, false); // prevent the name-column from being hidden
- ui->modList->installEventFilter(m_OrganizerCore.modList());
-
- // set up plugin list
- m_PluginListSortProxy = m_OrganizerCore.createPluginListProxyModel();
-
- ui->espList->setModel(m_PluginListSortProxy);
- ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder);
- ui->espList->setItemDelegateForColumn(PluginList::COL_FLAGS, new GenericIconDelegate(ui->espList));
- ui->espList->installEventFilter(m_OrganizerCore.pluginList());
-
- //ui->bsaList->setLocalMoveOnly(true);
-
- bool pluginListAdjusted = registerWidgetState(ui->espList->objectName(), ui->espList->header(), "plugin_list_state");
- registerWidgetState(ui->dataTree->objectName(), ui->dataTree->header());
- registerWidgetState(ui->downloadView->objectName(),
- ui->downloadView->header());
-
- ui->splitter->setStretchFactor(0, 3);
- ui->splitter->setStretchFactor(1, 2);
-
- 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()));
- ui->linkButton->setMenu(linkMenu);
-
- ui->listOptionsBtn->setMenu(modListContextMenu());
- connect(ui->listOptionsBtn, SIGNAL(pressed()), this, SLOT(on_listOptionsBtn_pressed()));
-
- ui->openFolderMenu->setMenu(openFolderMenu());
-
- updateDownloadListDelegate();
-
- ui->savegameList->installEventFilter(this);
- ui->savegameList->setMouseTracking(true);
-
- // don't allow mouse wheel to switch grouping, too many people accidentally
- // turn on grouping and then don't understand what happened
- EventFilter *noWheel
- = new EventFilter(this, [](QObject *, QEvent *event) -> bool {
- return event->type() == QEvent::Wheel;
- });
-
- ui->groupCombo->installEventFilter(noWheel);
- ui->profileBox->installEventFilter(noWheel);
-
- if (organizerCore.managedGame()->sortMechanism() == MOBase::IPluginGame::SortMechanism::NONE) {
- ui->bossButton->setDisabled(true);
- ui->bossButton->setToolTip(tr("There is no supported sort mechanism for this game. You will probably have to use a third-party tool."));
- }
-
- connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*)));
-
- connect(ui->modList, SIGNAL(dropModeUpdate(bool)), m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool)));
-
- connect(ui->modList->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(modlistSelectionChanged(QModelIndex,QModelIndex)));
- connect(m_ModListSortProxy, SIGNAL(filterActive(bool)), this, SLOT(modFilterActive(bool)));
- connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString)));
-
- connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), m_PluginListSortProxy, SLOT(updateFilter(QString)));
- connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString)));
-
- connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*)));
-
- connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed()));
- connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int)));
- connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString)));
-
- connect(&m_SavesWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(refreshSavesIfOpen()));
-
- connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString)));
- connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString)));
-
- connect(m_OrganizerCore.updater(), SIGNAL(restart()), this, SLOT(close()));
- connect(m_OrganizerCore.updater(), SIGNAL(updateAvailable()), this, SLOT(updateAvailable()));
- connect(m_OrganizerCore.updater(), SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString)));
-
- connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString)));
- 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(nexusLogin()));
- connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString)));
- connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)),
- this, SLOT(updateWindowTitle(const QString&, bool)));
-
- 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(&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()));
-
- connect(ui->espList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(esplistSelectionsChanged(QItemSelection)));
- connect(ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(modlistSelectionsChanged(QItemSelection)));
-
- new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated()));
- new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated()));
-
- new QShortcut(QKeySequence::Refresh, this, SLOT(refreshProfile_activated()));
-
- new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F), this, SLOT(search_activated()));
- new QShortcut(QKeySequence(Qt::Key_Escape), this, SLOT(searchClear_activated()));
-
- m_UpdateProblemsTimer.setSingleShot(true);
- connect(&m_UpdateProblemsTimer, SIGNAL(timeout()), this, SLOT(updateProblemsButton()));
-
- m_SaveMetaTimer.setSingleShot(false);
- connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas()));
- m_SaveMetaTimer.start(5000);
-
- setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool());
- FileDialogMemory::restore(initSettings);
-
- fixCategories();
-
- m_StartTime = QTime::currentTime();
-
- m_Tutorial.expose("modList", m_OrganizerCore.modList());
- m_Tutorial.expose("espList", m_OrganizerCore.pluginList());
-
- m_OrganizerCore.setUserInterface(this, this);
- for (const QString &fileName : m_PluginContainer.pluginFileNames()) {
- installTranslator(QFileInfo(fileName).baseName());
- }
-
- registerPluginTools(m_PluginContainer.plugins<IPluginTool>());
-
- for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins<IPluginModPage>()) {
- registerModPage(modPagePlugin);
- }
-
- // refresh profiles so the current profile can be activated
- refreshProfiles(false);
-
- ui->profileBox->setCurrentText(m_OrganizerCore.currentProfile()->name());
-
- refreshExecutablesList();
- updateToolBar();
-
- for (QAction *action : ui->toolBar->actions()) {
- // set the name of the widget to the name of the action to allow styling
- ui->toolBar->widgetForAction(action)->setObjectName(action->objectName());
- }
-}
-
-
-MainWindow::~MainWindow()
-{
- cleanup();
-
- m_PluginContainer.setUserInterface(nullptr, nullptr);
- m_OrganizerCore.setUserInterface(nullptr, nullptr);
- m_IntegratedBrowser.close();
- delete ui;
-}
-
-
-void MainWindow::updateWindowTitle(const QString &accountName, bool premium)
-{
- QString title = QString("%1 Mod Organizer v%2").arg(
- m_OrganizerCore.managedGame()->gameName(),
- m_OrganizerCore.getVersion().displayString());
-
- if (!accountName.isEmpty()) {
- title.append(QString(" (%1%2)").arg(accountName, premium ? "*" : ""));
- }
-
- this->setWindowTitle(title);
-}
-
-
-void MainWindow::disconnectPlugins()
-{
- if (ui->actionTool->menu() != nullptr) {
- ui->actionTool->menu()->clear();
- }
-}
-
-
-void MainWindow::resizeLists(bool modListCustom, bool pluginListCustom)
-{
- if (!modListCustom) {
- // resize mod list to fit content
- for (int i = 0; i < ui->modList->header()->count(); ++i) {
- ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
- }
- ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
- }
-
- // ensure the columns aren't so small you can't see them any more
- for (int i = 0; i < ui->modList->header()->count(); ++i) {
- if (ui->modList->header()->sectionSize(i) < 10) {
- ui->modList->header()->resizeSection(i, 10);
- }
- }
-
- if (!pluginListCustom) {
- // resize plugin list to fit content
- for (int i = 0; i < ui->espList->header()->count(); ++i) {
- ui->espList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
- }
- ui->espList->header()->setSectionResizeMode(0, QHeaderView::Stretch);
- }
-}
-
-
-void MainWindow::allowListResize()
-{
- // allow resize on mod list
- for (int i = 0; i < ui->modList->header()->count(); ++i) {
- ui->modList->header()->setSectionResizeMode(i, QHeaderView::Interactive);
- }
- //ui->modList->header()->setSectionResizeMode(ui->modList->header()->count() - 1, QHeaderView::Stretch);
- ui->modList->header()->setStretchLastSection(true);
-
-
- // allow resize on plugin list
- for (int i = 0; i < ui->espList->header()->count(); ++i) {
- ui->espList->header()->setSectionResizeMode(i, QHeaderView::Interactive);
- }
- //ui->espList->header()->setSectionResizeMode(ui->espList->header()->count() - 1, QHeaderView::Stretch);
- ui->espList->header()->setStretchLastSection(true);
-}
-
-void MainWindow::updateStyle(const QString&)
-{
- // no effect?
- ensurePolished();
-}
-
-void MainWindow::resizeEvent(QResizeEvent *event)
-{
- m_Tutorial.resize(event->size());
- QMainWindow::resizeEvent(event);
-}
-
-
-static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex idx)
-{
- QModelIndex result = idx;
- const QAbstractItemModel *model = idx.model();
- while (model != targetModel) {
- if (model == nullptr) {
- return QModelIndex();
- }
- const QAbstractProxyModel *proxyModel = qobject_cast<const QAbstractProxyModel*>(model);
- if (proxyModel == nullptr) {
- return QModelIndex();
- }
- result = proxyModel->mapToSource(result);
- model = proxyModel->sourceModel();
- }
- return result;
-}
-
-
-void MainWindow::actionToToolButton(QAction *&sourceAction)
-{
- 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;
-}
-
-void MainWindow::updateToolBar()
-{
- for (QAction *action : ui->toolBar->actions()) {
- if (action->objectName().startsWith("custom__")) {
- ui->toolBar->removeAction(action);
- action->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);
- if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) {
- qDebug("failed to connect trigger?");
- }
- ui->toolBar->insertAction(m_Sep, exeAction);
- }
- }
-}
-
-
-void MainWindow::scheduleUpdateButton()
-{
- if (!m_UpdateProblemsTimer.isActive()) {
- m_UpdateProblemsTimer.start(1000);
- }
-}
-
-
-void MainWindow::updateProblemsButton()
-{
- size_t numProblems = checkForProblems();
- if (numProblems > 0) {
- ui->actionProblems->setEnabled(true);
- ui->actionProblems->setIconText(tr("Problems"));
- ui->actionProblems->setToolTip(tr("There are potential problems with your setup"));
-
- QPixmap mergedIcon = QPixmap(":/MO/gui/warning").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");
- painter.drawPixmap(32, 32, 32, 32, QPixmap(badgeName.c_str()));
- }
- ui->actionProblems->setIcon(QIcon(mergedIcon));
- } else {
- ui->actionProblems->setEnabled(false);
- ui->actionProblems->setIconText(tr("No Problems"));
- ui->actionProblems->setToolTip(tr("Everything seems to be in order"));
- ui->actionProblems->setIcon(QIcon(":/MO/gui/warning"));
- }
-}
-
-
-bool MainWindow::errorReported(QString &logFile)
-{
- QDir dir(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()));
- QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"),
- QDir::Files, QDir::Name | QDir::Reversed);
-
- if (files.count() > 0) {
- logFile = files.at(0).absoluteFilePath();
- QFile file(logFile);
- if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
- char buffer[1024];
- int line = 0;
- while (!file.atEnd()) {
- file.readLine(buffer, 1024);
- if (strncmp(buffer, "ERROR", 5) == 0) {
- return true;
- }
-
- // prevent this function from taking forever
- if (line++ >= 50000) {
- break;
- }
- }
- }
- }
-
- return false;
-}
-
-
-size_t MainWindow::checkForProblems()
-{
- size_t numProblems = 0;
- for (IPluginDiagnose *diagnose : m_PluginContainer.plugins<IPluginDiagnose>()) {
- numProblems += diagnose->activeProblems().size();
- }
- return numProblems;
-}
-
-void MainWindow::about()
-{
- AboutDialog dialog(m_OrganizerCore.getVersion().displayString(), this);
- connect(&dialog, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString)));
- dialog.exec();
-}
-
-
-void MainWindow::createHelpWidget()
-{
- QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionHelp));
- QMenu *buttonMenu = toolBtn->menu();
- if (buttonMenu == nullptr) {
- return;
- }
- buttonMenu->clear();
-
- QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu);
- connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered()));
- buttonMenu->addAction(helpAction);
-
- QAction *wikiAction = new QAction(tr("Documentation Wiki"), buttonMenu);
- connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered()));
- buttonMenu->addAction(wikiAction);
-
- QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu);
- connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered()));
- buttonMenu->addAction(issueAction);
-
- QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu);
-
- typedef std::vector<std::pair<int, QAction*> > ActionList;
-
- ActionList tutorials;
-
- QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files);
- while (dirIter.hasNext()) {
- dirIter.next();
- QString fileName = dirIter.fileName();
-
- QFile file(dirIter.filePath());
- if (!file.open(QIODevice::ReadOnly)) {
- qCritical() << "Failed to open " << fileName;
- continue;
- }
- QString firstLine = QString::fromUtf8(file.readLine());
- if (firstLine.startsWith("//TL")) {
- QStringList params = firstLine.mid(4).trimmed().split('#');
- if (params.size() != 2) {
- qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters";
- continue;
- }
- QAction *tutAction = new QAction(params.at(0), tutorialMenu);
- tutAction->setData(fileName);
- tutorials.push_back(std::make_pair(params.at(1).toInt(), tutAction));
- }
- }
-
- std::sort(tutorials.begin(), tutorials.end(),
- [] (const ActionList::value_type &LHS, const ActionList::value_type &RHS) {
- return LHS.first < RHS.first; } );
-
- for (auto iter = tutorials.begin(); iter != tutorials.end(); ++iter) {
- connect(iter->second, SIGNAL(triggered()), this, SLOT(tutorialTriggered()));
- tutorialMenu->addAction(iter->second);
- }
-
- buttonMenu->addMenu(tutorialMenu);
- buttonMenu->addAction(tr("About"), this, SLOT(about()));
- buttonMenu->addAction(tr("About Qt"), qApp, SLOT(aboutQt()));
-}
-
-void MainWindow::modFilterActive(bool filterActive)
-{
- if (filterActive) {
-// m_OrganizerCore.modList()->setOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>());
- ui->modList->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
- } else if (ui->groupCombo->currentIndex() != 0) {
- ui->modList->setStyleSheet("QTreeView { border: 2px ridge #337733; }");
- } else {
- ui->modList->setStyleSheet("");
- }
-}
-
-void MainWindow::espFilterChanged(const QString &filter)
-{
- if (!filter.isEmpty()) {
- ui->espList->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
- } else {
- ui->espList->setStyleSheet("");
- }
-}
-
-void MainWindow::downloadFilterChanged(const QString &filter)
-{
- if (!filter.isEmpty()) {
- ui->downloadView->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
- } else {
- ui->downloadView->setStyleSheet("");
- }
-}
-
-void MainWindow::expandModList(const QModelIndex &index)
-{
- QAbstractItemModel *model = ui->modList->model();
-#pragma message("why is this so complicated? mapping the index doesn't work, probably a bug in QtGroupingProxy?")
- for (int i = 0; i < model->rowCount(); ++i) {
- QModelIndex targetIdx = model->index(i, 0);
- if (model->data(targetIdx).toString() == index.data().toString()) {
- ui->modList->expand(targetIdx);
- break;
- }
- }
-}
-
-
-bool MainWindow::addProfile()
-{
- QComboBox *profileBox = findChild<QComboBox*>("profileBox");
- bool okClicked = false;
-
- QString name = QInputDialog::getText(this, tr("Name"),
- tr("Please enter a name for the new profile"),
- QLineEdit::Normal, QString(), &okClicked);
- if (okClicked && (name.size() > 0)) {
- try {
- profileBox->addItem(name);
- profileBox->setCurrentIndex(profileBox->count() - 1);
- return true;
- } catch (const std::exception& e) {
- reportError(tr("failed to create profile: %1").arg(e.what()));
- return false;
- }
- } else {
- return false;
- }
-}
-
-void MainWindow::hookUpWindowTutorials()
-{
- QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files);
- while (dirIter.hasNext()) {
- dirIter.next();
- QString fileName = dirIter.fileName();
- QFile file(dirIter.filePath());
- if (!file.open(QIODevice::ReadOnly)) {
- qCritical() << "Failed to open " << fileName;
- continue;
- }
- QString firstLine = QString::fromUtf8(file.readLine());
- if (firstLine.startsWith("//WIN")) {
- QString windowName = firstLine.mid(6).trimmed();
- if (!m_OrganizerCore.settings().directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) {
- TutorialManager::instance().activateTutorial(windowName, fileName);
- }
- }
- }
-}
-
-void MainWindow::showEvent(QShowEvent *event)
-{
- refreshFilters();
-
- QMainWindow::showEvent(event);
-
- if (!m_WasVisible) {
- // only the first time the window becomes visible
- m_Tutorial.registerControl();
-
- hookUpWindowTutorials();
-
- if (m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) {
- QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial());
- if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) {
- if (QMessageBox::question(this, tr("Show tutorial?"),
- tr("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."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial);
- }
- } else {
- qCritical() << firstStepsTutorial << " missing";
- QPoint pos = ui->toolBar->mapToGlobal(QPoint());
- pos.rx() += ui->toolBar->width() / 2;
- pos.ry() += ui->toolBar->height();
- QWhatsThis::showText(pos,
- QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements"));
- }
-
- m_OrganizerCore.settings().directInterface().setValue("first_start", false);
- }
-
- // this has no visible impact when called before the ui is visible
- int grouping = m_OrganizerCore.settings().directInterface().value("group_state").toInt();
- ui->groupCombo->setCurrentIndex(grouping);
-
- allowListResize();
-
- m_OrganizerCore.settings().registerAsNXMHandler(false);
- m_WasVisible = true;
- updateProblemsButton();
- }
-}
-
-
-void MainWindow::closeEvent(QCloseEvent* event)
-{
- 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;
- } else {
- m_OrganizerCore.downloadManager()->pauseAll();
- }
- }
-
- std::vector<QString> hiddenList;
- hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName());
- HANDLE injected_process_still_running = m_OrganizerCore.findAndOpenAUSVFSProcess(hiddenList, GetCurrentProcessId());
- if (injected_process_still_running != INVALID_HANDLE_VALUE)
- {
- m_OrganizerCore.waitForApplication(injected_process_still_running);
- if (!m_closing) { // if operation cancelled
- event->ignore();
- return;
- }
- }
-
- setCursor(Qt::WaitCursor);
-}
-
-void MainWindow::cleanup()
-{
- if (ui->logList->model() != nullptr) {
- disconnect(ui->logList->model(), nullptr, nullptr, nullptr);
- ui->logList->setModel(nullptr);
- }
-
- QWebEngineProfile::defaultProfile()->clearAllVisitedLinks();
- m_IntegratedBrowser.close();
- m_SaveMetaTimer.stop();
- m_MetaSave.waitForFinished();
-}
-
-
-void MainWindow::setBrowserGeometry(const QByteArray &geometry)
-{
- m_IntegratedBrowser.restoreGeometry(geometry);
-}
-
-void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
-{
- QString const &save = newItem->data(Qt::UserRole).toString();
- if (m_CurrentSaveView == nullptr) {
- IPluginGame const *game = m_OrganizerCore.managedGame();
- SaveGameInfo const *info = game->feature<SaveGameInfo>();
- if (info != nullptr) {
- m_CurrentSaveView = info->getSaveGameWidget(this);
- }
- if (m_CurrentSaveView == nullptr) {
- return;
- }
- }
- m_CurrentSaveView->setSave(save);
-
- QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView);
-
- QPoint pos = QCursor::pos();
- if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
- pos.rx() -= (m_CurrentSaveView->width() + 2);
- } else {
- pos.rx() += 5;
- }
-
- if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
- pos.ry() -= (m_CurrentSaveView->height() + 10);
- } else {
- pos.ry() += 20;
- }
- m_CurrentSaveView->move(pos);
-
- m_CurrentSaveView->show();
- m_CurrentSaveView->setProperty("displayItem", qVariantFromValue(static_cast<void *>(newItem)));
-
- ui->savegameList->activateWindow();
-}
-
-
-void MainWindow::saveSelectionChanged(QListWidgetItem *newItem)
-{
- if (newItem == nullptr) {
- hideSaveGameInfo();
- } else if (m_CurrentSaveView == nullptr || newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
- displaySaveGameInfo(newItem);
- }
-}
-
-
-void MainWindow::hideSaveGameInfo()
-{
- if (m_CurrentSaveView != nullptr) {
- m_CurrentSaveView->deleteLater();
- m_CurrentSaveView = nullptr;
- }
-}
-
-bool MainWindow::eventFilter(QObject *object, QEvent *event)
-{
- if ((object == ui->savegameList) &&
- ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) {
- hideSaveGameInfo();
- }
- return false;
-}
-
-
-void MainWindow::toolPluginInvoke()
-{
- QAction *triggeredAction = qobject_cast<QAction*>(sender());
- IPluginTool *plugin = qobject_cast<IPluginTool*>(triggeredAction->data().value<QObject*>());
- if (plugin != nullptr) {
- try {
- plugin->display();
- } catch (const std::exception &e) {
- reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what()));
- } catch (...) {
- reportError(tr("Plugin \"%1\" failed").arg(plugin->name()));
- }
- }
-}
-
-void MainWindow::modPagePluginInvoke()
-{
- QAction *triggeredAction = qobject_cast<QAction*>(sender());
- IPluginModPage *plugin = qobject_cast<IPluginModPage*>(triggeredAction->data().value<QObject*>());
- if (plugin != nullptr) {
- if (plugin->useIntegratedBrowser()) {
- m_IntegratedBrowser.setWindowTitle(plugin->displayName());
- m_IntegratedBrowser.openUrl(plugin->pageURL());
- } else {
- QDesktopServices::openUrl(QUrl(plugin->pageURL()));
- }
- }
-}
-
-void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu)
-{
- if (name.isEmpty())
- name = tool->displayName();
-
- QAction *action = new QAction(tool->icon(), name, ui->toolBar);
- 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);
- }
-}
-
-void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins)
-{
- // Sort the plugins by display name
- std::sort(toolPlugins.begin(), toolPlugins.end(),
- [](IPluginTool *left, IPluginTool *right) {
- return left->displayName().toLower() < right->displayName().toLower();
- }
- );
-
- // Group the plugins into submenus
- QMap<QString, QList<QPair<QString, IPluginTool *>>> submenuMap;
- for (auto toolPlugin : toolPlugins) {
- QStringList toolName = toolPlugin->displayName().split("/");
- QString submenu = toolName[0];
- toolName.pop_front();
- submenuMap[submenu].append(QPair<QString, IPluginTool *>(toolName.join("/"), toolPlugin));
- }
-
- // Start registering plugins
- for (auto submenuKey : submenuMap.keys()) {
- if (submenuMap[submenuKey].length() > 1) {
- QMenu *submenu = new QMenu(submenuKey, this);
- 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);
- }
- else {
- registerPluginTool(submenuMap[submenuKey].front().second);
- }
- }
-}
-
-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);
-
- QToolButton *browserBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus));
- browserBtn->menu()->addAction(nexusAction);
- }
-
- QAction *action = new QAction(modPage->icon(), modPage->displayName(), ui->toolBar);
- 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);
-}
-
-
-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();
- 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);
- } else {
- qCritical("not an action?");
- }
-}
-
-
-void MainWindow::setExecutableIndex(int index)
-{
- QComboBox *executableBox = findChild<QComboBox*>("executablesListBox");
-
- if ((index != 0) && (executableBox->count() > index)) {
- executableBox->setCurrentIndex(index);
- } else {
- executableBox->setCurrentIndex(1);
- }
-}
-
-void MainWindow::activateSelectedProfile()
-{
- m_OrganizerCore.setCurrentProfile(ui->profileBox->currentText());
-
- m_ModListSortProxy->setProfile(m_OrganizerCore.currentProfile());
-
- refreshSaveList();
- m_OrganizerCore.refreshModList();
-}
-
-void MainWindow::on_profileBox_currentIndexChanged(int index)
-{
- if (ui->profileBox->isEnabled()) {
- int previousIndex = m_OldProfileIndex;
- m_OldProfileIndex = index;
-
- if ((previousIndex != -1) &&
- (m_OrganizerCore.currentProfile() != nullptr) &&
- m_OrganizerCore.currentProfile()->exists()) {
- m_OrganizerCore.saveCurrentLists();
- }
-
- // ensure the new index is valid
- if (index < 0 || index >= ui->profileBox->count()) {
- qDebug("invalid profile index, using last profile");
- ui->profileBox->setCurrentIndex(ui->profileBox->count() - 1);
- }
-
- if (ui->profileBox->currentIndex() == 0) {
- ui->profileBox->setCurrentIndex(previousIndex);
- ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec();
- while (!refreshProfiles()) {
- ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec();
- }
- } else {
- activateSelectedProfile();
- }
-
- LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature<LocalSavegames>();
- if (saveGames != nullptr) {
- if (saveGames->prepareProfile(m_OrganizerCore.currentProfile()))
- refreshSaveList();
- }
-
- BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
- if (invalidation != nullptr) {
- if (invalidation->prepareProfile(m_OrganizerCore.currentProfile()))
- QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh()));
- }
- }
-}
-
-void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon)
-{
- bool isDirectory = true;
- //QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- //QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
-
- std::wostringstream temp;
- temp << directorySoFar << "\\" << directoryEntry.getName();
- {
- std::vector<DirectoryEntry*>::const_iterator current, end;
- directoryEntry.getSubDirectories(current, end);
- for (; current != end; ++current) {
- QString pathName = ToQString((*current)->getName());
- QStringList columns(pathName);
- columns.append("");
- if (!(*current)->isEmpty()) {
- QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns);
- directoryChild->setData(0, Qt::DecorationRole, *folderIcon);
- directoryChild->setData(0, Qt::UserRole + 3, isDirectory);
-
- if (conflictsOnly) {
- updateTo(directoryChild, temp.str(), **current, conflictsOnly, fileIcon, folderIcon);
- if (directoryChild->childCount() != 0) {
- subTree->addChild(directoryChild);
- }
- else {
- delete directoryChild;
- }
- }
- else {
- QTreeWidgetItem *onDemandLoad = new QTreeWidgetItem(QStringList());
- onDemandLoad->setData(0, Qt::UserRole + 0, "__loaded_on_demand__");
- onDemandLoad->setData(0, Qt::UserRole + 1, ToQString(temp.str()));
- onDemandLoad->setData(0, Qt::UserRole + 2, conflictsOnly);
- directoryChild->addChild(onDemandLoad);
- subTree->addChild(directoryChild);
- }
- }
- else {
- QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns);
- directoryChild->setData(0, Qt::DecorationRole, *folderIcon);
- directoryChild->setData(0, Qt::UserRole + 3, isDirectory);
- subTree->addChild(directoryChild);
- }
- }
- }
-
-
- isDirectory = false;
- {
- for (const FileEntry::Ptr current : directoryEntry.getFiles()) {
- if (conflictsOnly && (current->getAlternatives().size() == 0)) {
- continue;
- }
-
- QString fileName = ToQString(current->getName());
- QStringList columns(fileName);
- bool isArchive = false;
- int originID = current->getOrigin(isArchive);
- FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
- QString source("data");
- unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
- if (modIndex != UINT_MAX) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- source = modInfo->name();
- }
-
- std::wstring archive = current->getArchive();
- if (archive.length() != 0) {
- source.append(" (").append(ToQString(archive)).append(")");
- }
- columns.append(source);
- QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns);
- if (isArchive) {
- QFont font = fileChild->font(0);
- font.setItalic(true);
- fileChild->setFont(0, font);
- fileChild->setFont(1, font);
- } else if (fileName.endsWith(ModInfo::s_HiddenExt)) {
- QFont font = fileChild->font(0);
- font.setStrikeOut(true);
- fileChild->setFont(0, font);
- fileChild->setFont(1, font);
- }
- fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath()));
- fileChild->setData(0, Qt::DecorationRole, *fileIcon);
- fileChild->setData(0, Qt::UserRole + 3, isDirectory);
- fileChild->setData(0, Qt::UserRole + 1, isArchive);
- fileChild->setData(1, Qt::UserRole, source);
- fileChild->setData(1, Qt::UserRole + 1, originID);
-
- std::vector<std::pair<int, std::wstring>> alternatives = current->getAlternatives();
-
- if (!alternatives.empty()) {
- std::wostringstream altString;
- altString << ToWString(tr("Also in: <br>"));
- for (std::vector<std::pair<int, std::wstring>>::iterator altIter = alternatives.begin();
- altIter != alternatives.end(); ++altIter) {
- if (altIter != alternatives.begin()) {
- altString << " , ";
- }
- altString << "<span style=\"white-space: nowrap;\"><i>" << m_OrganizerCore.directoryStructure()->getOriginByID(altIter->first).getName() << "</font></span>";
- }
- fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str())));
- fileChild->setForeground(1, QBrush(Qt::red));
- } else {
- fileChild->setToolTip(1, tr("No conflict"));
- }
- subTree->addChild(fileChild);
- }
- }
-
-
- //subTree->sortChildren(0, Qt::AscendingOrder);
-}
-
-void MainWindow::delayedRemove()
-{
- for (QTreeWidgetItem *item : m_RemoveWidget) {
- item->removeChild(item->child(0));
- }
- m_RemoveWidget.clear();
-}
-
-void MainWindow::expandDataTreeItem(QTreeWidgetItem *item)
-{
-
- if ((item->childCount() == 1) && (item->child(0)->data(0, Qt::UserRole).toString() == "__loaded_on_demand__")) {
- // read the data we need from the sub-item, then dispose of it
- QTreeWidgetItem *onDemandDataItem = item->child(0);
- std::wstring path = ToWString(onDemandDataItem->data(0, Qt::UserRole + 1).toString());
- bool conflictsOnly = onDemandDataItem->data(0, Qt::UserRole + 2).toBool();
-
- std::wstring virtualPath = (path + L"\\").substr(6) + ToWString(item->text(0));
- DirectoryEntry *dir = m_OrganizerCore.directoryStructure()->findSubDirectoryRecursive(virtualPath);
- if (dir != nullptr) {
- QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- updateTo(item, path, *dir, conflictsOnly, &fileIcon, &folderIcon);
- } else {
- qWarning("failed to update view of %ls", path.c_str());
- }
- m_RemoveWidget.push_back(item);
- QTimer::singleShot(5, this, SLOT(delayedRemove()));
- }
-}
-
-
-bool MainWindow::refreshProfiles(bool selectProfile)
-{
- QComboBox* profileBox = findChild<QComboBox*>("profileBox");
-
- QString currentProfileName = profileBox->currentText();
-
- profileBox->blockSignals(true);
- profileBox->clear();
- profileBox->addItem(QObject::tr("<Manage...>"));
-
- QDir profilesDir(Settings::instance().getProfileDirectory());
- profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
-
- QDirIterator profileIter(profilesDir);
-
- while (profileIter.hasNext()) {
- profileIter.next();
- try {
- profileBox->addItem(profileIter.fileName());
- } catch (const std::runtime_error& error) {
- reportError(QObject::tr("failed to parse profile %1: %2").arg(profileIter.fileName()).arg(error.what()));
- }
- }
-
- // now select one of the profiles, preferably the one that was selected before
- profileBox->blockSignals(false);
-
- if (selectProfile) {
- if (profileBox->count() > 1) {
- profileBox->setCurrentText(currentProfileName);
- if (profileBox->currentIndex() == 0) {
- profileBox->setCurrentIndex(1);
- }
- }
- }
- return profileBox->count() > 1;
-}
-
-
-void MainWindow::refreshExecutablesList()
-{
- QComboBox* executablesList = findChild<QComboBox*>("executablesListBox");
- executablesList->setEnabled(false);
- executablesList->clear();
- executablesList->addItem(tr("<Edit...>"));
-
- 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);
- model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole);
- }
-
- setExecutableIndex(1);
- executablesList->setEnabled(true);
-}
-
-
-void MainWindow::refreshDataTree()
-{
- QCheckBox *conflictsBox = findChild<QCheckBox*>("conflictsCheckBox");
- QTreeWidget *tree = findChild<QTreeWidget*>("dataTree");
- QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- tree->clear();
- QStringList columns("data");
- columns.append("");
- QTreeWidgetItem *subTree = new QTreeWidgetItem(columns);
- subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder));
- updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon);
- tree->insertTopLevelItem(0, subTree);
- subTree->setExpanded(true);
-}
-
-void MainWindow::refreshDataTreeKeepExpandedNodes()
-{
- QCheckBox *conflictsBox = findChild<QCheckBox*>("conflictsCheckBox");
- QTreeWidget *tree = findChild<QTreeWidget*>("dataTree");
- QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- QStringList expandedNodes;
- QTreeWidgetItemIterator it1(tree, QTreeWidgetItemIterator::NotHidden | QTreeWidgetItemIterator::HasChildren);
- while (*it1) {
- QTreeWidgetItem *current = (*it1);
- if (current->isExpanded() && !(current->text(0)=="data")) {
- expandedNodes.append(current->text(0)+"/"+current->parent()->text(0));
- }
- ++it1;
- }
-
- tree->clear();
- QStringList columns("data");
- columns.append("");
- QTreeWidgetItem *subTree = new QTreeWidgetItem(columns);
- subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder));
- updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon);
- tree->insertTopLevelItem(0, subTree);
- subTree->setExpanded(true);
- QTreeWidgetItemIterator it2(tree, QTreeWidgetItemIterator::HasChildren);
- while (*it2) {
- QTreeWidgetItem *current = (*it2);
- if (!(current->text(0)=="data") && expandedNodes.contains(current->text(0)+"/"+current->parent()->text(0))) {
- current->setExpanded(true);
- }
- ++it2;
- }
-}
-
-
-void MainWindow::refreshSavesIfOpen()
-{
- if (ui->tabWidget->currentIndex() == 3) {
- refreshSaveList();
- }
-}
-
-QDir MainWindow::currentSavesDir() const
-{
- QDir savesDir;
- if (m_OrganizerCore.currentProfile()->localSavesEnabled()) {
- savesDir.setPath(m_OrganizerCore.currentProfile()->savePath());
- } else {
- QString iniPath = m_OrganizerCore.currentProfile()->localSettingsEnabled()
- ? m_OrganizerCore.currentProfile()->absolutePath()
- : m_OrganizerCore.managedGame()->documentsDirectory().absolutePath();
- iniPath += "/" + m_OrganizerCore.managedGame()->iniFiles()[0];
-
- wchar_t path[MAX_PATH];
- ::GetPrivateProfileStringW(
- L"General", L"SLocalSavePath", L"Saves",
- path, MAX_PATH,
- iniPath.toStdWString().c_str()
- );
- savesDir.setPath(m_OrganizerCore.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path)));
- }
-
- return savesDir;
-}
-
-void MainWindow::startMonitorSaves()
-{
- stopMonitorSaves();
-
- QDir savesDir = currentSavesDir();
-
- m_SavesWatcher.addPath(savesDir.absolutePath());
-}
-
-void MainWindow::stopMonitorSaves()
-{
- if (m_SavesWatcher.directories().length() > 0) {
- m_SavesWatcher.removePaths(m_SavesWatcher.directories());
- }
-}
-
-void MainWindow::refreshSaveList()
-{
- ui->savegameList->clear();
-
- startMonitorSaves(); // re-starts monitoring
-
- QStringList filters;
- filters << QString("*.") + m_OrganizerCore.managedGame()->savegameExtension();
-
- QDir savesDir = currentSavesDir();
- savesDir.setNameFilters(filters);
- qDebug("reading save games from %s", qPrintable(savesDir.absolutePath()));
-
- QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time);
- for (const QFileInfo &file : files) {
- QListWidgetItem *item = new QListWidgetItem(file.fileName());
- item->setData(Qt::UserRole, file.absoluteFilePath());
- ui->savegameList->addItem(item);
- }
-}
-
-
-static bool BySortValue(const std::pair<UINT32, QTreeWidgetItem*> &LHS, const std::pair<UINT32, QTreeWidgetItem*> &RHS)
-{
- return LHS.first < RHS.first;
-}
-
-template <typename InputIterator>
-static QStringList toStringList(InputIterator current, InputIterator end)
-{
- QStringList result;
- for (; current != end; ++current) {
- result.append(*current);
- }
- return result;
-}
-
-void MainWindow::updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives)
-{
- m_DefaultArchives = defaultArchives;
- ui->bsaList->clear();
- ui->bsaList->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
- std::vector<std::pair<UINT32, QTreeWidgetItem*>> items;
-
- BSAInvalidation * invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
- std::vector<FileEntry::Ptr> files = m_OrganizerCore.directoryStructure()->getFiles();
-
- QStringList plugins = m_OrganizerCore.findFiles("", [](const QString &fileName) -> bool {
- return fileName.endsWith(".esp", Qt::CaseInsensitive)
- || fileName.endsWith(".esm", Qt::CaseInsensitive)
- || fileName.endsWith(".esl", Qt::CaseInsensitive);
- });
-
- auto hasAssociatedPlugin = [&](const QString &bsaName) -> bool {
- for (const QString &pluginName : plugins) {
- QFileInfo pluginInfo(pluginName);
- if (bsaName.startsWith(QFileInfo(pluginName).baseName(), Qt::CaseInsensitive)
- && (m_OrganizerCore.pluginList()->state(pluginInfo.fileName()) == IPluginList::STATE_ACTIVE)) {
- return true;
- }
- }
- return false;
- };
-
- for (FileEntry::Ptr current : files) {
- QFileInfo fileInfo(ToQString(current->getName().c_str()));
-
- if (fileInfo.suffix().toLower() == "bsa" || fileInfo.suffix().toLower() == "ba2") {
- int index = activeArchives.indexOf(fileInfo.fileName());
- if (index == -1) {
- index = 0xFFFF;
- }
- else {
- index += 2;
- }
-
- if ((invalidation != nullptr) && invalidation->isInvalidationBSA(fileInfo.fileName())) {
- index = 1;
- }
-
- int originId = current->getOrigin();
- FilesOrigin & origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId);
-
- QTreeWidgetItem * newItem = new QTreeWidgetItem(QStringList()
- << fileInfo.fileName()
- << ToQString(origin.getName()));
- newItem->setData(0, Qt::UserRole, index);
- newItem->setData(1, Qt::UserRole, originId);
- newItem->setFlags(newItem->flags() & ~(Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable));
- newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked);
- newItem->setData(0, Qt::UserRole, false);
- if (m_OrganizerCore.settings().forceEnableCoreFiles()
- && defaultArchives.contains(fileInfo.fileName())) {
- newItem->setCheckState(0, Qt::Checked);
- newItem->setDisabled(true);
- newItem->setData(0, Qt::UserRole, true);
- } else if (fileInfo.fileName().compare("update.bsa", Qt::CaseInsensitive) == 0) {
- newItem->setCheckState(0, Qt::Checked);
- newItem->setDisabled(true);
- } else if (hasAssociatedPlugin(fileInfo.fileName())) {
- newItem->setCheckState(0, Qt::Checked);
- newItem->setDisabled(true);
- } else {
- newItem->setCheckState(0, Qt::Unchecked);
- newItem->setDisabled(true);
- }
- if (index < 0) index = 0;
-
- UINT32 sortValue = ((origin.getPriority() & 0xFFFF) << 16) | (index & 0xFFFF);
- items.push_back(std::make_pair(sortValue, newItem));
- }
- }
- std::sort(items.begin(), items.end(), BySortValue);
-
- for (auto iter = items.begin(); iter != items.end(); ++iter) {
- int originID = iter->second->data(1, Qt::UserRole).toInt();
-
- FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
- QString modName("data");
- unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
- if (modIndex != UINT_MAX) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- modName = modInfo->name();
- }
- QList<QTreeWidgetItem*> items = ui->bsaList->findItems(modName, Qt::MatchFixedString);
- QTreeWidgetItem * subItem = nullptr;
- if (items.length() > 0) {
- subItem = items.at(0);
- }
- else {
- subItem = new QTreeWidgetItem(QStringList(modName));
- subItem->setFlags(subItem->flags() & ~Qt::ItemIsDragEnabled);
- ui->bsaList->addTopLevelItem(subItem);
- }
- subItem->addChild(iter->second);
- subItem->setExpanded(true);
- }
- checkBSAList();
-}
-
-void MainWindow::checkBSAList()
-{
- DataArchives * archives = m_OrganizerCore.managedGame()->feature<DataArchives>();
-
- if (archives != nullptr) {
- ui->bsaList->blockSignals(true);
- ON_BLOCK_EXIT([&]() { ui->bsaList->blockSignals(false); });
-
- QStringList defaultArchives = archives->archives(m_OrganizerCore.currentProfile());
-
- bool warning = false;
-
- for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) {
- bool modWarning = false;
- QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i);
- for (int j = 0; j < tlItem->childCount(); ++j) {
- QTreeWidgetItem * item = tlItem->child(j);
- QString filename = item->text(0);
- item->setIcon(0, QIcon());
- item->setToolTip(0, QString());
-
- if (item->checkState(0) == Qt::Unchecked) {
- if (defaultArchives.contains(filename)) {
- item->setIcon(0, QIcon(":/MO/gui/warning"));
- item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!"));
- modWarning = true;
- }
- }
- }
- if (modWarning) {
- ui->bsaList->expandItem(ui->bsaList->topLevelItem(i));
- warning = true;
- }
- }
- if (warning) {
- ui->tabWidget->setTabIcon(1, QIcon(":/MO/gui/warning"));
- } else {
- ui->tabWidget->setTabIcon(1, QIcon());
- }
- }
-}
-
-void MainWindow::saveModMetas()
-{
- if (m_MetaSave.isFinished()) {
- m_MetaSave = QtConcurrent::run([this]() {
- for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
- modInfo->saveMeta();
- }
- });
- }
-}
-
-void MainWindow::fixCategories()
-{
- for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
- std::set<int> categories = modInfo->getCategories();
- for (std::set<int>::iterator iter = categories.begin();
- iter != categories.end(); ++iter) {
- if (!m_CategoryFactory.categoryExists(*iter)) {
- modInfo->setCategory(*iter, false);
- }
- }
- }
-}
-
-
-void MainWindow::setupNetworkProxy(bool activate)
-{
- QNetworkProxyFactory::setUseSystemConfiguration(activate);
-/* QNetworkProxyQuery query(QUrl("http://www.google.com"), QNetworkProxyQuery::UrlRequest);
- query.setProtocolTag("http");
- QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery(query);
- if ((proxies.size() > 0) && (proxies.at(0).type() != QNetworkProxy::NoProxy)) {
- qDebug("Using proxy: %s", qPrintable(proxies.at(0).hostName()));
- QNetworkProxy::setApplicationProxy(proxies[0]);
- } else {
- qDebug("Not using proxy");
- }*/
-}
-
-
-void MainWindow::activateProxy(bool activate)
-{
- QProgressDialog busyDialog(tr("Activating Network Proxy"), QString(), 0, 0, parentWidget());
- busyDialog.setWindowFlags(busyDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
- busyDialog.setWindowModality(Qt::WindowModal);
- busyDialog.show();
- QFuture<void> future = QtConcurrent::run(MainWindow::setupNetworkProxy, activate);
- while (!future.isFinished()) {
- QCoreApplication::processEvents();
- ::Sleep(100);
- }
- busyDialog.hide();
-}
-
-void MainWindow::readSettings()
-{
- QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat);
-
- if (settings.contains("window_geometry")) {
- restoreGeometry(settings.value("window_geometry").toByteArray());
- }
-
- if (settings.contains("window_split")) {
- ui->splitter->restoreState(settings.value("window_split").toByteArray());
- }
-
- if (settings.contains("log_split")) {
- ui->topLevelSplitter->restoreState(settings.value("log_split").toByteArray());
- }
-
- bool filtersVisible = settings.value("filters_visible", false).toBool();
- setCategoryListVisible(filtersVisible);
- ui->displayCategoriesBtn->setChecked(filtersVisible);
-
- int selectedExecutable = settings.value("selected_executable").toInt();
- setExecutableIndex(selectedExecutable);
-
- if (settings.value("Settings/use_proxy", false).toBool()) {
- activateProxy(true);
- }
-}
-
-void MainWindow::processUpdates() {
- QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat);
- QVersionNumber lastVersion = QVersionNumber::fromString(settings.value("version", "2.1.2").toString()).normalized();
- QVersionNumber currentVersion = QVersionNumber::fromString(m_OrganizerCore.getVersion().displayString()).normalized();
- if (!m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) {
- if (lastVersion < QVersionNumber(2, 1, 3)) {
- bool lastHidden = true;
- for (int i = ModList::COL_GAME; i < ui->modList->model()->columnCount(); ++i) {
- bool hidden = ui->modList->header()->isSectionHidden(i);
- ui->modList->header()->setSectionHidden(i, lastHidden);
- lastHidden = hidden;
- }
- }
- if (lastVersion < QVersionNumber(2,1,6)) {
- ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
- }
- }
-
- if (currentVersion > lastVersion)
- settings.setValue("version", currentVersion.toString());
- else if (currentVersion < lastVersion)
- qWarning() << tr("Notice: Your current MO version (%1) is lower than the previous version (%2).<br>"
- "The GUI may not downgrade gracefully, so you may experience oddities.<br>"
- "However, there should be no serious issues.").arg(lastVersion.toString()).arg(currentVersion.toString()).toStdWString();
-}
-
-void MainWindow::storeSettings(QSettings &settings) {
- settings.setValue("group_state", ui->groupCombo->currentIndex());
- settings.setValue("selected_executable",
- ui->executablesListBox->currentIndex());
-
- if (settings.value("reset_geometry", false).toBool()) {
- settings.remove("window_geometry");
- settings.remove("window_split");
- settings.remove("log_split");
- settings.remove("filters_visible");
- settings.remove("browser_geometry");
- settings.beginGroup("geometry");
- for (auto key : settings.childKeys()) {
- settings.remove(key);
- }
- settings.endGroup();
- settings.remove("reset_geometry");
- } else {
- settings.setValue("window_geometry", saveGeometry());
- settings.setValue("window_split", ui->splitter->saveState());
- settings.setValue("log_split", ui->topLevelSplitter->saveState());
- settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry());
- settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked());
- for (const std::pair<QString, QHeaderView*> kv : m_PersistedGeometry) {
- QString key = QString("geometry/") + kv.first;
- settings.setValue(key, kv.second->saveState());
- }
- }
-}
-
-ILockedWaitingForProcess* MainWindow::lock()
-{
- if (m_LockDialog != nullptr) {
- ++m_LockCount;
- return m_LockDialog;
- }
- if (m_closing)
- m_LockDialog = new WaitingOnCloseDialog(this);
- else
- m_LockDialog = new LockedDialog(this, true);
- m_LockDialog->setModal(true);
- m_LockDialog->show();
- setEnabled(false);
- m_LockDialog->setEnabled(true); //What's the point otherwise?
- ++m_LockCount;
- return m_LockDialog;
-}
-
-void MainWindow::unlock()
-{
- //If you come through here with a null lock pointer, it's a bug!
- if (m_LockDialog == nullptr) {
- qDebug("Unlocking main window when already unlocked");
- return;
- }
- --m_LockCount;
- if (m_LockCount == 0) {
- if (m_closing && m_LockDialog->canceled())
- m_closing = false;
- m_LockDialog->hide();
- m_LockDialog->deleteLater();
- m_LockDialog = nullptr;
- setEnabled(true);
- }
-}
-
-void MainWindow::on_btnRefreshData_clicked()
-{
- m_OrganizerCore.refreshDirectoryStructure();
-}
-
-void MainWindow::on_btnRefreshDownloads_clicked()
-{
- m_OrganizerCore.downloadManager()->refreshList();
-}
-
-void MainWindow::on_tabWidget_currentChanged(int index)
-{
- if (index == 0) {
- m_OrganizerCore.refreshESPList();
- } else if (index == 1) {
- m_OrganizerCore.refreshBSAList();
- } else if (index == 2) {
- refreshDataTreeKeepExpandedNodes();
- } else if (index == 3) {
- refreshSaveList();
- }
-}
-
-
-void MainWindow::installMod(QString fileName)
-{
- try {
- if (fileName.isEmpty()) {
- QStringList extensions = m_OrganizerCore.installationManager()->getSupportedExtensions();
- for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) {
- *iter = "*." + *iter;
- }
-
- fileName = FileDialogMemory::getOpenFileName("installMod", this, tr("Choose Mod"), QString(),
- tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" "))));
- }
-
- if (fileName.isEmpty()) {
- return;
- } else {
- m_OrganizerCore.installMod(fileName, QString());
- }
- } catch (const std::exception &e) {
- reportError(e.what());
- }
-}
-
-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();
- 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);
- } catch (...) {
- ui->startButton->setEnabled(true);
- throw;
- }
- 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());
- 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;
- }
- settings.setValue(key, dialog.saveGeometry());
- refreshExecutablesList();
- } catch (const std::exception &e) {
- reportError(e.what());
- }
- return result;
-}
-
-void MainWindow::on_executablesListBox_currentIndexChanged(int index)
-{
- QComboBox* executablesList = findChild<QComboBox*>("executablesListBox");
-
- int previousIndex = m_OldExecutableIndex;
- m_OldExecutableIndex = index;
-
- if (executablesList->isEnabled()) {
- //I think the 2nd test is impossible
- if ((index == 0) || (index > static_cast<int>(m_OrganizerCore.executablesList()->size()))) {
- if (modifyExecutablesDialog()) {
- setExecutableIndex(previousIndex);
- }
- } else {
- setExecutableIndex(index);
- }
- }
-}
-
-void MainWindow::helpTriggered()
-{
- QWhatsThis::enterWhatsThisMode();
-}
-
-void MainWindow::wikiTriggered()
-{
- QDesktopServices::openUrl(QUrl("http://wiki.step-project.com/Guide:Mod_Organizer"));
-}
-
-void MainWindow::issueTriggered()
-{
- QDesktopServices::openUrl(QUrl("https://github.com/Modorganizer2/modorganizer/issues"));
-}
-
-void MainWindow::tutorialTriggered()
-{
- QAction *tutorialAction = qobject_cast<QAction*>(sender());
- if (tutorialAction != nullptr) {
- if (QMessageBox::question(this, tr("Start Tutorial?"),
- tr("You're about to start a tutorial. For technical reasons it's not possible to end "
- "the tutorial early. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- TutorialManager::instance().activateTutorial("MainWindow", tutorialAction->data().toString());
- }
- }
-}
-
-
-void MainWindow::on_actionInstallMod_triggered()
-{
- installMod();
-}
-
-void MainWindow::on_actionAdd_Profile_triggered()
-{
- for (;;) {
- ProfilesDialog profilesDialog(m_OrganizerCore.currentProfile()->name(),
- m_OrganizerCore.managedGame(),
- this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(profilesDialog.objectName());
- if (settings.contains(key)) {
- profilesDialog.restoreGeometry(settings.value(key).toByteArray());
- }
- // workaround: need to disable monitoring of the saves directory, otherwise the active
- // profile directory is locked
- stopMonitorSaves();
- profilesDialog.exec();
- settings.setValue(key, profilesDialog.saveGeometry());
- refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
- if (refreshProfiles() && !profilesDialog.failed()) {
- break;
- }
- }
-
- LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature<LocalSavegames>();
- if (saveGames != nullptr) {
- if (saveGames->prepareProfile(m_OrganizerCore.currentProfile()))
- refreshSaveList();
- }
-
- BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
- if (invalidation != nullptr) {
- if (invalidation->prepareProfile(m_OrganizerCore.currentProfile()))
- QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh()));
- }
-}
-
-void MainWindow::on_actionModify_Executables_triggered()
-{
- if (modifyExecutablesDialog()) {
- setExecutableIndex(m_OldExecutableIndex);
- }
-}
-
-
-void MainWindow::setModListSorting(int index)
-{
- Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder;
- int column = index >> 1;
- ui->modList->header()->setSortIndicator(column, order);
-}
-
-
-void MainWindow::setESPListSorting(int index)
-{
- switch (index) {
- case 0: {
- ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder);
- } break;
- case 1: {
- ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder);
- } break;
- case 2: {
- ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder);
- } break;
- case 3: {
- ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder);
- } break;
- }
-}
-
-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);
- }
-}
-
-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();
-}
-
-void MainWindow::modorder_changed()
-{
- for (unsigned int i = 0; i < m_OrganizerCore.currentProfile()->numMods(); ++i) {
- int priority = m_OrganizerCore.currentProfile()->getModPriority(i);
- if (m_OrganizerCore.currentProfile()->modEnabled(i)) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
- // priorities in the directory structure are one higher because data is 0
- m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->internalName())).setPriority(priority + 1);
- }
- }
- m_OrganizerCore.refreshBSAList();
- m_OrganizerCore.currentProfile()->writeModlist();
- m_ArchiveListWriter.write();
- m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins();
-
- { // refresh selection
- QModelIndex current = ui->modList->currentIndex();
- if (current.isValid()) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt());
- // clear caches on all mods conflicting with the moved mod
- for (int i : modInfo->getModOverwrite()) {
- ModInfo::getByIndex(i)->clearCaches();
- }
- for (int i : modInfo->getModOverwritten()) {
- ModInfo::getByIndex(i)->clearCaches();
- }
- // update conflict check on the moved mod
- modInfo->doConflictCheck();
- m_OrganizerCore.modList()->setOverwriteMarkers(modInfo->getModOverwrite(), modInfo->getModOverwritten());
- if (m_ModListSortProxy != nullptr) {
- m_ModListSortProxy->invalidate();
- }
- ui->modList->verticalScrollBar()->repaint();
- }
- }
-}
-
-void MainWindow::modInstalled(const QString &modName)
-{
- QModelIndexList posList =
- m_OrganizerCore.modList()->match(m_OrganizerCore.modList()->index(0, 0), Qt::DisplayRole, modName);
- if (posList.count() == 1) {
- ui->modList->scrollTo(posList.at(0));
- }
-}
-
-void MainWindow::procError(QProcess::ProcessError error)
-{
- reportError(tr("failed to spawn notepad.exe: %1").arg(error));
- this->sender()->deleteLater();
-}
-
-void MainWindow::procFinished(int, QProcess::ExitStatus)
-{
- this->sender()->deleteLater();
-}
-
-void MainWindow::showMessage(const QString &message)
-{
- MessageDialog::showMessage(message, this);
-}
-
-void MainWindow::showError(const QString &message)
-{
- reportError(message);
-}
-
-void MainWindow::installMod_clicked()
-{
- installMod();
-}
-
-void MainWindow::modRenamed(const QString &oldName, const QString &newName)
-{
- Profile::renameModInAllProfiles(oldName, newName);
-
- // immediately refresh the active profile because the data in memory is invalid
- m_OrganizerCore.currentProfile()->refreshModStatus();
-
- // also fix the directory structure
- try {
- if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldName))) {
- FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldName));
- origin.setName(ToWString(newName));
- } else {
-
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to change origin name: %1").arg(e.what()));
- }
-}
-
-void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName)
-{
- const FileEntry::Ptr filePtr = m_OrganizerCore.directoryStructure()->findFile(ToWString(filePath));
- if (filePtr.get() != nullptr) {
- try {
- if (m_OrganizerCore.directoryStructure()->originExists(ToWString(newOriginName))) {
- FilesOrigin &newOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(newOriginName));
-
- QString fullNewPath = ToQString(newOrigin.getPath()) + "\\" + filePath;
- WIN32_FIND_DATAW findData;
- HANDLE hFind;
- hFind = ::FindFirstFileW(ToWString(fullNewPath).c_str(), &findData);
- filePtr->addOrigin(newOrigin.getID(), findData.ftCreationTime, L"");
- FindClose(hFind);
- }
- if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldOriginName))) {
- FilesOrigin &oldOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldOriginName));
- filePtr->removeOrigin(oldOrigin.getID());
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to move \"%1\" from mod \"%2\" to \"%3\": %4").arg(filePath).arg(oldOriginName).arg(newOriginName).arg(e.what()));
- }
- } else {
- // this is probably not an error, the specified path is likely a directory
- }
-}
-
-QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type)
-{
- QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name));
- item->setData(0, Qt::ToolTipRole, name);
- item->setData(0, Qt::UserRole, categoryID);
- item->setData(0, Qt::UserRole + 1, type);
- if (root != nullptr) {
- root->addChild(item);
- } else {
- ui->categoriesList->addTopLevelItem(item);
- }
- return item;
-}
-
-void MainWindow::addContentFilters()
-{
- for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) {
- addFilterItem(nullptr, tr("<Contains %1>").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT);
- }
-}
-
-void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID)
-{
- for (unsigned int i = 1;
- i < static_cast<unsigned int>(m_CategoryFactory.numCategories()); ++i) {
- if ((m_CategoryFactory.getParentID(i) == targetID)) {
- int categoryID = m_CategoryFactory.getCategoryID(i);
- if (categoriesUsed.find(categoryID) != categoriesUsed.end()) {
- QTreeWidgetItem *item =
- addFilterItem(root, m_CategoryFactory.getCategoryName(i),
- categoryID, ModListSortProxy::TYPE_CATEGORY);
- if (m_CategoryFactory.hasChildren(i)) {
- addCategoryFilters(item, categoriesUsed, categoryID);
- }
- }
- }
- }
-}
-
-void MainWindow::refreshFilters()
-{
- QItemSelection currentSelection = ui->modList->selectionModel()->selection();
-
- QVariant currentIndexName = ui->modList->currentIndex().data();
- ui->modList->setCurrentIndex(QModelIndex());
-
- QStringList selectedItems;
- for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) {
- selectedItems.append(item->text(0));
- }
-
- ui->categoriesList->clear();
- addFilterItem(nullptr, tr("<Checked>"), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Unchecked>"), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Update>"), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Mod Backup>"), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Managed by MO>"), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Managed outside MO>"), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<No category>"), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Conflicted>"), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Not Endorsed>"), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL);
-
- addContentFilters();
- std::set<int> categoriesUsed;
- for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx);
- for (int categoryID : modInfo->getCategories()) {
- int currentID = categoryID;
- std::set<int> cycleTest;
- // also add parents so they show up in the tree
- while (currentID != 0) {
- categoriesUsed.insert(currentID);
- if (!cycleTest.insert(currentID).second) {
- qWarning("cycle in categories: %s", qPrintable(SetJoin(cycleTest, ", ")));
- break;
- }
- currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID));
- }
- }
- }
-
- addCategoryFilters(nullptr, categoriesUsed, 0);
-
- for (const QString &item : selectedItems) {
- QList<QTreeWidgetItem*> matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive);
- if (matches.size() > 0) {
- matches.at(0)->setSelected(true);
- }
- }
- ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select);
- QModelIndexList matchList;
- if (currentIndexName.isValid()) {
- matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName);
- }
-
- if (matchList.size() > 0) {
- ui->modList->setCurrentIndex(matchList.at(0));
- }
-}
-
-
-void MainWindow::renameMod_clicked()
-{
- try {
- ui->modList->edit(ui->modList->currentIndex());
- } catch (const std::exception &e) {
- reportError(tr("failed to rename mod: %1").arg(e.what()));
- }
-}
-
-
-void MainWindow::restoreBackup_clicked()
-{
- QRegExp backupRegEx("(.*)_backup[0-9]*$");
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- if (backupRegEx.indexIn(modInfo->name()) != -1) {
- QString regName = backupRegEx.cap(1);
- QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()));
- if (!modDir.exists(regName) ||
- (QMessageBox::question(this, tr("Overwrite?"),
- tr("This will replace the existing mod \"%1\". Continue?").arg(regName),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) {
- if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) {
- reportError(tr("failed to remove mod \"%1\"").arg(regName));
- } else {
- QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()) + "/" + regName;
- if (!modDir.rename(modInfo->absolutePath(), destinationPath)) {
- reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath));
- }
- m_OrganizerCore.refreshModList();
- }
- }
- }
-}
-
-void MainWindow::modlistChanged(const QModelIndex&, int)
-{
- m_OrganizerCore.currentProfile()->writeModlist();
-}
-
-void MainWindow::modlistSelectionChanged(const QModelIndex &current, const QModelIndex&)
-{
- if (current.isValid()) {
- ModInfo::Ptr selectedMod = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt());
- m_OrganizerCore.modList()->setOverwriteMarkers(selectedMod->getModOverwrite(), selectedMod->getModOverwritten());
- } else {
- m_OrganizerCore.modList()->setOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>());
- }
-/* if ((m_ModListSortProxy != nullptr)
- && !m_ModListSortProxy->beingInvalidated()) {
- m_ModListSortProxy->invalidate();
- }*/
- ui->modList->verticalScrollBar()->repaint();
-}
-
-void MainWindow::modlistSelectionsChanged(const QItemSelection &selected)
-{
- m_OrganizerCore.pluginList()->highlightPlugins(ui->modList->selectionModel(), *m_OrganizerCore.directoryStructure(), *m_OrganizerCore.currentProfile());
- ui->espList->verticalScrollBar()->repaint();
-}
-
-void MainWindow::esplistSelectionsChanged(const QItemSelection &selected)
-{
- m_OrganizerCore.modList()->highlightMods(ui->espList->selectionModel(), *m_OrganizerCore.directoryStructure());
- ui->modList->verticalScrollBar()->repaint();
-}
-
-void MainWindow::modListSortIndicatorChanged(int, Qt::SortOrder)
-{
- ui->modList->verticalScrollBar()->repaint();
-}
-
-void MainWindow::removeMod_clicked()
-{
- try {
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- QString mods;
- QStringList modNames;
- 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>";
- modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name());
- }
- if (QMessageBox::question(this, tr("Confirm"),
- tr("Remove the following mods?<br><ul>%1</ul>").arg(mods),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- // use mod names instead of indexes because those become invalid during the removal
- DownloadManager::startDisableDirWatcher();
- for (QString name : modNames) {
- m_OrganizerCore.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex());
- }
- DownloadManager::endDisableDirWatcher();
- }
- } else {
- m_OrganizerCore.modList()->removeRow(m_ContextRow, QModelIndex());
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to remove mod: %1").arg(e.what()));
- }
-}
-
-
-void MainWindow::modRemoved(const QString &fileName)
-{
- if (!fileName.isEmpty() && !QFileInfo(fileName).isAbsolute()) {
- m_OrganizerCore.downloadManager()->markUninstalled(fileName);
- }
-}
-
-
-void MainWindow::reinstallMod_clicked()
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- QString installationFile = modInfo->getInstallationFile();
- if (installationFile.length() != 0) {
- QString fullInstallationFile;
- QFileInfo fileInfo(installationFile);
- if (fileInfo.isAbsolute()) {
- if (fileInfo.exists()) {
- fullInstallationFile = installationFile;
- } else {
- fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName();
- }
- } else {
- fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + installationFile;
- }
- if (QFile::exists(fullInstallationFile)) {
- m_OrganizerCore.installMod(fullInstallationFile, modInfo->name());
- } else {
- QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists"));
- }
- } else {
- QMessageBox::information(this, tr("Failed"),
- tr("Mods installed with old versions of MO can't be reinstalled in this way."));
- }
-}
-
-void MainWindow::backupMod_clicked()
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- QString backupDirectory = m_OrganizerCore.installationManager()->generateBackupName(modInfo->absolutePath());
- if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) {
- QMessageBox::information(this, tr("Failed"),
- tr("Failed to create backup."));
- }
- m_OrganizerCore.refreshModList();
-}
-
-void MainWindow::resumeDownload(int downloadIndex)
-{
- if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) {
- m_OrganizerCore.downloadManager()->resumeDownload(downloadIndex);
- } else {
- QString username, password;
- if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
- m_OrganizerCore.doAfterLogin([this, downloadIndex] () {
- this->resumeDownload(downloadIndex);
- });
- NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
- } 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()->loggedIn()) {
- mod->endorse(true);
- } else {
- QString username, password;
- if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
- m_OrganizerCore.doAfterLogin(boost::bind(&MainWindow::endorseMod, this, mod));
- NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
- } else {
- MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this);
- }
- }
-}
-
-
-void MainWindow::endorse_clicked()
-{
- endorseMod(ModInfo::getByIndex(m_ContextRow));
-}
-
-void MainWindow::dontendorse_clicked()
-{
- ModInfo::getByIndex(m_ContextRow)->setNeverEndorse();
-}
-
-
-void MainWindow::unendorse_clicked()
-{
- QString username, password;
- if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) {
- ModInfo::getByIndex(m_ContextRow)->endorse(false);
- } else {
- if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
- m_OrganizerCore.doAfterLogin([this] () { this->unendorse_clicked(); });
- NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
- } else {
- MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this);
- }
- }
-}
-
-void MainWindow::loginFailed(const QString &error)
-{
- qDebug("login failed: %s", qPrintable(error));
- statusBar()->hide();
-}
-
-void MainWindow::windowTutorialFinished(const QString &windowName)
-{
- m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true);
-}
-
-void MainWindow::overwriteClosed(int)
-{
- OverwriteInfoDialog *dialog = this->findChild<OverwriteInfoDialog*>("__overwriteDialog");
- if (dialog != nullptr) {
- m_OrganizerCore.modList()->modInfoChanged(dialog->modInfo());
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog->objectName());
- settings.setValue(key, dialog->saveGeometry());
- dialog->deleteLater();
- }
- m_OrganizerCore.refreshDirectoryStructure();
-}
-
-
-void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab)
-{
- if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) {
- qDebug("A different mod information dialog is open. If this is incorrect, please restart MO");
- return;
- }
- std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) {
- QDialog *dialog = this->findChild<QDialog*>("__overwriteDialog");
- try {
- if (dialog == nullptr) {
- dialog = new OverwriteInfoDialog(modInfo, this);
- dialog->setObjectName("__overwriteDialog");
- } else {
- qobject_cast<OverwriteInfoDialog*>(dialog)->setModInfo(modInfo);
- }
- 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->show();
- dialog->raise();
- dialog->activateWindow();
- connect(dialog, SIGNAL(finished(int)), this, SLOT(overwriteClosed(int)));
- } catch (const std::exception &e) {
- reportError(tr("Failed to display overwrite dialog: %1").arg(e.what()));
- }
- } 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);
- }
-
- 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());
- }
-
- //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.exec();
- m_OrganizerCore.settings().directInterface().setValue("mod_info_tabs", dialog.saveTabState());
- settings.setValue(key, dialog.saveGeometry());
-
-
- modInfo->saveMeta();
- emit modInfoDisplayed();
- m_OrganizerCore.modList()->modInfoChanged(modInfo);
- }
-
- if (m_OrganizerCore.currentProfile()->modEnabled(index)
- && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) {
- FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name()));
- origin.enable(false);
-
- if (m_OrganizerCore.directoryStructure()->originExists(ToWString(modInfo->name()))) {
- FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name()));
- origin.enable(false);
-
- m_OrganizerCore.directoryRefresher()->addModToStructure(m_OrganizerCore.directoryStructure()
- , modInfo->name()
- , m_OrganizerCore.currentProfile()->getModPriority(index)
- , modInfo->absolutePath()
- , modInfo->stealFiles()
- , modInfo->archives());
- DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure());
- m_OrganizerCore.refreshLists();
- }
- }
-}
-
-bool MainWindow::closeWindow()
-{
- return close();
-}
-
-void MainWindow::setWindowEnabled(bool enabled)
-{
- setEnabled(enabled);
-}
-
-
-void MainWindow::modOpenNext(int tab)
-{
- QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0));
- index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0);
-
- 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);
- }
-}
-
-void MainWindow::modOpenPrev(int tab)
-{
- 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;
- }
-
- 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);
- }
-}
-
-void MainWindow::displayModInformation(const QString &modName, int tab)
-{
- unsigned int index = ModInfo::getIndex(modName);
- if (index == UINT_MAX) {
- qCritical("failed to resolve mod name %s", modName.toUtf8().constData());
- return;
- }
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
- displayModInformation(modInfo, index, tab);
-}
-
-
-void MainWindow::displayModInformation(int row, int tab)
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
- displayModInformation(modInfo, row, tab);
-}
-
-
-void MainWindow::ignoreMissingData_clicked()
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
- int row_idx = idx.data(Qt::UserRole + 1).toInt();
- ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
- //QDir(info->absolutePath()).mkdir("textures");
- info->testValid();
- info->markValidated(true);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
-
- emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1));
- }
- } else {
- ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
- //QDir(info->absolutePath()).mkdir("textures");
- info->testValid();
- info->markValidated(true);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
-
- emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1));
- }
-}
-
-void MainWindow::markConverted_clicked()
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
- int row_idx = idx.data(Qt::UserRole + 1).toInt();
- ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
- info->markConverted(true);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
- emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1));
- }
- } else {
- ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
- info->markConverted(true);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
- emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1));
- }
-}
-
-
-void MainWindow::visitOnNexus_clicked()
-{
- int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt();
- QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString();
- if (modID > 0) {
- linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName));
- } else {
- MessageDialog::showMessage(tr("Nexus ID for this Mod is unknown"), this);
- }
-}
-
-void MainWindow::visitWebPage_clicked()
-{
- 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);
- }
-}
-
-void MainWindow::openExplorer_clicked()
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- 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);
- }
- }
- else {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- }
-}
-
-void MainWindow::openOriginExplorer_clicked()
-{
- QItemSelectionModel *selection = ui->espList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 0) {
- for (QModelIndex idx : selection->selectedRows()) {
- 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);
- }
- }
- 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);
- }
-}
-
-void MainWindow::openExplorer_activated()
-{
- if (ui->modList->hasFocus()) {
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() == 1 ) {
-
- QModelIndex idx = selection->currentIndex();
- ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
- 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);
- }
-
- }
- }
-
- if (ui->espList->hasFocus()) {
- QItemSelectionModel *selection = ui->espList->selectionModel();
-
- if (selection->hasSelection() && selection->selectedRows().count() == 1) {
-
- 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();
-
- 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);
- }
-
- }
- }
-}
-
-void MainWindow::refreshProfile_activated()
-{
- m_OrganizerCore.profileRefresh();
-}
-
-void MainWindow::search_activated()
-{
- if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) {
- ui->modFilterEdit->setFocus();
- ui->modFilterEdit->setSelection(0, INT_MAX);
- }
-
- if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) {
- ui->espFilterEdit->setFocus();
- ui->espFilterEdit->setSelection(0, INT_MAX);
- }
-}
-
-void MainWindow::searchClear_activated()
-{
- if (ui->modFilterEdit->hasFocus()) {
- ui->modFilterEdit->clear();
- ui->modList->setFocus();
- }
-
- if (ui->espFilterEdit->hasFocus()) {
- ui->espFilterEdit->clear();
- ui->espList->setFocus();
- }
-}
-
-void MainWindow::information_clicked()
-{
- try {
- displayModInformation(m_ContextRow);
- } catch (const std::exception &e) {
- reportError(e.what());
- }
-}
-
-void MainWindow::createEmptyMod_clicked()
-{
- GuessedValue<QString> name;
- name.setFilter(&fixDirectoryName);
-
- while (name->isEmpty()) {
- bool ok;
- name.update(QInputDialog::getText(this, tr("Create Mod..."),
- tr("This will create an empty mod.\n"
- "Please enter a name:"), QLineEdit::Normal, "", &ok),
- GUESS_USER);
- if (!ok) {
- return;
- }
- }
-
- if (m_OrganizerCore.getMod(name) != nullptr) {
- reportError(tr("A mod with this name already exists"));
- return;
- }
-
- int newPriority = -1;
- if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) {
- newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow);
- }
-
- IModInterface *newMod = m_OrganizerCore.createMod(name);
- if (newMod == nullptr) {
- return;
- }
-
- m_OrganizerCore.refreshModList();
-
- if (newPriority >= 0) {
- m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
- }
-}
-
-void MainWindow::createSeparator_clicked()
-{
- GuessedValue<QString> name;
- name.setFilter(&fixDirectoryName);
- while (name->isEmpty())
- {
- bool ok;
- name.update(QInputDialog::getText(this, tr("Create Separator..."),
- tr("This will create a new separator.\n"
- "Please enter a name:"), QLineEdit::Normal, "", &ok),
- GUESS_USER);
- if (!ok) { return; }
- }
- if (m_OrganizerCore.getMod(name) != nullptr)
- {
- reportError(tr("A separator with this name already exists"));
- return;
- }
- name->append("_separator");
- if (m_OrganizerCore.getMod(name) != nullptr)
- {
- return;
- }
-
- int newPriority = -1;
- if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY)
- {
- newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow);
- }
-
- if (m_OrganizerCore.createMod(name) == nullptr) { return; }
- m_OrganizerCore.refreshModList();
-
- if (newPriority >= 0)
- {
- m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
- }
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QColor previousColor = settings.value("previousSeparatorColor", QColor()).value<QColor>();
- if (previousColor.isValid()) {
- ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(previousColor);
- }
-
-}
-
-void MainWindow::setColor_clicked()
-{
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- QColorDialog dialog(this);
- QColor currentColor = modInfo->getColor();
- QColor previousColor = settings.value("previousSeparatorColor", QColor()).value<QColor>();
- if (currentColor.isValid())
- dialog.setCurrentColor(currentColor);
- else
- dialog.setCurrentColor(previousColor);
- if (!dialog.exec())
- return;
- currentColor = dialog.currentColor();
- if (!currentColor.isValid())
- return;
- settings.setValue("previousSeparatorColor", currentColor);
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
- ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
- auto flags = info->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
- {
- info->setColor(currentColor);
- }
- }
- }
- else {
- modInfo->setColor(currentColor);
- }
-}
-
-void MainWindow::resetColor_clicked()
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- QColor color = QColor();
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
- ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
- auto flags = info->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
- {
- info->setColor(color);
- }
- }
- }
- else {
- modInfo->setColor(color);
- }
- Settings::instance().directInterface().remove("previousSeparatorColor");
-}
-
-void MainWindow::createModFromOverwrite()
-{
- GuessedValue<QString> name;
- name.setFilter(&fixDirectoryName);
-
- while (name->isEmpty()) {
- bool ok;
- name.update(QInputDialog::getText(this, tr("Create Mod..."),
- tr("This will move all files from overwrite into a new, regular mod.\n"
- "Please enter a name:"), QLineEdit::Normal, "", &ok),
- GUESS_USER);
- if (!ok) {
- return;
- }
- }
-
- if (m_OrganizerCore.getMod(name) != nullptr) {
- reportError(tr("A mod with this name already exists"));
- return;
- }
-
- IModInterface *newMod = m_OrganizerCore.createMod(name);
- if (newMod == nullptr) {
- return;
- }
-
- unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
- std::vector<ModInfo::EFlag> flags = mod->getFlags();
- return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); });
-
- ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex);
- shellMove(QStringList(QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"),
- QStringList(QDir::toNativeSeparators(newMod->absolutePath())), this);
-
- m_OrganizerCore.refreshModList();
-}
-
-void MainWindow::clearOverwrite()
-{
- unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
- std::vector<ModInfo::EFlag> flags = mod->getFlags();
- return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE)
- != flags.end();
- });
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex);
- if (modInfo)
- {
- QDir overwriteDir(modInfo->absolutePath());
- if (QMessageBox::question(this, tr("Are you sure?"),
- tr("About to recursively delete:\n") + overwriteDir.absolutePath(),
- QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok)
- {
- QStringList delList;
- for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot))
- delList.push_back(overwriteDir.absoluteFilePath(f));
- shellDelete(delList, true);
- updateProblemsButton();
- }
- }
-}
-
-void MainWindow::cancelModListEditor()
-{
- ui->modList->setEnabled(false);
- ui->modList->setEnabled(true);
-}
-
-void MainWindow::on_modList_doubleClicked(const QModelIndex &index)
-{
- if (!index.isValid()) {
- return;
- }
-
- if (m_OrganizerCore.modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) {
- // don't interpret double click if we only just checked a mod
- return;
- }
-
- QModelIndex sourceIdx = mapToModel(m_OrganizerCore.modList(), index);
- if (!sourceIdx.isValid()) {
- return;
- }
-
- Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers();
- if (modifiers.testFlag(Qt::ControlModifier)) {
- try {
- m_ContextRow = m_ModListSortProxy->mapToSource(index).row();
- openExplorer_clicked();
- // workaround to cancel the editor that might have opened because of
- // selection-click
- ui->modList->closePersistentEditor(index);
- }
- catch (const std::exception &e) {
- reportError(e.what());
- }
- }
- else {
- try {
- m_ContextRow = m_ModListSortProxy->mapToSource(index).row();
- displayModInformation(sourceIdx.row());
- // workaround to cancel the editor that might have opened because of
- // selection-click
- ui->modList->closePersistentEditor(index);
- }
- catch (const std::exception &e) {
- reportError(e.what());
- }
- }
-}
-
-void MainWindow::on_listOptionsBtn_pressed()
-{
- m_ContextRow = -1;
-}
-
-void MainWindow::openOriginInformation_clicked()
-{
- try {
- QItemSelectionModel *selection = ui->espList->selectionModel();
- //we don't want to open multiple modinfodialogs.
- /*if (selection->hasSelection() && selection->selectedRows().count() > 0) {
-
- for (QModelIndex idx : selection->selectedRows()) {
- QString fileName = idx.data().toString();
- ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
- std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
-
- if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
- displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
- }
- }
- }
- 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();
-
- if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
- displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
- }
- }
- catch (const std::exception &e) {
- reportError(e.what());
- }
-}
-
-void MainWindow::on_espList_doubleClicked(const QModelIndex &index)
-{
- if (!index.isValid()) {
- return;
- }
-
- if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) {
- // don't interpret double click if we only just checked a plugin
- return;
- }
-
- QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index);
- if (!sourceIdx.isValid()) {
- return;
- }
- try {
-
- QItemSelectionModel *selection = ui->espList->selectionModel();
-
- if (selection->hasSelection() && selection->selectedRows().count() == 1) {
-
- QModelIndex idx = selection->currentIndex();
- QString fileName = idx.data().toString();
-
- if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX)
- return;
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
- std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
-
- if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
-
- Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers();
- if (modifiers.testFlag(Qt::ControlModifier)) {
- openExplorer_activated();
- // workaround to cancel the editor that might have opened because of
- // selection-click
- ui->espList->closePersistentEditor(index);
- }
- else {
-
- displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
- // workaround to cancel the editor that might have opened because of
- // selection-click
- ui->espList->closePersistentEditor(index);
- }
- }
- }
- }
- catch (const std::exception &e) {
- reportError(e.what());
- }
-}
-
-bool MainWindow::populateMenuCategories(QMenu *menu, int targetID)
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- const std::set<int> &categories = modInfo->getCategories();
-
- bool childEnabled = false;
-
- for (unsigned int i = 1; i < m_CategoryFactory.numCategories(); ++i) {
- if (m_CategoryFactory.getParentID(i) == targetID) {
- QMenu *targetMenu = menu;
- if (m_CategoryFactory.hasChildren(i)) {
- targetMenu = menu->addMenu(m_CategoryFactory.getCategoryName(i).replace('&', "&&"));
- }
-
- int id = m_CategoryFactory.getCategoryID(i);
- QScopedPointer<QCheckBox> checkBox(new QCheckBox(targetMenu));
- bool enabled = categories.find(id) != categories.end();
- checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&"));
- if (enabled) {
- childEnabled = true;
- }
- checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked);
-
- QScopedPointer<QWidgetAction> checkableAction(new QWidgetAction(targetMenu));
- checkableAction->setDefaultWidget(checkBox.take());
- checkableAction->setData(id);
- targetMenu->addAction(checkableAction.take());
-
- if (m_CategoryFactory.hasChildren(i)) {
- if (populateMenuCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) {
- targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png"));
- }
- }
- }
- }
- return childEnabled;
-}
-
-void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow)
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow);
- for (QAction* action : menu->actions()) {
- if (action->menu() != nullptr) {
- replaceCategoriesFromMenu(action->menu(), modRow);
- } else {
- QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action);
- if (widgetAction != nullptr) {
- QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget());
- modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked());
- }
- }
- }
-}
-
-void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow)
-{
- if (referenceRow != -1 && referenceRow != modRow) {
- ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow);
- for (QAction* action : menu->actions()) {
- if (action->menu() != nullptr) {
- addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow);
- } else {
- QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action);
- if (widgetAction != nullptr) {
- QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget());
- int categoryId = widgetAction->data().toInt();
- bool checkedBefore = editedModInfo->categorySet(categoryId);
- bool checkedAfter = checkbox->isChecked();
-
- if (checkedBefore != checkedAfter) { // only update if the category was changed on the edited mod
- ModInfo::Ptr currentModInfo = ModInfo::getByIndex(modRow);
- currentModInfo->setCategory(categoryId, checkedAfter);
- }
- }
- }
- }
- } else {
- replaceCategoriesFromMenu(menu, modRow);
- }
-}
-
-void MainWindow::addRemoveCategories_MenuHandler() {
- QMenu *menu = qobject_cast<QMenu*>(sender());
- if (menu == nullptr) {
- qCritical("not a menu?");
- return;
- }
-
- QList<QPersistentModelIndex> selected;
- for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) {
- selected.append(QPersistentModelIndex(idx));
- }
-
- if (selected.size() > 0) {
- int minRow = INT_MAX;
- int maxRow = -1;
-
- for (const QPersistentModelIndex &idx : selected) {
- qDebug("change categories on: %s", qPrintable(idx.data().toString()));
- QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx);
- if (modIdx.row() != m_ContextIdx.row()) {
- addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row());
- }
- if (idx.row() < minRow) minRow = idx.row();
- if (idx.row() > maxRow) maxRow = idx.row();
- }
- replaceCategoriesFromMenu(menu, m_ContextIdx.row());
-
- m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1);
-
- for (const QPersistentModelIndex &idx : selected) {
- ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
- }
- } else {
- //For single mod selections, just do a replace
- replaceCategoriesFromMenu(menu, m_ContextRow);
- m_OrganizerCore.modList()->notifyChange(m_ContextRow);
- }
-
- refreshFilters();
-}
-
-void MainWindow::replaceCategories_MenuHandler() {
- QMenu *menu = qobject_cast<QMenu*>(sender());
- if (menu == nullptr) {
- qCritical("not a menu?");
- return;
- }
-
- QList<QPersistentModelIndex> selected;
- for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) {
- selected.append(QPersistentModelIndex(idx));
- }
-
- if (selected.size() > 0) {
- QStringList selectedMods;
- int minRow = INT_MAX;
- int maxRow = -1;
- for (int i = 0; i < selected.size(); ++i) {
- QModelIndex temp = mapToModel(m_OrganizerCore.modList(), selected.at(i));
- selectedMods.append(temp.data().toString());
- replaceCategoriesFromMenu(menu, mapToModel(m_OrganizerCore.modList(), selected.at(i)).row());
- if (temp.row() < minRow) minRow = temp.row();
- if (temp.row() > maxRow) maxRow = temp.row();
- }
-
- m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1);
-
- // find mods by their name because indices are invalidated
- QAbstractItemModel *model = ui->modList->model();
- for (const QString &mod : selectedMods) {
- QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1,
- Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive);
- if (matches.size() > 0) {
- ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
- }
- }
- } else {
- //For single mod selections, just do a replace
- replaceCategoriesFromMenu(menu, m_ContextRow);
- m_OrganizerCore.modList()->notifyChange(m_ContextRow);
- }
-
- refreshFilters();
-}
-
-void MainWindow::saveArchiveList()
-{
- if (m_OrganizerCore.isArchivesInit()) {
- SafeWriteFile archiveFile(m_OrganizerCore.currentProfile()->getArchivesFileName());
- for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) {
- QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i);
- for (int j = 0; j < tlItem->childCount(); ++j) {
- QTreeWidgetItem * item = tlItem->child(j);
- if (item->checkState(0) == Qt::Checked) {
- archiveFile->write(item->text(0).toUtf8().append("\r\n"));
- }
- }
- }
- if (archiveFile.commitIfDifferent(m_ArchiveListHash)) {
- qDebug("%s saved", qPrintable(QDir::toNativeSeparators(m_OrganizerCore.currentProfile()->getArchivesFileName())));
- }
- } else {
- qWarning("archive list not initialised");
- }
-}
-
-void MainWindow::checkModsForUpdates()
-{
- statusBar()->show();
- if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) {
- m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this);
- m_RefreshProgress->setRange(0, m_ModsToUpdate);
- } else {
- QString username, password;
- if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
- m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); });
- NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
- } else { // otherwise there will be no endorsement info
- MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"),
- this, true);
- m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this);
- }
- }
-}
-
-void MainWindow::changeVersioningScheme() {
- if (QMessageBox::question(this, tr("Continue?"),
- tr("The versioning scheme decides which version is considered newer than another.\n"
- "This function will guess the versioning scheme under the assumption that the installed version is outdated."),
- QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) {
-
- ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
-
- bool success = false;
-
- static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS };
-
- for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) {
- VersionInfo verOld(info->getVersion().canonicalString(), schemes[i]);
- VersionInfo verNew(info->getNewestVersion().canonicalString(), schemes[i]);
- if (verOld < verNew) {
- info->setVersion(verOld);
- info->setNewestVersion(verNew);
- success = true;
- }
- }
- if (!success) {
- QMessageBox::information(this, tr("Sorry"),
- tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->getNewestVersion().canonicalString()).arg(info->getVersion().canonicalString()),
- QMessageBox::Ok);
- }
- }
-}
-
-void MainWindow::ignoreUpdate() {
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
- ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
- info->ignoreUpdate(true);
- }
- }
- else {
- ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
- info->ignoreUpdate(true);
- }
-}
-
-void MainWindow::unignoreUpdate()
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
- ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
- info->ignoreUpdate(false);
- }
- }
- else {
- ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
- info->ignoreUpdate(false);
- }
-}
-
-void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu,
- ModInfo::Ptr info) {
- const std::set<int> &categories = info->getCategories();
- for (int categoryID : categories) {
- int catIdx = m_CategoryFactory.getCategoryIndex(categoryID);
- QWidgetAction *action = new QWidgetAction(primaryCategoryMenu);
- try {
- QRadioButton *categoryBox = new QRadioButton(
- m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"),
- primaryCategoryMenu);
- connect(categoryBox, &QRadioButton::toggled, [info, categoryID](bool enable) {
- if (enable) {
- info->setPrimaryCategory(categoryID);
- }
- });
- categoryBox->setChecked(categoryID == info->getPrimaryCategory());
- action->setDefaultWidget(categoryBox);
- } catch (const std::exception &e) {
- qCritical("failed to create category checkbox: %s", e.what());
- }
-
- action->setData(categoryID);
- primaryCategoryMenu->addAction(action);
- }
-}
-
-void MainWindow::addPrimaryCategoryCandidates()
-{
- QMenu *menu = qobject_cast<QMenu*>(sender());
- if (menu == nullptr) {
- qCritical("not a menu?");
- return;
- }
- menu->clear();
- ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
-
- addPrimaryCategoryCandidates(menu, modInfo);
-}
-
-void MainWindow::enableVisibleMods()
-{
- if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all visible mods?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- m_ModListSortProxy->enableAllVisible();
- }
-}
-
-void MainWindow::disableVisibleMods()
-{
- if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all visible mods?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- m_ModListSortProxy->disableAllVisible();
- }
-}
-
-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);
-}
-
-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);
-}
-
-void MainWindow::openInstallFolder()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(qApp->applicationDirPath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-void MainWindow::openPluginsFolder()
-{
- QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
- ::ShellExecuteW(nullptr, L"explore", ToWString(pluginsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-
-void MainWindow::openProfileFolder()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-void MainWindow::openIniFolder()
-{
- if (m_OrganizerCore.currentProfile()->localSettingsEnabled())
- {
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- }
- else {
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- }
-}
-
-void MainWindow::openDownloadsFolder()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getDownloadDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-void MainWindow::openModsFolder()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getModDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-void MainWindow::openGameFolder()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-void MainWindow::openMyGamesFolder()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-
-void MainWindow::exportModListCSV()
-{
- //SelectionDialog selection(tr("Choose what to export"));
-
- //selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0);
- //selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1);
- //selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2);
-
- QDialog selection(this);
- QGridLayout *grid = new QGridLayout;
- selection.setWindowTitle(tr("Export to csv"));
-
- QLabel *csvDescription = new QLabel();
- csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead."));
- grid->addWidget(csvDescription);
-
- QGroupBox *groupBoxRows = new QGroupBox(tr("Select what mods you want export:"));
- QRadioButton *all = new QRadioButton(tr("All installed mods"));
- QRadioButton *active = new QRadioButton(tr("Only active (checked) mods from your current profile"));
- QRadioButton *visible = new QRadioButton(tr("All currently visible mods in the mod list"));
-
- QVBoxLayout *vbox = new QVBoxLayout;
- vbox->addWidget(all);
- vbox->addWidget(active);
- vbox->addWidget(visible);
- vbox->addStretch(1);
- groupBoxRows->setLayout(vbox);
-
-
-
- grid->addWidget(groupBoxRows);
-
- QButtonGroup *buttonGroupRows = new QButtonGroup();
- buttonGroupRows->addButton(all, 0);
- buttonGroupRows->addButton(active, 1);
- buttonGroupRows->addButton(visible, 2);
- buttonGroupRows->button(0)->setChecked(true);
-
-
-
- QGroupBox *groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:"));
- groupBoxColumns->setFlat(true);
-
- QCheckBox *mod_Priority = new QCheckBox(tr("Mod_Priority"));
- mod_Priority->setChecked(true);
- QCheckBox *mod_Name = new QCheckBox(tr("Mod_Name"));
- mod_Name->setChecked(true);
- QCheckBox *mod_Status = new QCheckBox(tr("Mod_Status"));
- 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"));
- QCheckBox *mod_Version = new QCheckBox(tr("Mod_Version"));
- QCheckBox *install_Date = new QCheckBox(tr("Install_Date"));
- QCheckBox *download_File_Name = new QCheckBox(tr("Download_File_Name"));
-
- QVBoxLayout *vbox1 = new QVBoxLayout;
- vbox1->addWidget(mod_Priority);
- vbox1->addWidget(mod_Name);
- vbox1->addWidget(mod_Status);
- vbox1->addWidget(primary_Category);
- vbox1->addWidget(nexus_ID);
- vbox1->addWidget(mod_Nexus_URL);
- vbox1->addWidget(mod_Version);
- vbox1->addWidget(install_Date);
- vbox1->addWidget(download_File_Name);
- groupBoxColumns->setLayout(vbox1);
-
- grid->addWidget(groupBoxColumns);
-
- QPushButton *ok = new QPushButton("Ok");
- QPushButton *cancel = new QPushButton("Cancel");
- QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-
- connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept()));
- connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject()));
-
- grid->addWidget(buttons);
-
- selection.setLayout(grid);
-
-
- if (selection.exec() == QDialog::Accepted) {
-
- unsigned int numMods = ModInfo::getNumMods();
- int selectedRowID = buttonGroupRows->checkedId();
-
- try {
- QBuffer buffer;
- buffer.open(QIODevice::ReadWrite);
- CSVBuilder builder(&buffer);
- builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS);
- 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_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 (primary_Category->isChecked())
- fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING));
- if (nexus_ID->isChecked())
- fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER));
- if (mod_Nexus_URL->isChecked())
- fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING));
- if (mod_Version->isChecked())
- fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING));
- if (install_Date->isChecked())
- fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING));
- if (download_File_Name->isChecked())
- fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING));
-
- builder.setFields(fields);
-
- builder.writeHeader();
-
- for (unsigned int i = 0; i < numMods; ++i) {
- ModInfo::Ptr info = ModInfo::getByIndex(i);
- bool enabled = m_OrganizerCore.currentProfile()->modEnabled(i);
- if ((selectedRowID == 1) && !enabled) {
- continue;
- }
- else if ((selectedRowID == 2) && !m_ModListSortProxy->filterMatchesMod(info, enabled)) {
- continue;
- }
- std::vector<ModInfo::EFlag> flags = info->getFlags();
- 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')));
- if (mod_Name->isChecked())
- builder.setRowField("#Mod_Name", info->name());
- if (mod_Status->isChecked())
- builder.setRowField("#Mod_Status", (enabled)? "Enabled" : "Disabled");
- if (primary_Category->isChecked())
- builder.setRowField("#Primary_Category", (m_CategoryFactory.categoryExists(info->getPrimaryCategory())) ? m_CategoryFactory.getCategoryName(info->getPrimaryCategory()) : "");
- if (nexus_ID->isChecked())
- builder.setRowField("#Nexus_ID", info->getNexusID());
- if (mod_Nexus_URL->isChecked())
- builder.setRowField("#Mod_Nexus_URL",(info->getNexusID()>0)? NexusInterface::instance(&m_PluginContainer)->getModURL(info->getNexusID(), info->getGameName()) : "");
- if (mod_Version->isChecked())
- builder.setRowField("#Mod_Version", info->getVersion().canonicalString());
- if (install_Date->isChecked())
- builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss"));
- if (download_File_Name->isChecked())
- builder.setRowField("#Download_File_Name", info->getInstallationFile());
-
- builder.writeRow();
- }
- }
-
- SaveTextAsDialog saveDialog(this);
- saveDialog.setText(buffer.data());
- saveDialog.exec();
- }
- catch (const std::exception &e) {
- reportError(tr("export failed: %1").arg(e.what()));
- }
- }
-}
-
-static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu)
-{
- QPushButton *pushBtn = new QPushButton(subMenu->title());
- pushBtn->setMenu(subMenu);
- QWidgetAction *action = new QWidgetAction(menu);
- action->setDefaultWidget(pushBtn);
- menu->addAction(action);
-}
-
-QMenu *MainWindow::openFolderMenu()
-{
-
- QMenu *FolderMenu = new QMenu(this);
-
- FolderMenu->addAction(tr("Open Game folder"), this, SLOT(openGameFolder()));
-
- FolderMenu->addAction(tr("Open MyGames folder"), this, SLOT(openMyGamesFolder()));
-
- FolderMenu->addAction(tr("Open INIs folder"), this, SLOT(openIniFolder()));
-
- FolderMenu->addSeparator();
-
- FolderMenu->addAction(tr("Open Instance folder"), this, SLOT(openInstanceFolder()));
-
- FolderMenu->addAction(tr("Open Mods folder"), this, SLOT(openModsFolder()));
-
- FolderMenu->addAction(tr("Open Profile folder"), this, SLOT(openProfileFolder()));
-
- FolderMenu->addAction(tr("Open Downloads folder"), this, SLOT(openDownloadsFolder()));
-
- FolderMenu->addSeparator();
-
- FolderMenu->addAction(tr("Open MO2 Install folder"), this, SLOT(openInstallFolder()));
-
- FolderMenu->addAction(tr("Open MO2 Plugins folder"), this, SLOT(openPluginsFolder()));
-
- FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder()));
-
-
- return FolderMenu;
-}
-
-QMenu *MainWindow::modListContextMenu()
-{
- QMenu *menu = new QMenu(this);
- menu->addAction(tr("Install Mod..."), this, SLOT(installMod_clicked()));
-
- menu->addAction(tr("Create empty mod"), this, SLOT(createEmptyMod_clicked()));
-
- menu->addAction(tr("Create Separator"), this, SLOT(createSeparator_clicked()));
-
- menu->addSeparator();
-
- menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods()));
- menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods()));
-
- menu->addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates()));
-
- menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh()));
-
- menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV()));
-
-
- return menu;
-}
-
-void MainWindow::addModSendToContextMenu(QMenu *menu)
-{
- if (m_ModListSortProxy->sortColumn() != ModList::COL_PRIORITY)
- return;
-
- QMenu *sub_menu = new QMenu(this);
- sub_menu->setTitle(tr("Send to"));
- sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedModsToTop_clicked()));
- sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedModsToBottom_clicked()));
- sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedModsToPriority_clicked()));
- sub_menu->addAction(tr("Separator..."), this, SLOT(sendSelectedModsToSeparator_clicked()));
-
- menu->addMenu(sub_menu);
- menu->addSeparator();
-}
-
-void MainWindow::addPluginSendToContextMenu(QMenu *menu)
-{
- if (m_PluginListSortProxy->sortColumn() != PluginList::COL_PRIORITY)
- return;
-
- QMenu *sub_menu = new QMenu(this);
- sub_menu->setTitle(tr("Send to"));
- sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedPluginsToTop_clicked()));
- sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedPluginsToBottom_clicked()));
- sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedPluginsToPriority_clicked()));
-
- menu->addMenu(sub_menu);
- menu->addSeparator();
-}
-
-void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos)
-{
- try {
- QTreeView *modList = findChild<QTreeView*>("modList");
-
- m_ContextIdx = mapToModel(m_OrganizerCore.modList(), modList->indexAt(pos));
- m_ContextRow = m_ContextIdx.row();
-
- QMenu *menu = nullptr;
- QMenu *allMods = modListContextMenu();
- if (m_ContextRow == -1) {
- // no selection
- menu = allMods;
- } else {
- menu = new QMenu(this);
- allMods->setTitle(tr("All Mods"));
- menu->addMenu(allMods);
- menu->addSeparator();
- ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
- std::vector<ModInfo::EFlag> flags = info->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) {
- if (QDir(info->absolutePath()).count() > 2) {
- menu->addAction(tr("Sync to Mods..."), &m_OrganizerCore, SLOT(syncOverwrite()));
- menu->addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite()));
- menu->addAction(tr("Clear Overwrite..."), this, SLOT(clearOverwrite()));
- }
- menu->addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked()));
- } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) {
- menu->addAction(tr("Restore Backup"), this, SLOT(restoreBackup_clicked()));
- menu->addAction(tr("Remove Backup..."), this, SLOT(removeMod_clicked()));
- } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()){
- menu->addSeparator();
- QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"));
- populateMenuCategories(addRemoveCategoriesMenu, 0);
- connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler()));
- addMenuAsPushButton(menu, addRemoveCategoriesMenu);
- QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"));
- connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates()));
- addMenuAsPushButton(menu, primaryCategoryMenu);
- menu->addSeparator();
- menu->addAction(tr("Rename Separator..."), this, SLOT(renameMod_clicked()));
- menu->addAction(tr("Remove Separator..."), this, SLOT(removeMod_clicked()));
- menu->addSeparator();
- addModSendToContextMenu(menu);
- menu->addAction(tr("Select Color..."), this, SLOT(setColor_clicked()));
- if(info->getColor().isValid())
- menu->addAction(tr("Reset Color"), this, SLOT(resetColor_clicked()));
- menu->addSeparator();
- } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) {
- addModSendToContextMenu(menu);
- } else {
- QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"));
- populateMenuCategories(addRemoveCategoriesMenu, 0);
- connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler()));
- addMenuAsPushButton(menu, addRemoveCategoriesMenu);
-
- QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"));
- connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates()));
- addMenuAsPushButton(menu, primaryCategoryMenu);
-
- menu->addSeparator();
-
- if (info->downgradeAvailable()) {
- menu->addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme()));
- }
-
- if (info->updateAvailable() || info->downgradeAvailable()) {
- if (info->updateIgnored()) {
- menu->addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate()));
- } else {
- menu->addAction(tr("Ignore update"), this, SLOT(ignoreUpdate()));
- }
- }
-
- menu->addSeparator();
-
- menu->addAction(tr("Enable selected"), this, SLOT(enableSelectedMods_clicked()));
- menu->addAction(tr("Disable selected"), this, SLOT(disableSelectedMods_clicked()));
-
- menu->addSeparator();
-
- addModSendToContextMenu(menu);
-
- menu->addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked()));
- menu->addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked()));
- menu->addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked()));
- menu->addAction(tr("Create Backup"), this, SLOT(backupMod_clicked()));
-
- menu->addSeparator();
-
- if (info->getNexusID() > 0) {
- switch (info->endorsedState()) {
- case ModInfo::ENDORSED_TRUE: {
- menu->addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked()));
- } break;
- case ModInfo::ENDORSED_FALSE: {
- menu->addAction(tr("Endorse"), this, SLOT(endorse_clicked()));
- menu->addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked()));
- } break;
- case ModInfo::ENDORSED_NEVER: {
- menu->addAction(tr("Endorse"), this, SLOT(endorse_clicked()));
- } break;
- default: {
- QAction *action = new QAction(tr("Endorsement state unknown"), menu);
- action->setEnabled(false);
- menu->addAction(action);
- } break;
- }
- }
-
- menu->addSeparator();
-
- std::vector<ModInfo::EFlag> flags = info->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) {
- menu->addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked()));
- }
-
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) {
- menu->addAction(tr("Mark as converted/working"), this, SLOT(markConverted_clicked()));
- }
-
- 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()));
- }
-
- menu->addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked()));
- }
-
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) {
- QAction *infoAction = menu->addAction(tr("Information..."), this, SLOT(information_clicked()));
- menu->setDefaultAction(infoAction);
- }
- }
-
- menu->exec(modList->mapToGlobal(pos));
- } catch (const std::exception &e) {
- reportError(tr("Exception: ").arg(e.what()));
- } catch (...) {
- reportError(tr("Unknown exception"));
- }
-}
-
-
-void MainWindow::on_categoriesList_itemSelectionChanged()
-{
- QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows();
- std::vector<int> categories;
- std::vector<int> content;
- for (const QModelIndex &index : indices) {
- int filterType = index.data(Qt::UserRole + 1).toInt();
- if ((filterType == ModListSortProxy::TYPE_CATEGORY)
- || (filterType == ModListSortProxy::TYPE_SPECIAL)) {
- int categoryId = index.data(Qt::UserRole).toInt();
- if (categoryId != CategoryFactory::CATEGORY_NONE) {
- categories.push_back(categoryId);
- }
- } else if (filterType == ModListSortProxy::TYPE_CONTENT) {
- int contentId = index.data(Qt::UserRole).toInt();
- content.push_back(contentId);
- }
- }
-
- m_ModListSortProxy->setCategoryFilter(categories);
- m_ModListSortProxy->setContentFilter(content);
- ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0);
- //ui->clearFiltersButton->setStyleSheet("border:5px solid #ff0000;");
- ui->clearFiltersButton->setVisible(categories.size() > 0 || content.size() > 0);
-
- if (indices.count() == 0) {
- ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<All>")));
- } else if (indices.count() > 1) {
- ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<Multiple>")));
- } else {
- ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString()));
- }
- ui->modList->reset();
-}
-
-
-void MainWindow::deleteSavegame_clicked()
-{
- SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
-
- QString savesMsgLabel;
- QStringList deleteFiles;
-
- int count = 0;
-
- for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) {
- QString name = idx.data(Qt::UserRole).toString();
-
- if (count < 10) {
- savesMsgLabel += "<li>" + QFileInfo(name).completeBaseName() + "</li>";
- }
- ++count;
-
- if (info == nullptr) {
- deleteFiles.push_back(name);
- } else {
- ISaveGame const *save = info->getSaveGameInfo(name);
- deleteFiles += save->allFiles();
- delete save;
- }
- }
-
- if (count > 10) {
- savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
- }
-
- if (QMessageBox::question(this, tr("Confirm"),
- tr("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.", "", count)
- .arg(savesMsgLabel),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- shellDelete(deleteFiles, true); // recycle bin delete.
- refreshSaveList();
- }
-}
-
-
-void MainWindow::fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets)
-{
- ActivateModsDialog dialog(missingAssets, this);
- if (dialog.exec() == QDialog::Accepted) {
- // activate the required mods, then enable all esps
- std::set<QString> modsToActivate = dialog.getModsToActivate();
- for (std::set<QString>::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) {
- if ((*iter != "<data>") && (*iter != "<overwrite>")) {
- unsigned int modIndex = ModInfo::getIndex(*iter);
- m_OrganizerCore.currentProfile()->setModEnabled(modIndex, true);
- }
- }
-
- m_OrganizerCore.currentProfile()->writeModlist();
- m_OrganizerCore.refreshLists();
-
- std::set<QString> espsToActivate = dialog.getESPsToActivate();
- for (std::set<QString>::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) {
- m_OrganizerCore.pluginList()->enableESP(*iter);
- }
- m_OrganizerCore.saveCurrentLists();
- }
-}
-
-
-void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos)
-{
- QItemSelectionModel *selection = ui->savegameList->selectionModel();
-
- if (!selection->hasSelection()) {
- return;
- }
-
- QMenu menu;
- QAction *action = menu.addAction(tr("Enable Mods..."));
- action->setEnabled(false);
-
- if (selection->selectedIndexes().count() == 1) {
- SaveGameInfo const *info = this->m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
- if (info != nullptr) {
- QString save = ui->savegameList->currentItem()->data(Qt::UserRole).toString();
- SaveGameInfo::MissingAssets missing = info->getMissingAssets(save);
- if (missing.size() != 0) {
- connect(action, &QAction::triggered, this, [this, missing]{ fixMods_clicked(missing); });
- action->setEnabled(true);
- }
- }
- }
-
- QString deleteMenuLabel = tr("Delete %n save(s)", "", selection->selectedIndexes().count());
-
- menu.addAction(deleteMenuLabel, this, SLOT(deleteSavegame_clicked()));
-
- menu.exec(ui->savegameList->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();
-}
-
-namespace {
-QString getLinkfile(const QString &dir, const Executable &exec)
-{
- return QDir::fromNativeSeparators(dir) + "/" + exec.m_Title + ".lnk";
-}
-
-QString getDesktopLinkfile(const Executable &exec)
-{
- return getLinkfile(getDesktopDirectory(), exec);
-}
-
-QString getStartMenuLinkfile(const Executable &exec)
-{
- return getLinkfile(getStartMenuDirectory(), exec);
-}
-}
-
-void MainWindow::addWindowsLink(const ShortcutType mapping)
-{
- const Executable &selectedExecutable(getSelectedExecutable());
- QString const linkName = getLinkfile(mapping == ShortcutType::Desktop ? getDesktopDirectory() : getStartMenuDirectory(),
- selectedExecutable);
-
- 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());
-
- 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()));
-
- 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));
- }
- }
-}
-
-void MainWindow::linkDesktop()
-{
- addWindowsLink(ShortcutType::Desktop);
-}
-
-void MainWindow::linkMenu()
-{
- addWindowsLink(ShortcutType::StartMenu);
-}
-
-void MainWindow::on_actionSettings_triggered()
-{
- Settings &settings = m_OrganizerCore.settings();
-
- QString oldModDirectory(settings.getModDirectory());
- QString oldCacheDirectory(settings.getCacheDirectory());
- QString oldProfilesDirectory(settings.getProfileDirectory());
- QString oldManagedGameDirectory(settings.getManagedGameDirectory());
- bool oldDisplayForeign(settings.displayForeign());
- bool proxy = settings.useProxy();
- DownloadManager *dlManager = m_OrganizerCore.downloadManager();
-
- settings.query(&m_PluginContainer, this);
-
- if (oldManagedGameDirectory != settings.getManagedGameDirectory()) {
- QMessageBox::about(this, tr("Restarting MO"),
- tr("Changing the managed game directory requires restarting MO.\n"
- "Any pending downloads will be paused.\n\n"
- "Click OK to restart MO now."));
- dlManager->pauseAll();
- qApp->exit(INT_MAX);
- }
-
- InstallationManager *instManager = m_OrganizerCore.installationManager();
- instManager->setModsDirectory(settings.getModDirectory());
- instManager->setDownloadDirectory(settings.getDownloadDirectory());
-
- fixCategories();
- refreshFilters();
-
- if (settings.getProfileDirectory() != oldProfilesDirectory) {
- refreshProfiles();
- }
-
- if (dlManager->getOutputDirectory() != settings.getDownloadDirectory()) {
- if (dlManager->downloadsInProgress()) {
- MessageDialog::showMessage(tr("Can't change download directory while "
- "downloads are in progress!"),
- this);
- } else {
- dlManager->setOutputDirectory(settings.getDownloadDirectory());
- }
- }
- dlManager->setPreferredServers(settings.getPreferredServers());
-
- if ((settings.getModDirectory() != oldModDirectory)
- || (settings.displayForeign() != oldDisplayForeign)) {
- m_OrganizerCore.profileRefresh();
- }
-
- if (settings.getCacheDirectory() != oldCacheDirectory) {
- NexusInterface::instance(&m_PluginContainer)->setCacheDirectory(settings.getCacheDirectory());
- }
-
- if (proxy != settings.useProxy()) {
- activateProxy(settings.useProxy());
- }
-
- NexusInterface::instance(&m_PluginContainer)->setNMMVersion(settings.getNMMVersion());
-
- updateDownloadListDelegate();
-
- m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist());
- m_OrganizerCore.cycleDiagnostics();
-}
-
-
-void MainWindow::on_actionNexus_triggered()
-{
- QDesktopServices::openUrl(QUrl(NexusInterface::instance(&m_PluginContainer)->getGameURL(m_OrganizerCore.managedGame()->gameShortName())));
-}
-
-
-void MainWindow::linkClicked(const QString &url)
-{
- QDesktopServices::openUrl(QUrl(url));
-}
-
-
-void MainWindow::installTranslator(const QString &name)
-{
- QTranslator *translator = new QTranslator(this);
- QString fileName = name + "_" + m_CurrentLanguage;
- if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) {
- if ((m_CurrentLanguage != "en-US")
- && (m_CurrentLanguage != "en_US")
- && (m_CurrentLanguage != "en-GB")) {
- qDebug("localization file %s not found", qPrintable(fileName));
- } // we don't actually expect localization files for english
- }
-
- qApp->installTranslator(translator);
- m_Translators.push_back(translator);
-}
-
-
-void MainWindow::languageChange(const QString &newLanguage)
-{
- for (QTranslator *trans : m_Translators) {
- qApp->removeTranslator(trans);
- }
- m_Translators.clear();
-
- m_CurrentLanguage = newLanguage;
-
- installTranslator("qt");
- installTranslator("qtbase");
- installTranslator(ToQString(AppConfig::translationPrefix()));
- for (const QString &fileName : m_PluginContainer.pluginFileNames()) {
- installTranslator(QFileInfo(fileName).baseName());
- }
- ui->retranslateUi(this);
- qDebug("loaded language %s", qPrintable(newLanguage));
-
- ui->profileBox->setItemText(0, QObject::tr("<Manage...>"));
-
- createHelpWidget();
-
- updateDownloadListDelegate();
- updateProblemsButton();
-
- ui->listOptionsBtn->setMenu(modListContextMenu());
-
- ui->openFolderMenu->setMenu(openFolderMenu());
-
-}
-
-void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry)
-{
- for (FileEntry::Ptr current : directoryEntry.getFiles()) {
- bool isArchive = false;
- int origin = current->getOrigin(isArchive);
- if (isArchive) {
- // TODO: don't list files from archives. maybe make this an option?
- continue;
- }
- QString fullName = directory + "\\" + ToQString(current->getName());
- file.write(fullName.toUtf8());
-
- file.write("\t(");
- file.write(ToQString(m_OrganizerCore.directoryStructure()->getOriginByID(origin).getName()).toUtf8());
- file.write(")\r\n");
- }
-
- // recurse into subdirectories
- std::vector<DirectoryEntry*>::const_iterator current, end;
- directoryEntry.getSubDirectories(current, end);
- for (; current != end; ++current) {
- writeDataToFile(file, directory + "\\" + ToQString((*current)->getName()), **current);
- }
-}
-
-void MainWindow::writeDataToFile()
-{
- QString fileName = QFileDialog::getSaveFileName(this);
- QFile file(fileName);
- if (!file.open(QIODevice::WriteOnly)) {
- reportError(tr("failed to write to file %1").arg(fileName));
- }
-
- writeDataToFile(file, "data", *m_OrganizerCore.directoryStructure());
- file.close();
-
- MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this);
-}
-
-
-int MainWindow::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 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: {
- 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);
- refreshExecutablesList();
- }
- } break;
- case 2: {
- QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable."));
- } break;
- default: {
- // nop
- } break;
- }
- }
-}
-
-
-void MainWindow::originModified(int originID)
-{
- FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
- origin.enable(false);
- m_OrganizerCore.directoryStructure()->addFromOrigin(origin.getName(), origin.getPath(), origin.getPriority());
- DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure());
-}
-
-
-void MainWindow::hideFile()
-{
- QString oldName = m_ContextItem->data(0, Qt::UserRole).toString();
- QString newName = oldName + ModInfo::s_HiddenExt;
-
- 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;
- }
- } else {
- return;
- }
- }
-
- if (QFile::rename(oldName, newName)) {
- originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt());
- refreshDataTreeKeepExpandedNodes();
- } else {
- reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName)));
- }
-}
-
-
-void MainWindow::unhideFile()
-{
- QString oldName = m_ContextItem->data(0, Qt::UserRole).toString();
- 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;
- }
- } else {
- return;
- }
- }
- if (QFile::rename(oldName, newName)) {
- originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt());
- refreshDataTreeKeepExpandedNodes();
- } else {
- reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName)));
- }
-}
-
-
-void MainWindow::enableSelectedPlugins_clicked()
-{
- m_OrganizerCore.pluginList()->enableSelected(ui->espList->selectionModel());
-}
-
-
-void MainWindow::disableSelectedPlugins_clicked()
-{
- m_OrganizerCore.pluginList()->disableSelected(ui->espList->selectionModel());
-}
-
-void MainWindow::sendSelectedPluginsToTop_clicked()
-{
- m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), 0);
-}
-
-void MainWindow::sendSelectedPluginsToBottom_clicked()
-{
- m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), INT_MAX);
-}
-
-void MainWindow::sendSelectedPluginsToPriority_clicked()
-{
- bool ok;
- int newPriority = QInputDialog::getInt(this,
- tr("Set Priority"), tr("Set the priority of the selected plugins"),
- 0, 0, INT_MAX, 1, &ok);
- if (!ok) return;
-
- m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), newPriority);
-}
-
-
-void MainWindow::enableSelectedMods_clicked()
-{
- m_OrganizerCore.modList()->enableSelected(ui->modList->selectionModel());
-}
-
-
-void MainWindow::disableSelectedMods_clicked()
-{
- m_OrganizerCore.modList()->disableSelected(ui->modList->selectionModel());
-}
-
-
-void MainWindow::previewDataFile()
-{
- QString fileName = QDir::fromNativeSeparators(m_ContextItem->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
-
- // 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);
- }
-
-
-
- const FileEntry::Ptr file = m_OrganizerCore.directoryStructure()->searchFile(ToWString(fileName), nullptr);
-
- if (file.get() == nullptr) {
- reportError(tr("file not found: %1").arg(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) {
- 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."));
- }
-}
-
-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;
- }
- }
-}
-
-
-void MainWindow::updateAvailable()
-{
- for (QAction *action : ui->toolBar->actions()) {
- if (action->text() == tr("Update")) {
- action->setEnabled(true);
- action->setToolTip(tr("Update available"));
- break;
- }
- }
-}
-
-
-void MainWindow::motdReceived(const QString &motd)
-{
- // don't show motd after 5 seconds, may be annoying. Hopefully the user's
- // internet connection is faster next time
- if (m_StartTime.secsTo(QTime::currentTime()) < 5) {
- uint hash = qHash(motd);
- if (hash != m_OrganizerCore.settings().getMotDHash()) {
- MotDDialog dialog(motd);
- dialog.exec();
- m_OrganizerCore.settings().setMotDHash(hash);
- }
- }
-
- ui->actionEndorseMO->setVisible(false);
-}
-
-
-void MainWindow::notEndorsedYet()
-{
- ui->actionEndorseMO->setVisible(true);
-}
-
-
-void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
-{
- QTreeWidget *dataTree = findChild<QTreeWidget*>("dataTree");
- m_ContextItem = dataTree->itemAt(pos.x(), pos.y());
-
- QMenu menu;
- if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0)
- && (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) {
- menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile()));
- menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable()));
-
- QString fileName = m_ContextItem->text(0);
- if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) {
- menu.addAction(tr("Preview"), this, SLOT(previewDataFile()));
- }
-
- // offer to hide/unhide file, but not for files from archives
- if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) {
- if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) {
- menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile()));
- } else {
- menu.addAction(tr("Hide"), this, SLOT(hideFile()));
- }
- }
-
- menu.addSeparator();
- }
- menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile()));
- menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked()));
-
- menu.exec(dataTree->mapToGlobal(pos));
-}
-
-void MainWindow::on_conflictsCheckBox_toggled(bool)
-{
- refreshDataTreeKeepExpandedNodes();
-}
-
-
-void MainWindow::on_actionUpdate_triggered()
-{
- m_OrganizerCore.startMOUpdate();
-}
-
-
-void MainWindow::on_actionEndorseMO_triggered()
-{
- // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now
- IPluginGame * game = m_OrganizerCore.getGame("skyrimse");
- if (!game) return;
-
- if (QMessageBox::question(this, tr("Endorse Mod Organizer"),
- tr("Do you want to endorse Mod Organizer on %1 now?").arg(
- NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement(
- game->gameShortName(), game->nexusModOrganizerID(), true, this, QVariant(), QString());
- }
-}
-
-
-void MainWindow::updateDownloadListDelegate()
-{
- if (m_OrganizerCore.settings().compactDownloads()) {
- ui->downloadView->setItemDelegate(
- new DownloadListWidgetCompactDelegate(m_OrganizerCore.downloadManager(),
- m_OrganizerCore.settings().metaDownloads(),
- ui->downloadView,
- ui->downloadView));
- } else {
- ui->downloadView->setItemDelegate(
- new DownloadListWidgetDelegate(m_OrganizerCore.downloadManager(),
- m_OrganizerCore.settings().metaDownloads(),
- ui->downloadView,
- ui->downloadView));
- }
-
- DownloadListSortProxy *sortProxy = new DownloadListSortProxy(m_OrganizerCore.downloadManager(), ui->downloadView);
- sortProxy->setSourceModel(new DownloadList(m_OrganizerCore.downloadManager(), ui->downloadView));
- connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString)));
- connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(downloadFilterChanged(QString)));
-
- ui->downloadView->setModel(sortProxy);
- //ui->downloadView->sortByColumn(1, Qt::DescendingOrder);
- ui->downloadView->header()->resizeSections(QHeaderView::Stretch);
-
- connect(ui->downloadView->itemDelegate(), SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(openFile(int)), m_OrganizerCore.downloadManager(), SLOT(openFile(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(removeDownload(int, bool)), m_OrganizerCore.downloadManager(), SLOT(removeDownload(int, bool)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(restoreDownload(int)), m_OrganizerCore.downloadManager(), SLOT(restoreDownload(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(cancelDownload(int)), m_OrganizerCore.downloadManager(), SLOT(cancelDownload(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(pauseDownload(int)), m_OrganizerCore.downloadManager(), SLOT(pauseDownload(int)));
- connect(ui->downloadView->itemDelegate(), SIGNAL(resumeDownload(int)), this, SLOT(resumeDownload(int)));
-}
-
-
-void MainWindow::modDetailsUpdated(bool)
-{
- if (--m_ModsToUpdate == 0) {
- statusBar()->hide();
- m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE));
- for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) {
- if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) {
- ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i));
- break;
- }
- }
- m_RefreshProgress->setVisible(false);
- } else {
- m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate);
- }
-}
-
-void MainWindow::nxmUpdatesAvailable(const std::vector<int> &modIDs, QVariant userData, QVariant resultData, int)
-{
- m_ModsToUpdate -= static_cast<int>(modIDs.size());
- QVariantList resultList = resultData.toList();
- for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) {
- QVariantMap result = iter->toMap();
- // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now
- IPluginGame * game = m_OrganizerCore.getGame("skyrimse");
- if (game
- && result["id"].toInt() == game->nexusModOrganizerID()
- && result["game_id"].toInt() == game->nexusGameID()) {
- if (!result["voted_by_user"].toBool()) {
- ui->actionEndorseMO->setVisible(true);
- }
- } else {
- QString gameName = m_OrganizerCore.managedGame()->gameShortName();
- bool sameNexus = false;
- for (IPluginGame *game : m_PluginContainer.plugins<IPluginGame>()) {
- if (game->nexusGameID() == result["game_id"].toInt()) {
- gameName = game->gameShortName();
- if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID())
- sameNexus = true;
- break;
- }
- }
- std::vector<ModInfo::Ptr> info = ModInfo::getByModID(gameName, result["id"].toInt());
- if (sameNexus) {
- std::vector<ModInfo::Ptr> mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), result["id"].toInt());
- info.reserve(info.size() + mainInfo.size());
- info.insert(info.end(), mainInfo.begin(), mainInfo.end());
- }
- for (auto iter = info.begin(); iter != info.end(); ++iter) {
- (*iter)->setNewestVersion(result["version"].toString());
- (*iter)->setNexusDescription(result["description"].toString());
- if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn() &&
- result.contains("voted_by_user")) {
- // don't use endorsement info if we're not logged in or if the response doesn't contain it
- (*iter)->setIsEndorsed(result["voted_by_user"].toBool());
- }
- }
- }
- }
-
- if (m_ModsToUpdate <= 0) {
- statusBar()->hide();
- m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE));
- for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) {
- if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) {
- ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i));
- break;
- }
- }
- } else {
- m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate);
- }
-}
-
-void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int)
-{
- if (resultData.toBool()) {
- ui->actionEndorseMO->setVisible(false);
- QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!"));
- }
-
- if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
- this, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)))) {
- qCritical("failed to disconnect endorsement slot");
- }
-}
-
-void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultData, int)
-{
- QVariantList serverList = resultData.toList();
-
- QList<ServerInfo> servers;
- for (const QVariant &server : serverList) {
- QVariantMap serverInfo = server.toMap();
- ServerInfo info;
- info.name = serverInfo["Name"].toString();
- info.premium = serverInfo["IsPremium"].toBool();
- info.lastSeen = QDate::currentDate();
- info.preferred = !info.name.compare("CDN", Qt::CaseInsensitive);
- // other keys: ConnectedUsers, Country, URI
- servers.append(info);
- }
- m_OrganizerCore.settings().updateServers(servers);
-}
-
-
-void MainWindow::nxmRequestFailed(QString, int modID, int, QVariant, int, const QString &errorString)
-{
- if (modID == -1) {
- // must be the update-check that failed
- m_ModsToUpdate = 0;
- statusBar()->hide();
- }
- MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this);
-}
-
-
-BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination,
- QProgressDialog &progress)
-{
- QDir().mkdir(destination);
- BSA::EErrorCode result = BSA::ERROR_NONE;
- QString errorFile;
-
- for (unsigned int i = 0; i < folder->getNumFiles(); ++i) {
- BSA::File::Ptr file = folder->getFile(i);
- BSA::EErrorCode res = archive.extract(file, destination.toUtf8().constData());
- if (res != BSA::ERROR_NONE) {
- reportError(tr("failed to read %1: %2").arg(file->getName().c_str()).arg(res));
- result = res;
- }
- progress.setLabelText(file->getName().c_str());
- progress.setValue(progress.value() + 1);
- QCoreApplication::processEvents();
- if (progress.wasCanceled()) {
- result = BSA::ERROR_CANCELED;
- }
- }
-
- if (result != BSA::ERROR_NONE) {
- if (QMessageBox::critical(this, tr("Error"), tr("failed to extract %1 (errorcode %2)").arg(errorFile).arg(result),
- QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) {
- return result;
- }
- }
-
- for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) {
- BSA::Folder::Ptr subFolder = folder->getSubFolder(i);
- BSA::EErrorCode res = extractBSA(archive, subFolder,
- destination.mid(0).append("/").append(subFolder->getName().c_str()), progress);
- if (res != BSA::ERROR_NONE) {
- return res;
- }
- }
- return BSA::ERROR_NONE;
-}
-
-
-bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std::string fileName)
-{
- progress.setLabelText(fileName.c_str());
- progress.setValue(percentage);
- QCoreApplication::processEvents();
- return !progress.wasCanceled();
-}
-
-
-void MainWindow::extractBSATriggered()
-{
- QTreeWidgetItem *item = m_ContextItem;
- QString origin;
-
- QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA"));
- QStringList archives = {};
- if (!targetFolder.isEmpty()) {
- if (!item->parent()) {
- for (int i = 0; i < item->childCount(); ++i) {
- archives.append(item->child(i)->text(0));
- }
- origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(0))).getPath()));
- } else {
- origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(1))).getPath()));
- archives = QStringList({ item->text(0) });
- }
-
- for (auto archiveName : archives) {
- BSA::Archive archive;
- QString archivePath = QString("%1\\%2").arg(origin).arg(archiveName);
- BSA::EErrorCode result = archive.read(archivePath.toLocal8Bit().constData(), true);
- if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) {
- reportError(tr("failed to read %1: %2").arg(archivePath).arg(result));
- return;
- }
-
- QProgressDialog progress(this);
- progress.setMaximum(100);
- progress.setValue(0);
- progress.show();
- archive.extractAll(QDir::toNativeSeparators(targetFolder).toLocal8Bit().constData(),
- boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2));
- if (result == BSA::ERROR_INVALIDHASHES) {
- reportError(tr("This archive contains invalid hashes. Some files may be broken."));
- }
- archive.close();
- }
- }
-}
-
-
-void MainWindow::displayColumnSelection(const QPoint &pos)
-{
- QMenu menu;
-
- // display a list of all headers as checkboxes
- QAbstractItemModel *model = ui->modList->header()->model();
- for (int i = 1; i < model->columnCount(); ++i) {
- QString columnName = model->headerData(i, Qt::Horizontal).toString();
- QCheckBox *checkBox = new QCheckBox(&menu);
- checkBox->setText(columnName);
- checkBox->setChecked(!ui->modList->header()->isSectionHidden(i));
- QWidgetAction *checkableAction = new QWidgetAction(&menu);
- checkableAction->setDefaultWidget(checkBox);
- menu.addAction(checkableAction);
- }
- menu.exec(pos);
-
- // view/hide columns depending on check-state
- int i = 1;
- for (const QAction *action : menu.actions()) {
- const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction*>(action);
- if (widgetAction != nullptr) {
- const QCheckBox *checkBox = qobject_cast<const QCheckBox*>(widgetAction->defaultWidget());
- if (checkBox != nullptr) {
- ui->modList->header()->setSectionHidden(i, !checkBox->isChecked());
- }
- }
- ++i;
- }
-}
-
-void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos)
-{
- m_ContextItem = ui->bsaList->itemAt(pos);
-
-// m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos));
-
- QMenu menu;
- menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered()));
-
- menu.exec(ui->bsaList->mapToGlobal(pos));
-}
-
-void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int)
-{
- m_ArchiveListWriter.write();
- m_CheckBSATimer.start(500);
-}
-
-void MainWindow::on_actionProblems_triggered()
-{
- ProblemsDialog problems(m_PluginContainer.plugins<IPluginDiagnose>(), this);
- if (problems.hasProblems()) {
- problems.exec();
- updateProblemsButton();
- }
-}
-
-void MainWindow::on_actionChange_Game_triggered()
-{
- if (QMessageBox::question(this, tr("Are you sure?"),
- tr("This will restart MO, continue?"),
- QMessageBox::Yes | QMessageBox::Cancel)
- == QMessageBox::Yes) {
- InstanceManager::instance().clearCurrentInstance();
- qApp->exit(INT_MAX);
- }
-}
-
-void MainWindow::setCategoryListVisible(bool visible)
-{
- if (visible) {
- ui->categoriesGroup->show();
- ui->displayCategoriesBtn->setText(ToQString(L"\u00ab"));
- } else {
- ui->categoriesGroup->hide();
- ui->displayCategoriesBtn->setText(ToQString(L"\u00bb"));
- }
-}
-
-void MainWindow::on_displayCategoriesBtn_toggled(bool checked)
-{
- setCategoryListVisible(checked);
-}
-
-void MainWindow::editCategories()
-{
- CategoriesDialog dialog(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) {
- dialog.commitChanges();
- }
- settings.setValue(key, dialog.saveGeometry());
-
-}
-
-void MainWindow::deselectFilters()
-{
- ui->categoriesList->clearSelection();
-}
-
-void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos)
-{
- QMenu menu;
- menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories()));
- menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters()));
-
- menu.exec(ui->categoriesList->mapToGlobal(pos));
-}
-
-
-void MainWindow::updateESPLock(bool locked)
-{
- QItemSelection currentSelection = ui->espList->selectionModel()->selection();
- if (currentSelection.count() == 0) {
- // this path is probably useless
- m_OrganizerCore.pluginList()->lockESPIndex(m_ContextRow, locked);
- } else {
- Q_FOREACH (const QModelIndex &idx, currentSelection.indexes()) {
- m_OrganizerCore.pluginList()->lockESPIndex(mapToModel(m_OrganizerCore.pluginList(), idx).row(), locked);
- }
- }
-}
-
-
-void MainWindow::lockESPIndex()
-{
- updateESPLock(true);
-}
-
-void MainWindow::unlockESPIndex()
-{
- updateESPLock(false);
-}
-
-
-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");
- }
-
- updateToolBar();
-}
-
-
-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.exec(ui->toolBar->mapToGlobal(point));
- }
- }
-}
-
-void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos)
-{
- m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row();
-
- QMenu menu;
- menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedPlugins_clicked()));
- menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedPlugins_clicked()));
-
- menu.addSeparator();
-
- menu.addAction(tr("Enable all"), m_OrganizerCore.pluginList(), SLOT(enableAll()));
- menu.addAction(tr("Disable all"), m_OrganizerCore.pluginList(), SLOT(disableAll()));
-
- menu.addSeparator();
-
- addPluginSendToContextMenu(&menu);
-
- QItemSelection currentSelection = ui->espList->selectionModel()->selection();
- bool hasLocked = false;
- bool hasUnlocked = false;
- for (const QModelIndex &idx : currentSelection.indexes()) {
- int row = m_PluginListSortProxy->mapToSource(idx).row();
- if (m_OrganizerCore.pluginList()->isEnabled(row)) {
- if (m_OrganizerCore.pluginList()->isESPLocked(row)) {
- hasLocked = true;
- } else {
- hasUnlocked = true;
- }
- }
- }
-
- if (hasLocked) {
- menu.addAction(tr("Unlock load order"), this, SLOT(unlockESPIndex()));
- }
- if (hasUnlocked) {
- menu.addAction(tr("Lock load order"), this, SLOT(lockESPIndex()));
- }
-
- menu.addSeparator();
- menu.addAction(tr("Open Origin in Explorer"), this, SLOT(openOriginExplorer_clicked()));
-
- QModelIndex idx = ui->espList->selectionModel()->currentIndex();
- ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(idx.data().toString())));
- std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
-
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) {
- QAction *infoAction = menu.addAction(tr("Open Origin Info..."), this, SLOT(openOriginInformation_clicked()));
- menu.setDefaultAction(infoAction);
- }
-
- try {
- menu.exec(ui->espList->mapToGlobal(pos));
- } catch (const std::exception &e) {
- reportError(tr("Exception: ").arg(e.what()));
- } catch (...) {
- reportError(tr("Unknown exception"));
- }
-}
-
-void MainWindow::on_groupCombo_currentIndexChanged(int index)
-{
- if (m_ModListSortProxy == nullptr) {
- return;
- }
- QAbstractProxyModel *newModel = nullptr;
- switch (index) {
- case 1: {
- newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_CATEGORY, Qt::UserRole,
- 0, Qt::UserRole + 2);
- } break;
- case 2: {
- newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_MODID, Qt::DisplayRole,
- QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE,
- Qt::UserRole + 2);
- } break;
- default: {
- newModel = nullptr;
- } break;
- }
-
- if (newModel != nullptr) {
-#ifdef TEST_MODELS
- new ModelTest(newModel, this);
-#endif // TEST_MODELS
- m_ModListSortProxy->setSourceModel(newModel);
- connect(ui->modList, SIGNAL(expanded(QModelIndex)),newModel, SLOT(expanded(QModelIndex)));
- connect(ui->modList, SIGNAL(collapsed(QModelIndex)), newModel, SLOT(collapsed(QModelIndex)));
- connect(newModel, SIGNAL(expandItem(QModelIndex)), this, SLOT(expandModList(QModelIndex)));
- } else {
- m_ModListSortProxy->setSourceModel(m_OrganizerCore.modList());
- }
- modFilterActive(m_ModListSortProxy->isFilterActive());
-}
-
-const Executable &MainWindow::getSelectedExecutable() const
-{
- QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex());
- return m_OrganizerCore.executablesList()->find(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);
-}
-
-void MainWindow::on_showHiddenBox_toggled(bool checked)
-{
- m_OrganizerCore.downloadManager()->setShowHidden(checked);
-}
-
-
-void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite)
-{
- SECURITY_ATTRIBUTES secAttributes;
- secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
- secAttributes.bInheritHandle = TRUE;
- secAttributes.lpSecurityDescriptor = nullptr;
-
- if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) {
- qCritical("failed to create stdout reroute");
- }
-
- if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) {
- qCritical("failed to correctly set up the stdout reroute");
- *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE;
- }
-}
-
-std::string MainWindow::readFromPipe(HANDLE stdOutRead)
-{
- static const int chunkSize = 128;
- std::string result;
-
- char buffer[chunkSize + 1];
- buffer[chunkSize] = '\0';
-
- DWORD read = 1;
- while (read > 0) {
- if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) {
- break;
- }
- if (read > 0) {
- result.append(buffer, read);
- if (read < chunkSize) {
- break;
- }
- }
- }
- return result;
-}
-
-void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog)
-{
- std::vector<std::string> lines;
- boost::split(lines, lootOut, boost::is_any_of("\r\n"));
-
- std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\.");
- std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\.");
-
- for (const std::string &line : lines) {
- if (line.length() > 0) {
- size_t progidx = line.find("[progress]");
- size_t erroridx = line.find("[error]");
- if (progidx != std::string::npos) {
- dialog.setLabelText(line.substr(progidx + 11).c_str());
- } else if (erroridx != std::string::npos) {
- qWarning("%s", line.c_str());
- errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n");
- } else {
- std::smatch match;
- if (std::regex_match(line, match, exRequires)) {
- std::string modName(match[1].first, match[1].second);
- std::string dependency(match[2].first, match[2].second);
- m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str()));
- } else if (std::regex_match(line, match, exIncompatible)) {
- std::string modName(match[1].first, match[1].second);
- std::string dependency(match[2].first, match[2].second);
- m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str()));
- } else {
- qDebug("[loot] %s", line.c_str());
- }
- }
- }
- }
-}
-
-void MainWindow::on_bossButton_clicked()
-{
- std::string reportURL;
- std::string errorMessages;
-
- //m_OrganizerCore.currentProfile()->writeModlistNow();
- m_OrganizerCore.savePluginList();
- //Create a backup of the load orders w/ LOOT in name
- //to make sure that any sorting is easily undo-able.
- //Need to figure out how I want to do that.
-
- bool success = false;
-
- try {
- setEnabled(false);
- ON_BLOCK_EXIT([&] () { setEnabled(true); });
- QProgressDialog dialog(this);
- dialog.setLabelText(tr("Please wait while LOOT is running"));
- dialog.setMaximum(0);
- dialog.show();
-
- QString outPath = QDir::temp().absoluteFilePath("lootreport.json");
-
- QStringList parameters;
- parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName()
- << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath())
- << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath())
- << "--out" << QString("\"%1\"").arg(outPath);
-
- if (m_DidUpdateMasterList) {
- parameters << "--skipUpdateMasterlist";
- }
- HANDLE stdOutWrite = INVALID_HANDLE_VALUE;
- HANDLE stdOutRead = INVALID_HANDLE_VALUE;
- createStdoutPipe(&stdOutRead, &stdOutWrite);
- try {
- m_OrganizerCore.prepareVFS();
- } catch (const UsvfsConnectorException &e) {
- qDebug(e.what());
- return;
- } catch (const std::exception &e) {
- QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what());
- return;
- }
-
- HANDLE loot = startBinary(QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"),
- parameters.join(" "),
- qApp->applicationDirPath() + "/loot",
- true,
- stdOutWrite);
-
- // we don't use the write end
- ::CloseHandle(stdOutWrite);
-
- m_OrganizerCore.pluginList()->clearAdditionalInformation();
-
- DWORD retLen;
- JOBOBJECT_BASIC_PROCESS_ID_LIST info;
- HANDLE processHandle = loot;
-
- if (loot != INVALID_HANDLE_VALUE) {
- bool isJobHandle = true;
- ULONG lastProcessID;
- DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE);
- while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) {
- if (isJobHandle) {
- if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) {
- if (info.NumberOfProcessIdsInList == 0) {
- qDebug("no more processes in job");
- break;
- } else {
- if (lastProcessID != info.ProcessIdList[0]) {
- lastProcessID = info.ProcessIdList[0];
- if (processHandle != loot) {
- ::CloseHandle(processHandle);
- }
- processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID);
- }
- }
- } else {
- // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there
- // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running.
- // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without
- // the right to break out.
- if (::GetLastError() != ERROR_MORE_DATA) {
- isJobHandle = false;
- }
- }
- }
-
- if (dialog.wasCanceled()) {
- if (isJobHandle) {
- ::TerminateJobObject(loot, 1);
- } else {
- ::TerminateProcess(loot, 1);
- }
- }
-
- // keep processing events so the app doesn't appear dead
- QCoreApplication::processEvents();
- std::string lootOut = readFromPipe(stdOutRead);
- processLOOTOut(lootOut, errorMessages, dialog);
-
- res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE);
- }
-
- std::string remainder = readFromPipe(stdOutRead).c_str();
- if (remainder.length() > 0) {
- processLOOTOut(remainder, errorMessages, dialog);
- }
- DWORD exitCode = 0UL;
- ::GetExitCodeProcess(processHandle, &exitCode);
- ::CloseHandle(processHandle);
- if (exitCode != 0UL) {
- reportError(tr("loot failed. Exit code was: %1").arg(exitCode));
- return;
- } else {
- success = true;
- QFile outFile(outPath);
- outFile.open(QIODevice::ReadOnly);
- QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll());
- QJsonArray array = doc.array();
- for (auto iter = array.begin(); iter != array.end(); ++iter) {
- QJsonObject pluginObj = (*iter).toObject();
- QJsonArray pluginMessages = pluginObj["messages"].toArray();
- for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) {
- QJsonObject msg = (*msgIter).toObject();
- m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(),
- QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString()));
- }
- if (pluginObj["dirty"].toString() == "yes")
- m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty");
- }
-
- }
- } else {
- reportError(tr("failed to start loot"));
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to run loot: %1").arg(e.what()));
- }
-
- if (errorMessages.length() > 0) {
- QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occured"), errorMessages.c_str(), QMessageBox::Ok, this);
- warn->setModal(false);
- warn->show();
- }
-
- if (success) {
- m_DidUpdateMasterList = true;
- if (reportURL.length() > 0) {
- m_IntegratedBrowser.setWindowTitle("LOOT Report");
- QString report(reportURL.c_str());
- QStringList temp = report.split("?");
- QUrl url = QUrl::fromLocalFile(temp.at(0));
- if (temp.size() > 1) {
- url.setQuery(temp.at(1).toUtf8());
- }
- m_IntegratedBrowser.openUrl(url);
- }
- m_OrganizerCore.refreshESPList(false);
- m_OrganizerCore.savePluginList();
- }
-}
-
-
-const char *MainWindow::PATTERN_BACKUP_GLOB = ".????_??_??_??_??_??";
-const char *MainWindow::PATTERN_BACKUP_REGEX = "\\.(\\d\\d\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d)";
-const char *MainWindow::PATTERN_BACKUP_DATE = "yyyy_MM_dd_hh_mm_ss";
-
-
-bool MainWindow::createBackup(const QString &filePath, const QDateTime &time)
-{
- QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE);
- if (shellCopy(QStringList(filePath), QStringList(outPath), this)) {
- QFileInfo fileInfo(filePath);
- removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 10, QDir::Name);
- return true;
- } else {
- return false;
- }
-}
-
-void MainWindow::on_saveButton_clicked()
-{
- m_OrganizerCore.savePluginList();
- QDateTime now = QDateTime::currentDateTime();
- if (createBackup(m_OrganizerCore.currentProfile()->getPluginsFileName(), now)
- && createBackup(m_OrganizerCore.currentProfile()->getLoadOrderFileName(), now)
- && createBackup(m_OrganizerCore.currentProfile()->getLockedOrderFileName(), now)) {
- MessageDialog::showMessage(tr("Backup of load order created"), this);
- }
-}
-
-QString MainWindow::queryRestore(const QString &filePath)
-{
- QFileInfo pluginFileInfo(filePath);
- QString pattern = pluginFileInfo.fileName() + ".*";
- QFileInfoList files = pluginFileInfo.absoluteDir().entryInfoList(QStringList(pattern), QDir::Files, QDir::Name);
-
- SelectionDialog dialog(tr("Choose backup to restore"), this);
- QRegExp exp(pluginFileInfo.fileName() + PATTERN_BACKUP_REGEX);
- QRegExp exp2(pluginFileInfo.fileName() + "\\.(.*)");
- for(const QFileInfo &info : boost::adaptors::reverse(files)) {
- if (exp.exactMatch(info.fileName())) {
- QDateTime time = QDateTime::fromString(exp.cap(1), PATTERN_BACKUP_DATE);
- dialog.addChoice(time.toString(), "", exp.cap(1));
- } else if (exp2.exactMatch(info.fileName())) {
- dialog.addChoice(exp2.cap(1), "", exp2.cap(1));
- }
- }
-
- if (dialog.numChoices() == 0) {
- QMessageBox::information(this, tr("No Backups"), tr("There are no backups to restore"));
- return QString();
- }
-
- if (dialog.exec() == QDialog::Accepted) {
- return dialog.getChoiceData().toString();
- } else {
- return QString();
- }
-}
-
-void MainWindow::on_restoreButton_clicked()
-{
- QString pluginName = m_OrganizerCore.currentProfile()->getPluginsFileName();
- QString choice = queryRestore(pluginName);
- if (!choice.isEmpty()) {
- QString loadOrderName = m_OrganizerCore.currentProfile()->getLoadOrderFileName();
- QString lockedName = m_OrganizerCore.currentProfile()->getLockedOrderFileName();
- if (!shellCopy(pluginName + "." + choice, pluginName, true, this) ||
- !shellCopy(loadOrderName + "." + choice, loadOrderName, true, this) ||
- !shellCopy(lockedName + "." + choice, lockedName, true, this)) {
- QMessageBox::critical(this, tr("Restore failed"),
- tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError())));
- }
- m_OrganizerCore.refreshESPList(true);
- }
-}
-
-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);
- }
-}
-
-void MainWindow::on_restoreModsButton_clicked()
-{
- QString modlistName = m_OrganizerCore.currentProfile()->getModlistFileName();
- QString choice = queryRestore(modlistName);
- if (!choice.isEmpty()) {
- if (!shellCopy(modlistName + "." + choice, modlistName, true, this)) {
- QMessageBox::critical(this, tr("Restore failed"),
- tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError())));
- }
- m_OrganizerCore.refreshModList(false);
- }
-}
-
-void MainWindow::on_actionCopy_Log_to_Clipboard_triggered()
-{
- QStringList lines;
- QAbstractItemModel *model = ui->logList->model();
- for (int i = 0; i < model->rowCount(); ++i) {
- lines.append(QString("%1 [%2] %3").arg(model->index(i, 0).data().toString())
- .arg(model->index(i, 1).data(Qt::UserRole).toString())
- .arg(model->index(i, 1).data().toString()));
- }
- QApplication::clipboard()->setText(lines.join("\n"));
-}
-
-void MainWindow::on_categoriesAndBtn_toggled(bool checked)
-{
- if (checked) {
- m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_AND);
- }
-}
-
-void MainWindow::on_categoriesOrBtn_toggled(bool checked)
-{
- if (checked) {
- m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_OR);
- }
-}
-
-void MainWindow::on_managedArchiveLabel_linkHovered(const QString&)
-{
- QToolTip::showText(QCursor::pos(),
- ui->managedArchiveLabel->toolTip());
-}
-
-void MainWindow::dragEnterEvent(QDragEnterEvent *event)
-{
- //Accept copy or move drags to the download window. Link drags are not
- //meaningful (Well, they are - we could drop a link in the download folder,
- //but you need privileges to do that).
- if (ui->downloadTab->isVisible() &&
- (event->proposedAction() == Qt::CopyAction ||
- event->proposedAction() == Qt::MoveAction) &&
- event->answerRect().intersects(ui->downloadTab->rect())) {
-
- //If I read the documentation right, this won't work under a motif windows
- //manager and the check needs to be done at the drop. However, that means
- //the user might be allowed to drop things which we can't sanely process
- QMimeData const *data = event->mimeData();
-
- if (data->hasUrls()) {
- QStringList extensions =
- m_OrganizerCore.installationManager()->getSupportedExtensions();
-
- //This is probably OK - scan to see if these are moderately sane archive
- //types
- QList<QUrl> urls = data->urls();
- bool ok = true;
- for (const QUrl &url : urls) {
- if (url.isLocalFile()) {
- QString local = url.toLocalFile();
- bool fok = false;
- for (auto ext : extensions) {
- if (local.endsWith(ext, Qt::CaseInsensitive)) {
- fok = true;
- break;
- }
- }
- if (! fok) {
- ok = false;
- break;
- }
- }
- }
- if (ok) {
- event->accept();
- }
- }
- }
-}
-
-void MainWindow::dropLocalFile(const QUrl &url, const QString &outputDir, bool move)
-{
- QFileInfo file(url.toLocalFile());
- if (!file.exists()) {
- qWarning("invalid source file %s", qPrintable(file.absoluteFilePath()));
- return;
- }
- QString target = outputDir + "/" + file.fileName();
- if (QFile::exists(target)) {
- QMessageBox box(QMessageBox::Question,
- file.fileName(),
- tr("A file with the same name has already been downloaded. "
- "What would you like to do?"));
- box.addButton(tr("Overwrite"), QMessageBox::ActionRole);
- box.addButton(tr("Rename new file"), QMessageBox::YesRole);
- box.addButton(tr("Ignore file"), QMessageBox::RejectRole);
-
- box.exec();
- switch (box.buttonRole(box.clickedButton())) {
- case QMessageBox::RejectRole:
- return;
- case QMessageBox::ActionRole:
- break;
- default:
- case QMessageBox::YesRole:
- target = m_OrganizerCore.downloadManager()->getDownloadFileName(file.fileName());
- break;
- }
- }
-
- bool success = false;
- if (move) {
- success = shellMove(file.absoluteFilePath(), target, true, this);
- } else {
- success = shellCopy(file.absoluteFilePath(), target, true, this);
- }
- if (!success) {
- qCritical("file operation failed: %s", qPrintable(windowsErrorString(::GetLastError())));
- }
-}
-
-bool MainWindow::registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName) {
- // register the view so it's geometry gets saved at exit
- m_PersistedGeometry.push_back(std::make_pair(name, view));
-
- // also, restore the geometry if it was saved before
- QSettings &settings = m_OrganizerCore.settings().directInterface();
-
- QString key = QString("geometry/%1").arg(name);
- QByteArray data;
-
- if ((oldSettingName != nullptr) && settings.contains(oldSettingName)) {
- data = settings.value(oldSettingName).toByteArray();
- settings.remove(oldSettingName);
- } else if (settings.contains(key)) {
- data = settings.value(key).toByteArray();
- }
-
- if (!data.isEmpty()) {
- view->restoreState(data);
- return true;
- } else {
- return false;
- }
-}
-
-void MainWindow::dropEvent(QDropEvent *event)
-{
- Qt::DropAction action = event->proposedAction();
- QString outputDir = m_OrganizerCore.downloadManager()->getOutputDirectory();
- if (action == Qt::MoveAction) {
- //Tell windows I'm taking control and will delete the source of a move.
- event->setDropAction(Qt::TargetMoveAction);
- }
- for (const QUrl &url : event->mimeData()->urls()) {
- if (url.isLocalFile()) {
- dropLocalFile(url, outputDir, action == Qt::MoveAction);
- } else {
- m_OrganizerCore.downloadManager()->startDownloadURLs(QStringList() << url.url());
- }
- }
- event->accept();
-}
-
-
-void MainWindow::on_clickBlankButton_clicked()
-{
- deselectFilters();
-}
-
-void MainWindow::on_clearFiltersButton_clicked()
-{
- deselectFilters();
-}
-
-void MainWindow::sendSelectedModsToPriority(int newPriority)
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- std::vector<int> modsToMove;
- for (auto idx : selection->selectedRows(ModList::COL_PRIORITY)) {
- modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt()));
- }
- m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority);
- } else {
- m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority);
- }
-}
-
-void MainWindow::sendSelectedModsToTop_clicked()
-{
- sendSelectedModsToPriority(0);
-}
-
-void MainWindow::sendSelectedModsToBottom_clicked()
-{
- sendSelectedModsToPriority(INT_MAX);
-}
-
-void MainWindow::sendSelectedModsToPriority_clicked()
-{
- bool ok;
- int newPriority = QInputDialog::getInt(this,
- tr("Set Priority"), tr("Set the priority of the selected mods"),
- 0, 0, INT_MAX, 1, &ok);
- if (!ok) return;
-
- sendSelectedModsToPriority(newPriority);
-}
-
-void MainWindow::sendSelectedModsToSeparator_clicked()
-{
- QStringList separators;
- for (auto mod : m_OrganizerCore.modList()->allMods()) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod));
- if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) {
- separators << mod.chopped(10);
- }
- }
-
- ListDialog dialog(this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog.objectName());
-
- dialog.setWindowTitle("Select a separator...");
- dialog.setChoices(separators);
- dialog.restoreGeometry(settings.value(key).toByteArray());
- if (dialog.exec() == QDialog::Accepted) {
- QString result = dialog.getChoice();
- if (!result.isEmpty()) {
- result += "_separator";
-
- int newPriority = INT_MAX;
- bool foundSection = false;
- for (auto mod : m_OrganizerCore.modsSortedByProfilePriority()) {
- unsigned int modIndex = ModInfo::getIndex(mod);
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- if (!foundSection && result.compare(mod) == 0) {
- foundSection = true;
- } else if (foundSection && modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) {
- newPriority = m_OrganizerCore.currentProfile()->getModPriority(modIndex);
- break;
- }
- }
-
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- std::vector<int> modsToMove;
- for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) {
- modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt()));
- }
- m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority);
- } else {
- int oldPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow);
- if (oldPriority < newPriority)
- --newPriority;
- m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority);
- }
- }
- }
- settings.setValue(key, dialog.saveGeometry());
-}
+/*
+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 "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include "directoryentry.h"
+#include "directoryrefresher.h"
+#include "executableinfo.h"
+#include "executableslist.h"
+#include "guessedvalue.h"
+#include "imodinterface.h"
+#include "iplugingame.h"
+#include "iplugindiagnose.h"
+#include "isavegame.h"
+#include "isavegameinfowidget.h"
+#include "nexusinterface.h"
+#include "organizercore.h"
+#include "pluginlistsortproxy.h"
+#include "previewgenerator.h"
+#include "serverinfo.h"
+#include "savegameinfo.h"
+#include "spawn.h"
+#include "versioninfo.h"
+#include "instancemanager.h"
+
+#include "report.h"
+#include "modlist.h"
+#include "modlistsortproxy.h"
+#include "qtgroupingproxy.h"
+#include "profile.h"
+#include "pluginlist.h"
+#include "profilesdialog.h"
+#include "editexecutablesdialog.h"
+#include "categories.h"
+#include "categoriesdialog.h"
+#include "modinfodialog.h"
+#include "overwriteinfodialog.h"
+#include "activatemodsdialog.h"
+#include "downloadlist.h"
+#include "downloadlistwidget.h"
+#include "messagedialog.h"
+#include "installationmanager.h"
+#include "lockeddialog.h"
+#include "waitingonclosedialog.h"
+#include "logbuffer.h"
+#include "downloadlistsortproxy.h"
+#include "motddialog.h"
+#include "filedialogmemory.h"
+#include "tutorialmanager.h"
+#include "modflagicondelegate.h"
+#include "genericicondelegate.h"
+#include "selectiondialog.h"
+#include "csvbuilder.h"
+#include "savetextasdialog.h"
+#include "problemsdialog.h"
+#include "previewdialog.h"
+#include "browserdialog.h"
+#include "aboutdialog.h"
+#include <safewritefile.h>
+#include "nxmaccessmanager.h"
+#include "appconfig.h"
+#include "eventfilter.h"
+#include <utility.h>
+#include <dataarchives.h>
+#include <bsainvalidation.h>
+#include <taskprogressmanager.h>
+#include <scopeguard.h>
+#include <usvfs.h>
+#include "localsavegames.h"
+#include "listdialog.h"
+
+#include <QAbstractItemDelegate>
+#include <QAbstractProxyModel>
+#include <QAction>
+#include <QApplication>
+#include <QButtonGroup>
+#include <QBuffer>
+#include <QCheckBox>
+#include <QClipboard>
+#include <QCloseEvent>
+#include <QCoreApplication>
+#include <QCursor>
+#include <QDebug>
+#include <QDesktopWidget>
+#include <QDesktopServices>
+#include <QDialog>
+#include <QDirIterator>
+#include <QDragEnterEvent>
+#include <QDropEvent>
+#include <QEvent>
+#include <QFileDialog>
+#include <QFIleIconProvider>
+#include <QFont>
+#include <QFuture>
+#include <QHash>
+#include <QIODevice>
+#include <QIcon>
+#include <QInputDialog>
+#include <QItemSelection>
+#include <QItemSelectionModel>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonValueRef>
+#include <QLineEdit>
+#include <QListWidgetItem>
+#include <QMenu>
+#include <QMessageBox>
+#include <QMimeData>
+#include <QModelIndex>
+#include <QNetworkProxyFactory>
+#include <QPainter>
+#include <QPixmap>
+#include <QPoint>
+#include <QProcess>
+#include <QProgressDialog>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QRect>
+#include <QRegExp>
+#include <QResizeEvent>
+#include <QSettings>
+#include <QScopedPointer>
+#include <QSizePolicy>
+#include <QSize>
+#include <QTime>
+#include <QTimer>
+#include <QToolButton>
+#include <QToolTip>
+#include <QTranslator>
+#include <QTreeWidget>
+#include <QTreeWidgetItemIterator>
+#include <QUrl>
+#include <QVariantList>
+#include <QVersionNumber>
+#include <QWhatsThis>
+#include <QWidgetAction>
+#include <QWebEngineProfile>
+#include <QShortcut>
+#include <QColorDialog>
+#include <QColor>
+
+#include <QDebug>
+#include <QtGlobal>
+
+#ifndef Q_MOC_RUN
+#include <boost/thread.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/bind.hpp>
+#include <boost/assign.hpp>
+#include <boost/range/adaptor/reversed.hpp>
+#endif
+
+#include <shlobj.h>
+
+#include <limits.h>
+#include <exception>
+#include <functional>
+#include <map>
+#include <regex>
+#include <stdexcept>
+#include <sstream>
+#include <utility>
+
+#ifdef TEST_MODELS
+#include "modeltest.h"
+#endif // TEST_MODELS
+
+#pragma warning( disable : 4428 )
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+MainWindow::MainWindow(QSettings &initSettings
+ , OrganizerCore &organizerCore
+ , PluginContainer &pluginContainer
+ , QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::MainWindow)
+ , m_WasVisible(false)
+ , m_Tutorial(this, "MainWindow")
+ , m_OldProfileIndex(-1)
+ , m_ModListGroupingProxy(nullptr)
+ , m_ModListSortProxy(nullptr)
+ , m_OldExecutableIndex(-1)
+ , m_CategoryFactory(CategoryFactory::instance())
+ , m_ContextItem(nullptr)
+ , m_ContextAction(nullptr)
+ , m_ContextRow(-1)
+ , m_CurrentSaveView(nullptr)
+ , m_OrganizerCore(organizerCore)
+ , m_PluginContainer(pluginContainer)
+ , m_DidUpdateMasterList(false)
+ , m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this))
+{
+ 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);
+
+ languageChange(m_OrganizerCore.settings().language());
+
+ m_CategoryFactory.loadCategories();
+
+ ui->logList->setModel(LogBuffer::instance());
+ ui->logList->setColumnWidth(0, 100);
+ ui->logList->setAutoScroll(true);
+ ui->logList->scrollToBottom();
+ ui->logList->addAction(ui->actionCopy_Log_to_Clipboard);
+ int splitterSize = this->size().height(); // actually total window size, but the splitter doesn't seem to return the true value
+ ui->topLevelSplitter->setSizes(QList<int>() << splitterSize - 100 << 100);
+ connect(ui->logList->model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
+ ui->logList, SLOT(scrollToBottom()));
+ 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();
+
+ 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
+ m_ModListSortProxy = m_OrganizerCore.createModListProxyModel();
+
+ ui->modList->setModel(m_ModListSortProxy);
+
+ GenericIconDelegate *contentDelegate = new GenericIconDelegate(ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150);
+ connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), contentDelegate, SLOT(columnResized(int,int,int)));
+ ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
+ ModFlagIconDelegate *flagDelegate = new ModFlagIconDelegate(ui->modList, ModList::COL_FLAGS, 120);
+ connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), flagDelegate, SLOT(columnResized(int,int,int)));
+ ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate);
+ ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate);
+ ui->modList->header()->installEventFilter(m_OrganizerCore.modList());
+ connect(ui->modList->header(), SIGNAL(sectionResized(int, int, int)), this, SLOT(modListSectionResized(int, int, int)));
+
+ bool modListAdjusted = registerWidgetState(ui->modList->objectName(), ui->modList->header(), "mod_list_state");
+
+ if (modListAdjusted) {
+ // hack: force the resize-signal to be triggered because restoreState doesn't seem to do that
+ for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
+ int sectionSize = ui->modList->header()->sectionSize(column);
+ ui->modList->header()->resizeSection(column, sectionSize + 1);
+ ui->modList->header()->resizeSection(column, sectionSize);
+ }
+ } else {
+ // hide these columns by default
+ ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_MODID, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_GAME, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
+ }
+
+ ui->modList->header()->setSectionHidden(ModList::COL_NAME, false); // prevent the name-column from being hidden
+ ui->modList->installEventFilter(m_OrganizerCore.modList());
+
+ // set up plugin list
+ m_PluginListSortProxy = m_OrganizerCore.createPluginListProxyModel();
+
+ ui->espList->setModel(m_PluginListSortProxy);
+ ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder);
+ ui->espList->setItemDelegateForColumn(PluginList::COL_FLAGS, new GenericIconDelegate(ui->espList));
+ ui->espList->installEventFilter(m_OrganizerCore.pluginList());
+
+ ui->bsaList->setLocalMoveOnly(true);
+
+ initDownloadView();
+ bool pluginListAdjusted = registerWidgetState(ui->espList->objectName(), ui->espList->header(), "plugin_list_state");
+ registerWidgetState(ui->dataTree->objectName(), ui->dataTree->header());
+ registerWidgetState(ui->downloadView->objectName(),
+ ui->downloadView->header());
+
+ ui->splitter->setStretchFactor(0, 3);
+ ui->splitter->setStretchFactor(1, 2);
+
+ 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()));
+ ui->linkButton->setMenu(linkMenu);
+
+ QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn);
+ initModListContextMenu(listOptionsMenu);
+ ui->listOptionsBtn->setMenu(listOptionsMenu);
+ connect(ui->listOptionsBtn, SIGNAL(pressed()), this, SLOT(on_listOptionsBtn_pressed()));
+
+ ui->openFolderMenu->setMenu(openFolderMenu());
+
+ ui->savegameList->installEventFilter(this);
+ ui->savegameList->setMouseTracking(true);
+
+ // don't allow mouse wheel to switch grouping, too many people accidentally
+ // turn on grouping and then don't understand what happened
+ EventFilter *noWheel
+ = new EventFilter(this, [](QObject *, QEvent *event) -> bool {
+ return event->type() == QEvent::Wheel;
+ });
+
+ ui->groupCombo->installEventFilter(noWheel);
+ ui->profileBox->installEventFilter(noWheel);
+
+ if (organizerCore.managedGame()->sortMechanism() == MOBase::IPluginGame::SortMechanism::NONE) {
+ ui->bossButton->setDisabled(true);
+ 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*)));
+
+ connect(ui->modList, SIGNAL(dropModeUpdate(bool)), m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool)));
+
+ connect(m_ModListSortProxy, SIGNAL(filterActive(bool)), this, SLOT(modFilterActive(bool)));
+ connect(m_ModListSortProxy, SIGNAL(layoutChanged()), this, SLOT(updateModCount()));
+ connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString)));
+
+ connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), m_PluginListSortProxy, SLOT(updateFilter(QString)));
+ connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString)));
+
+ connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*)));
+
+ connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed()));
+ connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int)));
+ connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString)));
+
+ connect(&m_SavesWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(refreshSavesIfOpen()));
+
+ connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString)));
+ connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString)));
+
+ connect(m_OrganizerCore.updater(), SIGNAL(restart()), this, SLOT(close()));
+ connect(m_OrganizerCore.updater(), SIGNAL(updateAvailable()), this, SLOT(updateAvailable()));
+ connect(m_OrganizerCore.updater(), SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString)));
+
+ connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString)));
+ 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(&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(&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()));
+
+ connect(ui->espList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(esplistSelectionsChanged(QItemSelection)));
+ connect(ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(modlistSelectionsChanged(QItemSelection)));
+
+ new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated()));
+ new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated()));
+
+ new QShortcut(QKeySequence::Refresh, this, SLOT(refreshProfile_activated()));
+
+ new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F), this, SLOT(search_activated()));
+ new QShortcut(QKeySequence(Qt::Key_Escape), this, SLOT(searchClear_activated()));
+
+ m_UpdateProblemsTimer.setSingleShot(true);
+ connect(&m_UpdateProblemsTimer, SIGNAL(timeout()), this, SLOT(updateProblemsButton()));
+
+ m_SaveMetaTimer.setSingleShot(false);
+ connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas()));
+ m_SaveMetaTimer.start(5000);
+
+ setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool());
+ FileDialogMemory::restore(initSettings);
+
+ fixCategories();
+
+ m_StartTime = QTime::currentTime();
+
+ m_Tutorial.expose("modList", m_OrganizerCore.modList());
+ m_Tutorial.expose("espList", m_OrganizerCore.pluginList());
+
+ m_OrganizerCore.setUserInterface(this, this);
+ for (const QString &fileName : m_PluginContainer.pluginFileNames()) {
+ installTranslator(QFileInfo(fileName).baseName());
+ }
+
+ registerPluginTools(m_PluginContainer.plugins<IPluginTool>());
+
+ for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins<IPluginModPage>()) {
+ registerModPage(modPagePlugin);
+ }
+
+ // refresh profiles so the current profile can be activated
+ refreshProfiles(false);
+
+ ui->profileBox->setCurrentText(m_OrganizerCore.currentProfile()->name());
+
+ if (m_OrganizerCore.getArchiveParsing())
+ {
+ ui->showArchiveDataCheckBox->setCheckState(Qt::Checked);
+ ui->showArchiveDataCheckBox->setEnabled(true);
+ m_showArchiveData = true;
+ }
+ else
+ {
+ ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked);
+ ui->showArchiveDataCheckBox->setEnabled(false);
+ m_showArchiveData = false;
+ }
+
+ refreshExecutablesList();
+ updateToolBar();
+
+ 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);
+ }
+
+ updatePluginCount();
+ updateModCount();
+}
+
+
+MainWindow::~MainWindow()
+{
+ try {
+ cleanup();
+
+ m_PluginContainer.setUserInterface(nullptr, nullptr);
+ m_OrganizerCore.setUserInterface(nullptr, nullptr);
+ m_IntegratedBrowser.close();
+ delete ui;
+ } catch (std::exception &e) {
+ QMessageBox::critical(nullptr, tr("Crash on exit"),
+ tr("MO crashed while exiting. Some settings may not be saved.\n\nError: %1").arg(e.what()),
+ QMessageBox::Ok);
+ }
+}
+
+
+void MainWindow::updateWindowTitle(const QString &accountName, bool premium)
+{
+ 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 ? "*" : ""));
+ }
+
+ this->setWindowTitle(title);
+}
+
+
+void MainWindow::disconnectPlugins()
+{
+ if (ui->actionTool->menu() != nullptr) {
+ ui->actionTool->menu()->clear();
+ }
+}
+
+
+void MainWindow::resizeLists(bool modListCustom, bool pluginListCustom)
+{
+ if (!modListCustom) {
+ // resize mod list to fit content
+ for (int i = 0; i < ui->modList->header()->count(); ++i) {
+ ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
+ }
+
+ // ensure the columns aren't so small you can't see them any more
+ for (int i = 0; i < ui->modList->header()->count(); ++i) {
+ if (ui->modList->header()->sectionSize(i) < 10) {
+ ui->modList->header()->resizeSection(i, 10);
+ }
+ }
+
+ if (!pluginListCustom) {
+ // resize plugin list to fit content
+ for (int i = 0; i < ui->espList->header()->count(); ++i) {
+ ui->espList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ ui->espList->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ }
+}
+
+
+void MainWindow::allowListResize()
+{
+ // allow resize on mod list
+ for (int i = 0; i < ui->modList->header()->count(); ++i) {
+ ui->modList->header()->setSectionResizeMode(i, QHeaderView::Interactive);
+ }
+ //ui->modList->header()->setSectionResizeMode(ui->modList->header()->count() - 1, QHeaderView::Stretch);
+ ui->modList->header()->setStretchLastSection(true);
+
+
+ // allow resize on plugin list
+ for (int i = 0; i < ui->espList->header()->count(); ++i) {
+ ui->espList->header()->setSectionResizeMode(i, QHeaderView::Interactive);
+ }
+ //ui->espList->header()->setSectionResizeMode(ui->espList->header()->count() - 1, QHeaderView::Stretch);
+ ui->espList->header()->setStretchLastSection(true);
+}
+
+void MainWindow::updateStyle(const QString&)
+{
+ // no effect?
+ ensurePolished();
+}
+
+void MainWindow::resizeEvent(QResizeEvent *event)
+{
+ m_Tutorial.resize(event->size());
+ QMainWindow::resizeEvent(event);
+}
+
+
+static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex idx)
+{
+ QModelIndex result = idx;
+ const QAbstractItemModel *model = idx.model();
+ while (model != targetModel) {
+ if (model == nullptr) {
+ return QModelIndex();
+ }
+ const QAbstractProxyModel *proxyModel = qobject_cast<const QAbstractProxyModel*>(model);
+ if (proxyModel == nullptr) {
+ return QModelIndex();
+ }
+ result = proxyModel->mapToSource(result);
+ model = proxyModel->sourceModel();
+ }
+ return result;
+}
+
+
+void MainWindow::actionToToolButton(QAction *&sourceAction)
+{
+ 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;
+}
+
+void MainWindow::updateToolBar()
+{
+ for (QAction *action : ui->toolBar->actions()) {
+ if (action->objectName().startsWith("custom__")) {
+ ui->toolBar->removeAction(action);
+ action->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);
+ if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) {
+ qDebug("failed to connect trigger?");
+ }
+ ui->toolBar->insertAction(m_Sep, exeAction);
+ }
+ }
+}
+
+
+void MainWindow::scheduleUpdateButton()
+{
+ if (!m_UpdateProblemsTimer.isActive()) {
+ m_UpdateProblemsTimer.start(1000);
+ }
+}
+
+
+void MainWindow::updateProblemsButton()
+{
+ size_t numProblems = checkForProblems();
+ 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);
+ {
+ QPainter painter(&mergedIcon);
+ 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));
+ } 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"));
+ }
+}
+
+
+bool MainWindow::errorReported(QString &logFile)
+{
+ QDir dir(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()));
+ QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"),
+ QDir::Files, QDir::Name | QDir::Reversed);
+
+ if (files.count() > 0) {
+ logFile = files.at(0).absoluteFilePath();
+ QFile file(logFile);
+ if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ char buffer[1024];
+ int line = 0;
+ while (!file.atEnd()) {
+ file.readLine(buffer, 1024);
+ if (strncmp(buffer, "ERROR", 5) == 0) {
+ return true;
+ }
+
+ // prevent this function from taking forever
+ if (line++ >= 50000) {
+ break;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+
+size_t MainWindow::checkForProblems()
+{
+ size_t numProblems = 0;
+ for (QObject *pluginObj : m_PluginContainer.plugins<QObject>()) {
+ IPlugin *plugin = qobject_cast<IPlugin*>(pluginObj);
+ if (plugin == nullptr || plugin->isActive()) {
+ IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(pluginObj);
+ if (diagnose != nullptr)
+ numProblems += diagnose->activeProblems().size();
+ }
+ }
+ return numProblems;
+}
+
+void MainWindow::about()
+{
+ AboutDialog dialog(m_OrganizerCore.getVersion().displayString(3), this);
+ connect(&dialog, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString)));
+ dialog.exec();
+}
+
+
+void MainWindow::createEndorseWidget()
+{
+ QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionEndorseMO));
+ QMenu *buttonMenu = toolBtn->menu();
+ if (buttonMenu == nullptr) {
+ return;
+ }
+ buttonMenu->clear();
+
+ QAction *endorseAction = new QAction(tr("Endorse"), buttonMenu);
+ connect(endorseAction, SIGNAL(triggered()), this, SLOT(actionEndorseMO()));
+ buttonMenu->addAction(endorseAction);
+
+ QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu);
+ connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(actionWontEndorseMO()));
+ buttonMenu->addAction(wontEndorseAction);
+}
+
+
+void MainWindow::createHelpWidget()
+{
+ QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionHelp));
+ QMenu *buttonMenu = toolBtn->menu();
+ if (buttonMenu == nullptr) {
+ return;
+ }
+ buttonMenu->clear();
+
+ QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu);
+ connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered()));
+ buttonMenu->addAction(helpAction);
+
+ QAction *wikiAction = new QAction(tr("Documentation"), buttonMenu);
+ connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered()));
+ buttonMenu->addAction(wikiAction);
+
+ QAction *discordAction = new QAction(tr("Chat on Discord"), buttonMenu);
+ connect(discordAction, SIGNAL(triggered()), this, SLOT(discordTriggered()));
+ buttonMenu->addAction(discordAction);
+
+ QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu);
+ connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered()));
+ buttonMenu->addAction(issueAction);
+
+ QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu);
+
+ typedef std::vector<std::pair<int, QAction*> > ActionList;
+
+ ActionList tutorials;
+
+ QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files);
+ while (dirIter.hasNext()) {
+ dirIter.next();
+ QString fileName = dirIter.fileName();
+
+ QFile file(dirIter.filePath());
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open " << fileName;
+ continue;
+ }
+ QString firstLine = QString::fromUtf8(file.readLine());
+ if (firstLine.startsWith("//TL")) {
+ QStringList params = firstLine.mid(4).trimmed().split('#');
+ if (params.size() != 2) {
+ qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters";
+ continue;
+ }
+ QAction *tutAction = new QAction(params.at(0), tutorialMenu);
+ tutAction->setData(fileName);
+ tutorials.push_back(std::make_pair(params.at(1).toInt(), tutAction));
+ }
+ }
+
+ std::sort(tutorials.begin(), tutorials.end(),
+ [] (const ActionList::value_type &LHS, const ActionList::value_type &RHS) {
+ return LHS.first < RHS.first; } );
+
+ for (auto iter = tutorials.begin(); iter != tutorials.end(); ++iter) {
+ connect(iter->second, SIGNAL(triggered()), this, SLOT(tutorialTriggered()));
+ tutorialMenu->addAction(iter->second);
+ }
+
+ buttonMenu->addMenu(tutorialMenu);
+ buttonMenu->addAction(tr("About"), this, SLOT(about()));
+ buttonMenu->addAction(tr("About Qt"), qApp, SLOT(aboutQt()));
+}
+
+void MainWindow::modFilterActive(bool filterActive)
+{
+ ui->clearFiltersButton->setVisible(filterActive);
+ if (filterActive) {
+// m_OrganizerCore.modList()->setOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>());
+ ui->modList->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
+ ui->activeModsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }");
+ } else if (ui->groupCombo->currentIndex() != 0) {
+ ui->modList->setStyleSheet("QTreeView { border: 2px ridge #337733; }");
+ ui->activeModsCounter->setStyleSheet("");
+ } else {
+ ui->modList->setStyleSheet("");
+ ui->activeModsCounter->setStyleSheet("");
+ }
+}
+
+void MainWindow::espFilterChanged(const QString &filter)
+{
+ if (!filter.isEmpty()) {
+ ui->espList->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
+ ui->activePluginsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }");
+ } else {
+ ui->espList->setStyleSheet("");
+ ui->activePluginsCounter->setStyleSheet("");
+ }
+ updatePluginCount();
+}
+
+void MainWindow::downloadFilterChanged(const QString &filter)
+{
+ if (!filter.isEmpty()) {
+ ui->downloadView->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
+ } else {
+ ui->downloadView->setStyleSheet("");
+ }
+}
+
+void MainWindow::expandModList(const QModelIndex &index)
+{
+ QAbstractItemModel *model = ui->modList->model();
+#pragma message("why is this so complicated? mapping the index doesn't work, probably a bug in QtGroupingProxy?")
+ for (int i = 0; i < model->rowCount(); ++i) {
+ QModelIndex targetIdx = model->index(i, 0);
+ if (model->data(targetIdx).toString() == index.data().toString()) {
+ ui->modList->expand(targetIdx);
+ break;
+ }
+ }
+}
+
+
+bool MainWindow::addProfile()
+{
+ QComboBox *profileBox = findChild<QComboBox*>("profileBox");
+ bool okClicked = false;
+
+ QString name = QInputDialog::getText(this, tr("Name"),
+ tr("Please enter a name for the new profile"),
+ QLineEdit::Normal, QString(), &okClicked);
+ if (okClicked && (name.size() > 0)) {
+ try {
+ profileBox->addItem(name);
+ profileBox->setCurrentIndex(profileBox->count() - 1);
+ return true;
+ } catch (const std::exception& e) {
+ reportError(tr("failed to create profile: %1").arg(e.what()));
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+void MainWindow::hookUpWindowTutorials()
+{
+ QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files);
+ while (dirIter.hasNext()) {
+ dirIter.next();
+ QString fileName = dirIter.fileName();
+ QFile file(dirIter.filePath());
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open " << fileName;
+ continue;
+ }
+ QString firstLine = QString::fromUtf8(file.readLine());
+ if (firstLine.startsWith("//WIN")) {
+ QString windowName = firstLine.mid(6).trimmed();
+ if (!m_OrganizerCore.settings().directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) {
+ TutorialManager::instance().activateTutorial(windowName, fileName);
+ }
+ }
+ }
+}
+
+void MainWindow::showEvent(QShowEvent *event)
+{
+ refreshFilters();
+
+ QMainWindow::showEvent(event);
+
+ if (!m_WasVisible) {
+ // only the first time the window becomes visible
+ m_Tutorial.registerControl();
+
+ hookUpWindowTutorials();
+
+ if (m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) {
+ QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial());
+ if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) {
+ if (QMessageBox::question(this, tr("Show tutorial?"),
+ tr("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."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial);
+ }
+ } else {
+ qCritical() << firstStepsTutorial << " missing";
+ QPoint pos = ui->toolBar->mapToGlobal(QPoint());
+ pos.rx() += ui->toolBar->width() / 2;
+ pos.ry() += ui->toolBar->height();
+ QWhatsThis::showText(pos,
+ QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements"));
+ }
+
+ m_OrganizerCore.settings().directInterface().setValue("first_start", false);
+ }
+
+ // this has no visible impact when called before the ui is visible
+ int grouping = m_OrganizerCore.settings().directInterface().value("group_state").toInt();
+ ui->groupCombo->setCurrentIndex(grouping);
+
+ allowListResize();
+
+ m_OrganizerCore.settings().registerAsNXMHandler(false);
+ m_WasVisible = true;
+ updateProblemsButton();
+ }
+}
+
+
+void MainWindow::closeEvent(QCloseEvent* event)
+{
+ 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;
+ } else {
+ m_OrganizerCore.downloadManager()->pauseAll();
+ }
+ }
+
+ std::vector<QString> hiddenList;
+ hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName());
+ HANDLE injected_process_still_running = m_OrganizerCore.findAndOpenAUSVFSProcess(hiddenList, GetCurrentProcessId());
+ if (injected_process_still_running != INVALID_HANDLE_VALUE)
+ {
+ m_OrganizerCore.waitForApplication(injected_process_still_running);
+ if (!m_closing) { // if operation cancelled
+ event->ignore();
+ return;
+ }
+ }
+
+ setCursor(Qt::WaitCursor);
+}
+
+void MainWindow::cleanup()
+{
+ if (ui->logList->model() != nullptr) {
+ disconnect(ui->logList->model(), nullptr, nullptr, nullptr);
+ ui->logList->setModel(nullptr);
+ }
+
+ QWebEngineProfile::defaultProfile()->clearAllVisitedLinks();
+ m_IntegratedBrowser.close();
+ m_SaveMetaTimer.stop();
+ m_MetaSave.waitForFinished();
+}
+
+
+void MainWindow::setBrowserGeometry(const QByteArray &geometry)
+{
+ m_IntegratedBrowser.restoreGeometry(geometry);
+}
+
+void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
+{
+ QString const &save = newItem->data(Qt::UserRole).toString();
+ if (m_CurrentSaveView == nullptr) {
+ IPluginGame const *game = m_OrganizerCore.managedGame();
+ SaveGameInfo const *info = game->feature<SaveGameInfo>();
+ if (info != nullptr) {
+ m_CurrentSaveView = info->getSaveGameWidget(this);
+ }
+ if (m_CurrentSaveView == nullptr) {
+ return;
+ }
+ }
+ m_CurrentSaveView->setSave(save);
+
+ QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView);
+
+ QPoint pos = QCursor::pos();
+ if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
+ pos.rx() -= (m_CurrentSaveView->width() + 2);
+ } else {
+ pos.rx() += 5;
+ }
+
+ if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
+ pos.ry() -= (m_CurrentSaveView->height() + 10);
+ } else {
+ pos.ry() += 20;
+ }
+ m_CurrentSaveView->move(pos);
+
+ m_CurrentSaveView->show();
+ m_CurrentSaveView->setProperty("displayItem", qVariantFromValue(static_cast<void *>(newItem)));
+
+ ui->savegameList->activateWindow();
+}
+
+
+void MainWindow::saveSelectionChanged(QListWidgetItem *newItem)
+{
+ if (newItem == nullptr) {
+ hideSaveGameInfo();
+ } else if (m_CurrentSaveView == nullptr || newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
+ displaySaveGameInfo(newItem);
+ }
+}
+
+
+void MainWindow::hideSaveGameInfo()
+{
+ if (m_CurrentSaveView != nullptr) {
+ m_CurrentSaveView->deleteLater();
+ m_CurrentSaveView = nullptr;
+ }
+}
+
+bool MainWindow::eventFilter(QObject *object, QEvent *event)
+{
+ if ((object == ui->savegameList) &&
+ ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) {
+ hideSaveGameInfo();
+ }
+ return false;
+}
+
+
+void MainWindow::toolPluginInvoke()
+{
+ QAction *triggeredAction = qobject_cast<QAction*>(sender());
+ IPluginTool *plugin = qobject_cast<IPluginTool*>(triggeredAction->data().value<QObject*>());
+ if (plugin != nullptr) {
+ try {
+ plugin->display();
+ } catch (const std::exception &e) {
+ reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what()));
+ } catch (...) {
+ reportError(tr("Plugin \"%1\" failed").arg(plugin->name()));
+ }
+ }
+}
+
+void MainWindow::modPagePluginInvoke()
+{
+ QAction *triggeredAction = qobject_cast<QAction*>(sender());
+ IPluginModPage *plugin = qobject_cast<IPluginModPage*>(triggeredAction->data().value<QObject*>());
+ if (plugin != nullptr) {
+ if (plugin->useIntegratedBrowser()) {
+ m_IntegratedBrowser.setWindowTitle(plugin->displayName());
+ m_IntegratedBrowser.openUrl(plugin->pageURL());
+ } else {
+ QDesktopServices::openUrl(QUrl(plugin->pageURL()));
+ }
+ }
+}
+
+void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu)
+{
+ if (name.isEmpty())
+ name = tool->displayName();
+
+ QAction *action = new QAction(tool->icon(), name, ui->toolBar);
+ 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);
+ }
+}
+
+void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins)
+{
+ // Sort the plugins by display name
+ std::sort(toolPlugins.begin(), toolPlugins.end(),
+ [](IPluginTool *left, IPluginTool *right) {
+ return left->displayName().toLower() < right->displayName().toLower();
+ }
+ );
+
+ // Remove inactive plugins
+ toolPlugins.erase(
+ std::remove_if(toolPlugins.begin(), toolPlugins.end(), [](IPluginTool *plugin) -> bool { return !plugin->isActive(); }),
+ toolPlugins.end()
+ );
+
+ // Group the plugins into submenus
+ QMap<QString, QList<QPair<QString, IPluginTool *>>> submenuMap;
+ for (auto toolPlugin : toolPlugins) {
+ QStringList toolName = toolPlugin->displayName().split("/");
+ QString submenu = toolName[0];
+ toolName.pop_front();
+ submenuMap[submenu].append(QPair<QString, IPluginTool *>(toolName.join("/"), toolPlugin));
+ }
+
+ // Start registering plugins
+ for (auto submenuKey : submenuMap.keys()) {
+ if (submenuMap[submenuKey].length() > 1) {
+ QMenu *submenu = new QMenu(submenuKey, this);
+ 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);
+ }
+ else {
+ registerPluginTool(submenuMap[submenuKey].front().second);
+ }
+ }
+}
+
+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);
+
+ QToolButton *browserBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus));
+ browserBtn->menu()->addAction(nexusAction);
+ }
+
+ QAction *action = new QAction(modPage->icon(), modPage->displayName(), ui->toolBar);
+ 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);
+}
+
+
+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 {
+ qCritical("not an action?");
+ }
+}
+
+
+void MainWindow::setExecutableIndex(int index)
+{
+ QComboBox *executableBox = findChild<QComboBox*>("executablesListBox");
+
+ if ((index != 0) && (executableBox->count() > index)) {
+ executableBox->setCurrentIndex(index);
+ } else {
+ executableBox->setCurrentIndex(1);
+ }
+}
+
+void MainWindow::activateSelectedProfile()
+{
+ m_OrganizerCore.setCurrentProfile(ui->profileBox->currentText());
+
+ m_ModListSortProxy->setProfile(m_OrganizerCore.currentProfile());
+
+ refreshSaveList();
+ m_OrganizerCore.refreshModList();
+ updateModCount();
+ updatePluginCount();
+}
+
+void MainWindow::on_profileBox_currentIndexChanged(int index)
+{
+ if (ui->profileBox->isEnabled()) {
+ int previousIndex = m_OldProfileIndex;
+ m_OldProfileIndex = index;
+
+ if ((previousIndex != -1) &&
+ (m_OrganizerCore.currentProfile() != nullptr) &&
+ m_OrganizerCore.currentProfile()->exists()) {
+ m_OrganizerCore.saveCurrentLists();
+ }
+
+ // ensure the new index is valid
+ if (index < 0 || index >= ui->profileBox->count()) {
+ qDebug("invalid profile index, using last profile");
+ ui->profileBox->setCurrentIndex(ui->profileBox->count() - 1);
+ }
+
+ if (ui->profileBox->currentIndex() == 0) {
+ ui->profileBox->setCurrentIndex(previousIndex);
+ ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec();
+ while (!refreshProfiles()) {
+ ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore.managedGame(), this).exec();
+ }
+ } else {
+ activateSelectedProfile();
+ }
+
+ LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature<LocalSavegames>();
+ if (saveGames != nullptr) {
+ if (saveGames->prepareProfile(m_OrganizerCore.currentProfile()))
+ refreshSaveList();
+ }
+
+ BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
+ if (invalidation != nullptr) {
+ if (invalidation->prepareProfile(m_OrganizerCore.currentProfile()))
+ QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh()));
+ }
+ }
+}
+
+void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon)
+{
+ bool isDirectory = true;
+ //QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
+ //QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
+
+ std::wostringstream temp;
+ temp << directorySoFar << "\\" << directoryEntry.getName();
+ {
+ std::vector<DirectoryEntry*>::const_iterator current, end;
+ directoryEntry.getSubDirectories(current, end);
+ for (; current != end; ++current) {
+ QString pathName = ToQString((*current)->getName());
+ QStringList columns(pathName);
+ columns.append("");
+ if (!(*current)->isEmpty()) {
+ QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns);
+ directoryChild->setData(0, Qt::DecorationRole, *folderIcon);
+ directoryChild->setData(0, Qt::UserRole + 3, isDirectory);
+
+ if (conflictsOnly || !m_showArchiveData) {
+ updateTo(directoryChild, temp.str(), **current, conflictsOnly, fileIcon, folderIcon);
+ if (directoryChild->childCount() != 0) {
+ subTree->addChild(directoryChild);
+ }
+ else {
+ delete directoryChild;
+ }
+ }
+ else {
+ QTreeWidgetItem *onDemandLoad = new QTreeWidgetItem(QStringList());
+ onDemandLoad->setData(0, Qt::UserRole + 0, "__loaded_on_demand__");
+ onDemandLoad->setData(0, Qt::UserRole + 1, ToQString(temp.str()));
+ onDemandLoad->setData(0, Qt::UserRole + 2, conflictsOnly);
+ directoryChild->addChild(onDemandLoad);
+ subTree->addChild(directoryChild);
+ }
+ }
+ else {
+ QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns);
+ directoryChild->setData(0, Qt::DecorationRole, *folderIcon);
+ directoryChild->setData(0, Qt::UserRole + 3, isDirectory);
+ subTree->addChild(directoryChild);
+ }
+ }
+ }
+
+
+ isDirectory = false;
+ {
+ for (const FileEntry::Ptr current : directoryEntry.getFiles()) {
+ if (conflictsOnly && (current->getAlternatives().size() == 0)) {
+ continue;
+ }
+
+ bool isArchive = false;
+ int originID = current->getOrigin(isArchive);
+ if (!m_showArchiveData && isArchive) {
+ continue;
+ }
+
+ QString fileName = ToQString(current->getName());
+ QStringList columns(fileName);
+ FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
+ QString source("data");
+ unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
+ if (modIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ source = modInfo->name();
+ }
+
+ std::pair<std::wstring, int> archive = current->getArchive();
+ if (archive.first.length() != 0) {
+ source.append(" (").append(ToQString(archive.first)).append(")");
+ }
+ columns.append(source);
+ QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns);
+ if (isArchive) {
+ QFont font = fileChild->font(0);
+ font.setItalic(true);
+ fileChild->setFont(0, font);
+ fileChild->setFont(1, font);
+ } else if (fileName.endsWith(ModInfo::s_HiddenExt)) {
+ QFont font = fileChild->font(0);
+ font.setStrikeOut(true);
+ fileChild->setFont(0, font);
+ fileChild->setFont(1, font);
+ }
+ fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath()));
+ fileChild->setData(0, Qt::DecorationRole, *fileIcon);
+ fileChild->setData(0, Qt::UserRole + 3, isDirectory);
+ fileChild->setData(0, Qt::UserRole + 1, isArchive);
+ fileChild->setData(1, Qt::UserRole, source);
+ fileChild->setData(1, Qt::UserRole + 1, originID);
+
+ std::vector<std::pair<int, std::pair<std::wstring, int>>> alternatives = current->getAlternatives();
+
+ if (!alternatives.empty()) {
+ std::wostringstream altString;
+ altString << ToWString(tr("Also in: <br>"));
+ 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 << "<span style=\"white-space: nowrap;\"><i>" << m_OrganizerCore.directoryStructure()->getOriginByID(altIter->first).getName() << "</font></span>";
+ }
+ fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str())));
+ fileChild->setForeground(1, QBrush(Qt::red));
+ } else {
+ fileChild->setToolTip(1, tr("No conflict"));
+ }
+ subTree->addChild(fileChild);
+ }
+ }
+
+
+ //subTree->sortChildren(0, Qt::AscendingOrder);
+}
+
+void MainWindow::delayedRemove()
+{
+ for (QTreeWidgetItem *item : m_RemoveWidget) {
+ item->removeChild(item->child(0));
+ }
+ m_RemoveWidget.clear();
+}
+
+void MainWindow::expandDataTreeItem(QTreeWidgetItem *item)
+{
+ if ((item->childCount() == 1) && (item->child(0)->data(0, Qt::UserRole).toString() == "__loaded_on_demand__")) {
+ // read the data we need from the sub-item, then dispose of it
+ QTreeWidgetItem *onDemandDataItem = item->child(0);
+ std::wstring path = ToWString(onDemandDataItem->data(0, Qt::UserRole + 1).toString());
+ bool conflictsOnly = onDemandDataItem->data(0, Qt::UserRole + 2).toBool();
+
+ std::wstring virtualPath = (path + L"\\").substr(6) + ToWString(item->text(0));
+ DirectoryEntry *dir = m_OrganizerCore.directoryStructure()->findSubDirectoryRecursive(virtualPath);
+ if (dir != nullptr) {
+ QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
+ QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
+ updateTo(item, path, *dir, conflictsOnly, &fileIcon, &folderIcon);
+ } else {
+ qWarning("failed to update view of %ls", path.c_str());
+ }
+ m_RemoveWidget.push_back(item);
+ QTimer::singleShot(5, this, SLOT(delayedRemove()));
+ }
+}
+
+
+bool MainWindow::refreshProfiles(bool selectProfile)
+{
+ QComboBox* profileBox = findChild<QComboBox*>("profileBox");
+
+ QString currentProfileName = profileBox->currentText();
+
+ profileBox->blockSignals(true);
+ profileBox->clear();
+ profileBox->addItem(QObject::tr("<Manage...>"));
+
+ QDir profilesDir(Settings::instance().getProfileDirectory());
+ profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
+
+ QDirIterator profileIter(profilesDir);
+
+ while (profileIter.hasNext()) {
+ profileIter.next();
+ try {
+ profileBox->addItem(profileIter.fileName());
+ } catch (const std::runtime_error& error) {
+ reportError(QObject::tr("failed to parse profile %1: %2").arg(profileIter.fileName()).arg(error.what()));
+ }
+ }
+
+ // now select one of the profiles, preferably the one that was selected before
+ profileBox->blockSignals(false);
+
+ if (selectProfile) {
+ if (profileBox->count() > 1) {
+ profileBox->setCurrentText(currentProfileName);
+ if (profileBox->currentIndex() == 0) {
+ profileBox->setCurrentIndex(1);
+ }
+ }
+ }
+ return profileBox->count() > 1;
+}
+
+
+void MainWindow::refreshExecutablesList()
+{
+ QComboBox* executablesList = findChild<QComboBox*>("executablesListBox");
+ executablesList->setEnabled(false);
+ executablesList->clear();
+ executablesList->addItem(tr("<Edit...>"));
+
+ 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);
+ model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole);
+ }
+
+ setExecutableIndex(1);
+ executablesList->setEnabled(true);
+}
+
+
+void MainWindow::refreshDataTree()
+{
+ QCheckBox *conflictsBox = findChild<QCheckBox*>("conflictsCheckBox");
+ QTreeWidget *tree = findChild<QTreeWidget*>("dataTree");
+ QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
+ QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
+ tree->clear();
+ QStringList columns("data");
+ columns.append("");
+ QTreeWidgetItem *subTree = new QTreeWidgetItem(columns);
+ subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder));
+ updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon);
+ tree->insertTopLevelItem(0, subTree);
+ subTree->setExpanded(true);
+}
+
+void MainWindow::refreshDataTreeKeepExpandedNodes()
+{
+ QCheckBox *conflictsBox = findChild<QCheckBox*>("conflictsCheckBox");
+ QTreeWidget *tree = findChild<QTreeWidget*>("dataTree");
+ QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
+ QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
+ QStringList expandedNodes;
+ QTreeWidgetItemIterator it1(tree, QTreeWidgetItemIterator::NotHidden | QTreeWidgetItemIterator::HasChildren);
+ while (*it1) {
+ QTreeWidgetItem *current = (*it1);
+ if (current->isExpanded() && !(current->text(0)=="data")) {
+ expandedNodes.append(current->text(0)+"/"+current->parent()->text(0));
+ }
+ ++it1;
+ }
+
+ tree->clear();
+ QStringList columns("data");
+ columns.append("");
+ QTreeWidgetItem *subTree = new QTreeWidgetItem(columns);
+ subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder));
+ updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon);
+ tree->insertTopLevelItem(0, subTree);
+ subTree->setExpanded(true);
+ QTreeWidgetItemIterator it2(tree, QTreeWidgetItemIterator::HasChildren);
+ while (*it2) {
+ QTreeWidgetItem *current = (*it2);
+ if (!(current->text(0)=="data") && expandedNodes.contains(current->text(0)+"/"+current->parent()->text(0))) {
+ current->setExpanded(true);
+ }
+ ++it2;
+ }
+}
+
+
+void MainWindow::refreshSavesIfOpen()
+{
+ if (ui->tabWidget->currentIndex() == 3) {
+ refreshSaveList();
+ }
+}
+
+QDir MainWindow::currentSavesDir() const
+{
+ QDir savesDir;
+ if (m_OrganizerCore.currentProfile()->localSavesEnabled()) {
+ savesDir.setPath(m_OrganizerCore.currentProfile()->savePath());
+ } else {
+ QString iniPath = m_OrganizerCore.currentProfile()->localSettingsEnabled()
+ ? m_OrganizerCore.currentProfile()->absolutePath()
+ : m_OrganizerCore.managedGame()->documentsDirectory().absolutePath();
+ iniPath += "/" + m_OrganizerCore.managedGame()->iniFiles()[0];
+
+ wchar_t path[MAX_PATH];
+ ::GetPrivateProfileStringW(
+ L"General", L"SLocalSavePath", L"Saves",
+ path, MAX_PATH,
+ iniPath.toStdWString().c_str()
+ );
+ savesDir.setPath(m_OrganizerCore.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path)));
+ }
+
+ return savesDir;
+}
+
+void MainWindow::startMonitorSaves()
+{
+ stopMonitorSaves();
+
+ QDir savesDir = currentSavesDir();
+
+ m_SavesWatcher.addPath(savesDir.absolutePath());
+}
+
+void MainWindow::stopMonitorSaves()
+{
+ if (m_SavesWatcher.directories().length() > 0) {
+ m_SavesWatcher.removePaths(m_SavesWatcher.directories());
+ }
+}
+
+void MainWindow::refreshSaveList()
+{
+ ui->savegameList->clear();
+
+ startMonitorSaves(); // re-starts monitoring
+
+ QStringList filters;
+ filters << QString("*.") + m_OrganizerCore.managedGame()->savegameExtension();
+
+ QDir savesDir = currentSavesDir();
+ savesDir.setNameFilters(filters);
+ qDebug("reading save games from %s", qUtf8Printable(savesDir.absolutePath()));
+
+ QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time);
+ for (const QFileInfo &file : files) {
+ QListWidgetItem *item = new QListWidgetItem(file.fileName());
+ item->setData(Qt::UserRole, file.absoluteFilePath());
+ ui->savegameList->addItem(item);
+ }
+}
+
+
+static bool BySortValue(const std::pair<UINT32, QTreeWidgetItem*> &LHS, const std::pair<UINT32, QTreeWidgetItem*> &RHS)
+{
+ return LHS.first < RHS.first;
+}
+
+template <typename InputIterator>
+static QStringList toStringList(InputIterator current, InputIterator end)
+{
+ QStringList result;
+ for (; current != end; ++current) {
+ result.append(*current);
+ }
+ return result;
+}
+
+void MainWindow::updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives)
+{
+ m_DefaultArchives = defaultArchives;
+ ui->bsaList->clear();
+ ui->bsaList->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ std::vector<std::pair<UINT32, QTreeWidgetItem*>> items;
+
+ BSAInvalidation * invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
+ std::vector<FileEntry::Ptr> files = m_OrganizerCore.directoryStructure()->getFiles();
+
+ QStringList plugins = m_OrganizerCore.findFiles("", [](const QString &fileName) -> bool {
+ return fileName.endsWith(".esp", Qt::CaseInsensitive)
+ || fileName.endsWith(".esm", Qt::CaseInsensitive)
+ || fileName.endsWith(".esl", Qt::CaseInsensitive);
+ });
+
+ auto hasAssociatedPlugin = [&](const QString &bsaName) -> bool {
+ for (const QString &pluginName : plugins) {
+ QFileInfo pluginInfo(pluginName);
+ if (bsaName.startsWith(QFileInfo(pluginName).baseName(), Qt::CaseInsensitive)
+ && (m_OrganizerCore.pluginList()->state(pluginInfo.fileName()) == IPluginList::STATE_ACTIVE)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ for (FileEntry::Ptr current : files) {
+ QFileInfo fileInfo(ToQString(current->getName().c_str()));
+
+ if (fileInfo.suffix().toLower() == "bsa" || fileInfo.suffix().toLower() == "ba2") {
+ int index = activeArchives.indexOf(fileInfo.fileName());
+ if (index == -1) {
+ index = 0xFFFF;
+ }
+ else {
+ index += 2;
+ }
+
+ if ((invalidation != nullptr) && invalidation->isInvalidationBSA(fileInfo.fileName())) {
+ index = 1;
+ }
+
+ int originId = current->getOrigin();
+ FilesOrigin & origin = m_OrganizerCore.directoryStructure()->getOriginByID(originId);
+
+ QTreeWidgetItem * newItem = new QTreeWidgetItem(QStringList()
+ << fileInfo.fileName()
+ << ToQString(origin.getName()));
+ newItem->setData(0, Qt::UserRole, index);
+ newItem->setData(1, Qt::UserRole, originId);
+ newItem->setFlags(newItem->flags() & ~(Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable));
+ newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked);
+ newItem->setData(0, Qt::UserRole, false);
+ if (m_OrganizerCore.settings().forceEnableCoreFiles()
+ && defaultArchives.contains(fileInfo.fileName())) {
+ newItem->setCheckState(0, Qt::Checked);
+ newItem->setDisabled(true);
+ newItem->setData(0, Qt::UserRole, true);
+ } else if (fileInfo.fileName().compare("update.bsa", Qt::CaseInsensitive) == 0) {
+ newItem->setCheckState(0, Qt::Checked);
+ newItem->setDisabled(true);
+ } else if (hasAssociatedPlugin(fileInfo.fileName())) {
+ newItem->setCheckState(0, Qt::Checked);
+ newItem->setDisabled(true);
+ } else {
+ newItem->setCheckState(0, Qt::Unchecked);
+ newItem->setDisabled(true);
+ }
+ if (index < 0) index = 0;
+
+ UINT32 sortValue = ((origin.getPriority() & 0xFFFF) << 16) | (index & 0xFFFF);
+ items.push_back(std::make_pair(sortValue, newItem));
+ }
+ }
+ std::sort(items.begin(), items.end(), BySortValue);
+
+ for (auto iter = items.begin(); iter != items.end(); ++iter) {
+ int originID = iter->second->data(1, Qt::UserRole).toInt();
+
+ FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
+ QString modName("data");
+ unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
+ if (modIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ modName = modInfo->name();
+ }
+ QList<QTreeWidgetItem*> items = ui->bsaList->findItems(modName, Qt::MatchFixedString);
+ QTreeWidgetItem * subItem = nullptr;
+ if (items.length() > 0) {
+ subItem = items.at(0);
+ }
+ else {
+ subItem = new QTreeWidgetItem(QStringList(modName));
+ subItem->setFlags(subItem->flags() & ~Qt::ItemIsDragEnabled);
+ ui->bsaList->addTopLevelItem(subItem);
+ }
+ subItem->addChild(iter->second);
+ subItem->setExpanded(true);
+ }
+ checkBSAList();
+}
+
+void MainWindow::checkBSAList()
+{
+ DataArchives * archives = m_OrganizerCore.managedGame()->feature<DataArchives>();
+
+ if (archives != nullptr) {
+ ui->bsaList->blockSignals(true);
+ ON_BLOCK_EXIT([&]() { ui->bsaList->blockSignals(false); });
+
+ QStringList defaultArchives = archives->archives(m_OrganizerCore.currentProfile());
+
+ bool warning = false;
+
+ for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) {
+ bool modWarning = false;
+ QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i);
+ for (int j = 0; j < tlItem->childCount(); ++j) {
+ QTreeWidgetItem * item = tlItem->child(j);
+ QString filename = item->text(0);
+ item->setIcon(0, QIcon());
+ item->setToolTip(0, QString());
+
+ if (item->checkState(0) == Qt::Unchecked) {
+ if (defaultArchives.contains(filename)) {
+ item->setIcon(0, QIcon(":/MO/gui/warning"));
+ item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!"));
+ modWarning = true;
+ }
+ }
+ }
+ if (modWarning) {
+ ui->bsaList->expandItem(ui->bsaList->topLevelItem(i));
+ warning = true;
+ }
+ }
+ if (warning) {
+ ui->tabWidget->setTabIcon(1, QIcon(":/MO/gui/warning"));
+ } else {
+ ui->tabWidget->setTabIcon(1, QIcon());
+ }
+ }
+}
+
+void MainWindow::saveModMetas()
+{
+ if (m_MetaSave.isFinished()) {
+ m_MetaSave = QtConcurrent::run([this]() {
+ for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ modInfo->saveMeta();
+ }
+ });
+ }
+}
+
+void MainWindow::fixCategories()
+{
+ for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ std::set<int> categories = modInfo->getCategories();
+ for (std::set<int>::iterator iter = categories.begin();
+ iter != categories.end(); ++iter) {
+ if (!m_CategoryFactory.categoryExists(*iter)) {
+ modInfo->setCategory(*iter, false);
+ }
+ }
+ }
+}
+
+
+void MainWindow::setupNetworkProxy(bool activate)
+{
+ QNetworkProxyFactory::setUseSystemConfiguration(activate);
+/* QNetworkProxyQuery query(QUrl("http://www.google.com"), QNetworkProxyQuery::UrlRequest);
+ query.setProtocolTag("http");
+ QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery(query);
+ if ((proxies.size() > 0) && (proxies.at(0).type() != QNetworkProxy::NoProxy)) {
+ qDebug("Using proxy: %s", qUtf8Printable(proxies.at(0).hostName()));
+ QNetworkProxy::setApplicationProxy(proxies[0]);
+ } else {
+ qDebug("Not using proxy");
+ }*/
+}
+
+
+void MainWindow::activateProxy(bool activate)
+{
+ QProgressDialog busyDialog(tr("Activating Network Proxy"), QString(), 0, 0, parentWidget());
+ busyDialog.setWindowFlags(busyDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ busyDialog.setWindowModality(Qt::WindowModal);
+ busyDialog.show();
+ QFuture<void> future = QtConcurrent::run(MainWindow::setupNetworkProxy, activate);
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ ::Sleep(100);
+ }
+ busyDialog.hide();
+}
+
+void MainWindow::readSettings()
+{
+ QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat);
+
+ if (settings.contains("window_geometry")) {
+ restoreGeometry(settings.value("window_geometry").toByteArray());
+ }
+
+ if (settings.contains("window_split")) {
+ ui->splitter->restoreState(settings.value("window_split").toByteArray());
+ }
+
+ if (settings.contains("log_split")) {
+ ui->topLevelSplitter->restoreState(settings.value("log_split").toByteArray());
+ }
+
+ bool filtersVisible = settings.value("filters_visible", false).toBool();
+ setCategoryListVisible(filtersVisible);
+ ui->displayCategoriesBtn->setChecked(filtersVisible);
+
+ int selectedExecutable = settings.value("selected_executable").toInt();
+ setExecutableIndex(selectedExecutable);
+
+ if (settings.value("Settings/use_proxy", false).toBool()) {
+ activateProxy(true);
+ }
+}
+
+void MainWindow::processUpdates() {
+ QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat);
+ QVersionNumber lastVersion = QVersionNumber::fromString(settings.value("version", "2.1.2").toString()).normalized();
+ QVersionNumber currentVersion = QVersionNumber::fromString(m_OrganizerCore.getVersion().displayString()).normalized();
+ if (!m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) {
+ if (lastVersion < QVersionNumber(2, 1, 3)) {
+ bool lastHidden = true;
+ for (int i = ModList::COL_GAME; i < ui->modList->model()->columnCount(); ++i) {
+ bool hidden = ui->modList->header()->isSectionHidden(i);
+ ui->modList->header()->setSectionHidden(i, lastHidden);
+ lastHidden = hidden;
+ }
+ }
+ if (lastVersion < QVersionNumber(2, 1, 6)) {
+ ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
+ }
+ if (lastVersion < QVersionNumber(2, 2, 0)) {
+ QSettings &instance = Settings::instance().directInterface();
+ instance.beginGroup("Settings");
+ instance.remove("steam_password");
+ instance.remove("nexus_username");
+ instance.remove("nexus_password");
+ instance.remove("nexus_login");
+ instance.remove("nexus_api_key");
+ instance.remove("ask_for_nexuspw");
+ instance.remove("nmm_version");
+ instance.endGroup();
+ instance.beginGroup("Servers");
+ instance.remove("");
+ instance.endGroup();
+ }
+ }
+
+ if (currentVersion > lastVersion) {
+ //NOP
+ } else if (currentVersion < lastVersion)
+ qWarning() << tr("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.").arg(currentVersion.toString()).arg(lastVersion.toString()).toStdWString();
+ //save version in all case
+ settings.setValue("version", currentVersion.toString());
+}
+
+void MainWindow::storeSettings(QSettings &settings) {
+ settings.setValue("group_state", ui->groupCombo->currentIndex());
+ settings.setValue("selected_executable",
+ ui->executablesListBox->currentIndex());
+
+ if (settings.value("reset_geometry", false).toBool()) {
+ settings.remove("window_geometry");
+ settings.remove("window_split");
+ settings.remove("log_split");
+ settings.remove("filters_visible");
+ settings.remove("browser_geometry");
+ settings.remove("geometry");
+ settings.remove("reset_geometry");
+ } else {
+ settings.setValue("window_geometry", saveGeometry());
+ settings.setValue("window_split", ui->splitter->saveState());
+ settings.setValue("log_split", ui->topLevelSplitter->saveState());
+ settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry());
+ settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked());
+ for (const std::pair<QString, QHeaderView*> kv : m_PersistedGeometry) {
+ QString key = QString("geometry/") + kv.first;
+ settings.setValue(key, kv.second->saveState());
+ }
+ }
+}
+
+ILockedWaitingForProcess* MainWindow::lock()
+{
+ if (m_LockDialog != nullptr) {
+ ++m_LockCount;
+ return m_LockDialog;
+ }
+ if (m_closing)
+ m_LockDialog = new WaitingOnCloseDialog(this);
+ else
+ m_LockDialog = new LockedDialog(this, true);
+ m_LockDialog->setModal(true);
+ m_LockDialog->show();
+ setEnabled(false);
+ m_LockDialog->setEnabled(true); //What's the point otherwise?
+ ++m_LockCount;
+ return m_LockDialog;
+}
+
+void MainWindow::unlock()
+{
+ //If you come through here with a null lock pointer, it's a bug!
+ if (m_LockDialog == nullptr) {
+ qDebug("Unlocking main window when already unlocked");
+ return;
+ }
+ --m_LockCount;
+ if (m_LockCount == 0) {
+ if (m_closing && m_LockDialog->canceled())
+ m_closing = false;
+ m_LockDialog->hide();
+ m_LockDialog->deleteLater();
+ m_LockDialog = nullptr;
+ setEnabled(true);
+ }
+}
+
+void MainWindow::on_btnRefreshData_clicked()
+{
+ m_OrganizerCore.refreshDirectoryStructure();
+}
+
+void MainWindow::on_btnRefreshDownloads_clicked()
+{
+ m_OrganizerCore.downloadManager()->refreshList();
+}
+
+void MainWindow::on_tabWidget_currentChanged(int index)
+{
+ if (index == 0) {
+ m_OrganizerCore.refreshESPList();
+ } else if (index == 1) {
+ m_OrganizerCore.refreshBSAList();
+ } else if (index == 2) {
+ refreshDataTreeKeepExpandedNodes();
+ } else if (index == 3) {
+ refreshSaveList();
+ }
+}
+
+
+void MainWindow::installMod(QString fileName)
+{
+ try {
+ if (fileName.isEmpty()) {
+ QStringList extensions = m_OrganizerCore.installationManager()->getSupportedExtensions();
+ for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) {
+ *iter = "*." + *iter;
+ }
+
+ fileName = FileDialogMemory::getOpenFileName("installMod", this, tr("Choose Mod"), QString(),
+ tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" "))));
+ }
+
+ if (fileName.isEmpty()) {
+ return;
+ } else {
+ m_OrganizerCore.installMod(fileName, QString());
+ }
+ } catch (const std::exception &e) {
+ reportError(e.what());
+ }
+}
+
+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)) {
+ 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);
+ } catch (...) {
+ ui->startButton->setEnabled(true);
+ throw;
+ }
+ 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());
+ 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;
+ }
+ settings.setValue(key, dialog.saveGeometry());
+ refreshExecutablesList();
+ } catch (const std::exception &e) {
+ reportError(e.what());
+ }
+ return result;
+}
+
+void MainWindow::on_executablesListBox_currentIndexChanged(int index)
+{
+ QComboBox* executablesList = findChild<QComboBox*>("executablesListBox");
+
+ int previousIndex = m_OldExecutableIndex;
+ m_OldExecutableIndex = index;
+
+ if (executablesList->isEnabled()) {
+ //I think the 2nd test is impossible
+ if ((index == 0) || (index > static_cast<int>(m_OrganizerCore.executablesList()->size()))) {
+ if (modifyExecutablesDialog()) {
+ setExecutableIndex(previousIndex);
+ }
+ } else {
+ setExecutableIndex(index);
+ }
+ }
+}
+
+void MainWindow::helpTriggered()
+{
+ QWhatsThis::enterWhatsThisMode();
+}
+
+void MainWindow::wikiTriggered()
+{
+ QDesktopServices::openUrl(QUrl("https://modorganizer2.github.io/"));
+}
+
+void MainWindow::discordTriggered()
+{
+ QDesktopServices::openUrl(QUrl("https://discord.gg/cYwdcxj"));
+}
+
+void MainWindow::issueTriggered()
+{
+ QDesktopServices::openUrl(QUrl("https://github.com/Modorganizer2/modorganizer/issues"));
+}
+
+void MainWindow::tutorialTriggered()
+{
+ QAction *tutorialAction = qobject_cast<QAction*>(sender());
+ if (tutorialAction != nullptr) {
+ if (QMessageBox::question(this, tr("Start Tutorial?"),
+ tr("You're about to start a tutorial. For technical reasons it's not possible to end "
+ "the tutorial early. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ TutorialManager::instance().activateTutorial("MainWindow", tutorialAction->data().toString());
+ }
+ }
+}
+
+
+void MainWindow::on_actionInstallMod_triggered()
+{
+ installMod();
+}
+
+void MainWindow::on_actionAdd_Profile_triggered()
+{
+ for (;;) {
+ ProfilesDialog profilesDialog(m_OrganizerCore.currentProfile()->name(),
+ m_OrganizerCore.managedGame(),
+ this);
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+ QString key = QString("geometry/%1").arg(profilesDialog.objectName());
+ if (settings.contains(key)) {
+ profilesDialog.restoreGeometry(settings.value(key).toByteArray());
+ }
+ // workaround: need to disable monitoring of the saves directory, otherwise the active
+ // profile directory is locked
+ stopMonitorSaves();
+ profilesDialog.exec();
+ settings.setValue(key, profilesDialog.saveGeometry());
+ refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
+ if (refreshProfiles() && !profilesDialog.failed()) {
+ break;
+ }
+ }
+
+ LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature<LocalSavegames>();
+ if (saveGames != nullptr) {
+ if (saveGames->prepareProfile(m_OrganizerCore.currentProfile()))
+ refreshSaveList();
+ }
+
+ BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
+ if (invalidation != nullptr) {
+ if (invalidation->prepareProfile(m_OrganizerCore.currentProfile()))
+ QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh()));
+ }
+}
+
+void MainWindow::on_actionModify_Executables_triggered()
+{
+ if (modifyExecutablesDialog()) {
+ setExecutableIndex(m_OldExecutableIndex);
+ }
+}
+
+
+void MainWindow::setModListSorting(int index)
+{
+ Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder;
+ int column = index >> 1;
+ ui->modList->header()->setSortIndicator(column, order);
+}
+
+
+void MainWindow::setESPListSorting(int index)
+{
+ switch (index) {
+ case 0: {
+ ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder);
+ } break;
+ case 1: {
+ ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder);
+ } break;
+ case 2: {
+ ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder);
+ } break;
+ case 3: {
+ ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder);
+ } break;
+ }
+}
+
+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);
+ }
+}
+
+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();
+}
+
+void MainWindow::esplist_changed()
+{
+ updatePluginCount();
+}
+
+void MainWindow::modorder_changed()
+{
+ for (unsigned int i = 0; i < m_OrganizerCore.currentProfile()->numMods(); ++i) {
+ int priority = m_OrganizerCore.currentProfile()->getModPriority(i);
+ if (m_OrganizerCore.currentProfile()->modEnabled(i)) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ // priorities in the directory structure are one higher because data is 0
+ m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->internalName())).setPriority(priority + 1);
+ }
+ }
+ m_OrganizerCore.refreshBSAList();
+ m_OrganizerCore.currentProfile()->writeModlist();
+ m_ArchiveListWriter.write();
+ m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins();
+
+ { // refresh selection
+ QModelIndex current = ui->modList->currentIndex();
+ if (current.isValid()) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt());
+ // clear caches on all mods conflicting with the moved mod
+ for (int i : modInfo->getModOverwrite()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModOverwritten()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveOverwrite()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveOverwritten()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveLooseOverwrite()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveLooseOverwritten()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ // update conflict check on the moved mod
+ modInfo->doConflictCheck();
+ m_OrganizerCore.modList()->setOverwriteMarkers(modInfo->getModOverwrite(), modInfo->getModOverwritten());
+ m_OrganizerCore.modList()->setArchiveOverwriteMarkers(modInfo->getModArchiveOverwrite(), modInfo->getModArchiveOverwritten());
+ m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(modInfo->getModArchiveLooseOverwrite(), modInfo->getModArchiveLooseOverwritten());
+ if (m_ModListSortProxy != nullptr)
+ m_ModListSortProxy->invalidate();
+ ui->modList->verticalScrollBar()->repaint();
+ }
+ }
+}
+
+void MainWindow::modInstalled(const QString &modName)
+{
+ QModelIndexList posList =
+ m_OrganizerCore.modList()->match(m_OrganizerCore.modList()->index(0, 0), Qt::DisplayRole, modName);
+ if (posList.count() == 1) {
+ ui->modList->scrollTo(posList.at(0));
+ }
+
+ // force an update to happen
+ std::multimap<QString, int> IDs;
+ ModInfo::Ptr info = ModInfo::getByIndex(ModInfo::getIndex(modName));
+ IDs.insert(std::make_pair<QString, int>(info->getGameName(), info->getNexusID()));
+ modUpdateCheck(IDs);
+}
+
+void MainWindow::procError(QProcess::ProcessError error)
+{
+ reportError(tr("failed to spawn notepad.exe: %1").arg(error));
+ this->sender()->deleteLater();
+}
+
+void MainWindow::procFinished(int, QProcess::ExitStatus)
+{
+ this->sender()->deleteLater();
+}
+
+void MainWindow::showMessage(const QString &message)
+{
+ MessageDialog::showMessage(message, this);
+}
+
+void MainWindow::showError(const QString &message)
+{
+ reportError(message);
+}
+
+void MainWindow::installMod_clicked()
+{
+ installMod();
+}
+
+void MainWindow::modRenamed(const QString &oldName, const QString &newName)
+{
+ Profile::renameModInAllProfiles(oldName, newName);
+
+ // immediately refresh the active profile because the data in memory is invalid
+ m_OrganizerCore.currentProfile()->refreshModStatus();
+
+ // also fix the directory structure
+ try {
+ if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldName))) {
+ FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldName));
+ origin.setName(ToWString(newName));
+ } else {
+
+ }
+ } catch (const std::exception &e) {
+ reportError(tr("failed to change origin name: %1").arg(e.what()));
+ }
+}
+
+void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName)
+{
+ const FileEntry::Ptr filePtr = m_OrganizerCore.directoryStructure()->findFile(ToWString(filePath));
+ if (filePtr.get() != nullptr) {
+ try {
+ if (m_OrganizerCore.directoryStructure()->originExists(ToWString(newOriginName))) {
+ FilesOrigin &newOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(newOriginName));
+
+ QString fullNewPath = ToQString(newOrigin.getPath()) + "\\" + filePath;
+ WIN32_FIND_DATAW findData;
+ HANDLE hFind;
+ hFind = ::FindFirstFileW(ToWString(fullNewPath).c_str(), &findData);
+ filePtr->addOrigin(newOrigin.getID(), findData.ftCreationTime, L"", -1);
+ FindClose(hFind);
+ }
+ if (m_OrganizerCore.directoryStructure()->originExists(ToWString(oldOriginName))) {
+ FilesOrigin &oldOrigin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(oldOriginName));
+ filePtr->removeOrigin(oldOrigin.getID());
+ }
+ } catch (const std::exception &e) {
+ reportError(tr("failed to move \"%1\" from mod \"%2\" to \"%3\": %4").arg(filePath).arg(oldOriginName).arg(newOriginName).arg(e.what()));
+ }
+ } else {
+ // this is probably not an error, the specified path is likely a directory
+ }
+}
+
+QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type)
+{
+ QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name));
+ item->setData(0, Qt::ToolTipRole, name);
+ item->setData(0, Qt::UserRole, categoryID);
+ item->setData(0, Qt::UserRole + 1, type);
+ if (root != nullptr) {
+ root->addChild(item);
+ } else {
+ ui->categoriesList->addTopLevelItem(item);
+ }
+ return item;
+}
+
+void MainWindow::addContentFilters()
+{
+ for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) {
+ addFilterItem(nullptr, tr("<Contains %1>").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT);
+ }
+}
+
+void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID)
+{
+ for (unsigned int i = 1;
+ i < static_cast<unsigned int>(m_CategoryFactory.numCategories()); ++i) {
+ if ((m_CategoryFactory.getParentID(i) == targetID)) {
+ int categoryID = m_CategoryFactory.getCategoryID(i);
+ if (categoriesUsed.find(categoryID) != categoriesUsed.end()) {
+ QTreeWidgetItem *item =
+ addFilterItem(root, m_CategoryFactory.getCategoryName(i),
+ categoryID, ModListSortProxy::TYPE_CATEGORY);
+ if (m_CategoryFactory.hasChildren(i)) {
+ addCategoryFilters(item, categoriesUsed, categoryID);
+ }
+ }
+ }
+ }
+}
+
+void MainWindow::refreshFilters()
+{
+ QItemSelection currentSelection = ui->modList->selectionModel()->selection();
+
+ QVariant currentIndexName = ui->modList->currentIndex().data();
+ ui->modList->setCurrentIndex(QModelIndex());
+
+ QStringList selectedItems;
+ for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) {
+ selectedItems.append(item->text(0));
+ }
+
+ ui->categoriesList->clear();
+ addFilterItem(nullptr, tr("<Checked>"), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Unchecked>"), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Update>"), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Mod Backup>"), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Managed by MO>"), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Managed outside MO>"), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<No category>"), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Conflicted>"), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL);
+ addFilterItem(nullptr, tr("<Not Endorsed>"), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL);
+
+ addContentFilters();
+ std::set<int> categoriesUsed;
+ for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx);
+ for (int categoryID : modInfo->getCategories()) {
+ int currentID = categoryID;
+ std::set<int> cycleTest;
+ // also add parents so they show up in the tree
+ while (currentID != 0) {
+ categoriesUsed.insert(currentID);
+ if (!cycleTest.insert(currentID).second) {
+ qWarning("cycle in categories: %s", qUtf8Printable(SetJoin(cycleTest, ", ")));
+ break;
+ }
+ currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID));
+ }
+ }
+ }
+
+ addCategoryFilters(nullptr, categoriesUsed, 0);
+
+ for (const QString &item : selectedItems) {
+ QList<QTreeWidgetItem*> matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive);
+ if (matches.size() > 0) {
+ matches.at(0)->setSelected(true);
+ }
+ }
+ ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select);
+ QModelIndexList matchList;
+ if (currentIndexName.isValid()) {
+ matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName);
+ }
+
+ if (matchList.size() > 0) {
+ ui->modList->setCurrentIndex(matchList.at(0));
+ }
+}
+
+
+void MainWindow::renameMod_clicked()
+{
+ try {
+ ui->modList->edit(ui->modList->currentIndex());
+ } catch (const std::exception &e) {
+ reportError(tr("failed to rename mod: %1").arg(e.what()));
+ }
+}
+
+
+void MainWindow::restoreBackup_clicked()
+{
+ QRegExp backupRegEx("(.*)_backup[0-9]*$");
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ if (backupRegEx.indexIn(modInfo->name()) != -1) {
+ QString regName = backupRegEx.cap(1);
+ QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()));
+ if (!modDir.exists(regName) ||
+ (QMessageBox::question(this, tr("Overwrite?"),
+ tr("This will replace the existing mod \"%1\". Continue?").arg(regName),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) {
+ if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) {
+ reportError(tr("failed to remove mod \"%1\"").arg(regName));
+ } else {
+ QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()) + "/" + regName;
+ if (!modDir.rename(modInfo->absolutePath(), destinationPath)) {
+ reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath));
+ }
+ m_OrganizerCore.refreshModList();
+ }
+ }
+ }
+}
+
+void MainWindow::modlistChanged(const QModelIndex&, int)
+{
+ m_OrganizerCore.currentProfile()->writeModlist();
+ updateModCount();
+}
+
+void MainWindow::modlistChanged(const QModelIndexList&, int)
+{
+ m_OrganizerCore.currentProfile()->writeModlist();
+ updateModCount();
+}
+
+void MainWindow::modlistSelectionsChanged(const QItemSelection &selected)
+{
+ if (selected.count()) {
+ auto selection = selected.last();
+ auto index = selection.indexes().last();
+ ModInfo::Ptr selectedMod = ModInfo::getByIndex(index.data(Qt::UserRole + 1).toInt());
+ m_OrganizerCore.modList()->setOverwriteMarkers(selectedMod->getModOverwrite(), selectedMod->getModOverwritten());
+ m_OrganizerCore.modList()->setArchiveOverwriteMarkers(selectedMod->getModArchiveOverwrite(), selectedMod->getModArchiveOverwritten());
+ m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(selectedMod->getModArchiveLooseOverwrite(), selectedMod->getModArchiveLooseOverwritten());
+ } else {
+ m_OrganizerCore.modList()->setOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>());
+ m_OrganizerCore.modList()->setArchiveOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>());
+ m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>());
+ }
+ ui->modList->verticalScrollBar()->repaint();
+
+ m_OrganizerCore.pluginList()->highlightPlugins(ui->modList->selectionModel(), *m_OrganizerCore.directoryStructure(), *m_OrganizerCore.currentProfile());
+ ui->espList->verticalScrollBar()->repaint();
+}
+
+void MainWindow::esplistSelectionsChanged(const QItemSelection &selected)
+{
+ m_OrganizerCore.modList()->highlightMods(ui->espList->selectionModel(), *m_OrganizerCore.directoryStructure());
+ ui->modList->verticalScrollBar()->repaint();
+}
+
+void MainWindow::modListSortIndicatorChanged(int, Qt::SortOrder)
+{
+ ui->modList->verticalScrollBar()->repaint();
+}
+
+void MainWindow::modListSectionResized(int logicalIndex, int oldSize, int newSize)
+{
+ bool enabled = (newSize != 0);
+ qobject_cast<ModListSortProxy *>(ui->modList->model())->setColumnVisible(logicalIndex, enabled);
+}
+
+void MainWindow::removeMod_clicked()
+{
+ try {
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ QString mods;
+ QStringList modNames;
+ 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>";
+ modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name());
+ }
+ if (QMessageBox::question(this, tr("Confirm"),
+ tr("Remove the following mods?<br><ul>%1</ul>").arg(mods),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ // use mod names instead of indexes because those become invalid during the removal
+ DownloadManager::startDisableDirWatcher();
+ for (QString name : modNames) {
+ m_OrganizerCore.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex());
+ }
+ DownloadManager::endDisableDirWatcher();
+ }
+ } else {
+ m_OrganizerCore.modList()->removeRow(m_ContextRow, QModelIndex());
+ }
+ updateModCount();
+ updatePluginCount();
+ } catch (const std::exception &e) {
+ reportError(tr("failed to remove mod: %1").arg(e.what()));
+ }
+}
+
+
+void MainWindow::modRemoved(const QString &fileName)
+{
+ if (!fileName.isEmpty() && !QFileInfo(fileName).isAbsolute()) {
+ m_OrganizerCore.downloadManager()->markUninstalled(fileName);
+ }
+}
+
+
+void MainWindow::reinstallMod_clicked()
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ QString installationFile = modInfo->getInstallationFile();
+ if (installationFile.length() != 0) {
+ QString fullInstallationFile;
+ QFileInfo fileInfo(installationFile);
+ if (fileInfo.isAbsolute()) {
+ if (fileInfo.exists()) {
+ fullInstallationFile = installationFile;
+ } else {
+ fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName();
+ }
+ } else {
+ fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + installationFile;
+ }
+ if (QFile::exists(fullInstallationFile)) {
+ m_OrganizerCore.installMod(fullInstallationFile, modInfo->name());
+ } else {
+ QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists"));
+ }
+ } else {
+ QMessageBox::information(this, tr("Failed"),
+ tr("Mods installed with old versions of MO can't be reinstalled in this way."));
+ }
+}
+
+void MainWindow::backupMod_clicked()
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ QString backupDirectory = m_OrganizerCore.installationManager()->generateBackupName(modInfo->absolutePath());
+ if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) {
+ QMessageBox::information(this, tr("Failed"),
+ tr("Failed to create backup."));
+ }
+ m_OrganizerCore.refreshModList();
+}
+
+void MainWindow::resumeDownload(int downloadIndex)
+{
+ if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) {
+ 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()) {
+ 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()) {
+ 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;
+ }
+ }
+ }
+ else {
+ endorseMod(ModInfo::getByIndex(m_ContextRow));
+ }
+}
+
+void MainWindow::dontendorse_clicked()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->setNeverEndorse();
+ }
+ }
+ else {
+ ModInfo::getByIndex(m_ContextRow)->setNeverEndorse();
+ }
+}
+
+
+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);
+ }
+ }
+}
+
+
+void MainWindow::unendorse_clicked()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) {
+ 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));
+ }
+}
+
+
+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);
+ }
+ }
+}
+
+
+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);
+ }
+ }
+ } 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);
+ }
+ }
+ } 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)
+{
+ m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true);
+}
+
+void MainWindow::overwriteClosed(int)
+{
+ OverwriteInfoDialog *dialog = this->findChild<OverwriteInfoDialog*>("__overwriteDialog");
+ if (dialog != nullptr) {
+ m_OrganizerCore.modList()->modInfoChanged(dialog->modInfo());
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+ QString key = QString("geometry/%1").arg(dialog->objectName());
+ settings.setValue(key, dialog->saveGeometry());
+ dialog->deleteLater();
+ }
+ m_OrganizerCore.refreshDirectoryStructure();
+}
+
+
+void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab)
+{
+ if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) {
+ qDebug("A different mod information dialog is open. If this is incorrect, please restart MO");
+ return;
+ }
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) {
+ QDialog *dialog = this->findChild<QDialog*>("__overwriteDialog");
+ try {
+ if (dialog == nullptr) {
+ dialog = new OverwriteInfoDialog(modInfo, this);
+ dialog->setObjectName("__overwriteDialog");
+ } else {
+ qobject_cast<OverwriteInfoDialog*>(dialog)->setModInfo(modInfo);
+ }
+ 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->show();
+ dialog->raise();
+ dialog->activateWindow();
+ connect(dialog, SIGNAL(finished(int)), this, SLOT(overwriteClosed(int)));
+ } catch (const std::exception &e) {
+ reportError(tr("Failed to display overwrite dialog: %1").arg(e.what()));
+ }
+ } 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);
+ }
+
+ 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());
+ }
+
+ //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.exec();
+ m_OrganizerCore.settings().directInterface().setValue("mod_info_tabs", dialog.saveTabState());
+ settings.setValue(key, dialog.saveGeometry());
+
+ modInfo->saveMeta();
+ emit modInfoDisplayed();
+ m_OrganizerCore.modList()->modInfoChanged(modInfo);
+ }
+
+ if (m_OrganizerCore.currentProfile()->modEnabled(index)
+ && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) {
+ FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name()));
+ origin.enable(false);
+
+ if (m_OrganizerCore.directoryStructure()->originExists(ToWString(modInfo->name()))) {
+ FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name()));
+ origin.enable(false);
+
+ m_OrganizerCore.directoryRefresher()->addModToStructure(m_OrganizerCore.directoryStructure()
+ , modInfo->name()
+ , m_OrganizerCore.currentProfile()->getModPriority(index)
+ , modInfo->absolutePath()
+ , modInfo->stealFiles()
+ , modInfo->archives());
+ DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure());
+ m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins();
+ m_OrganizerCore.refreshLists();
+ }
+ }
+}
+
+bool MainWindow::closeWindow()
+{
+ return close();
+}
+
+void MainWindow::setWindowEnabled(bool enabled)
+{
+ setEnabled(enabled);
+}
+
+
+void MainWindow::modOpenNext(int tab)
+{
+ QModelIndex index = m_ModListSortProxy->mapFromSource(m_OrganizerCore.modList()->index(m_ContextRow, 0));
+ index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0);
+
+ 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);
+ }
+}
+
+void MainWindow::modOpenPrev(int tab)
+{
+ 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;
+ }
+
+ 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);
+ }
+}
+
+void MainWindow::displayModInformation(const QString &modName, int tab)
+{
+ unsigned int index = ModInfo::getIndex(modName);
+ if (index == UINT_MAX) {
+ qCritical("failed to resolve mod name %s", qUtf8Printable(modName));
+ return;
+ }
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ displayModInformation(modInfo, index, tab);
+}
+
+
+void MainWindow::displayModInformation(int row, int tab)
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
+ displayModInformation(modInfo, row, tab);
+}
+
+
+void MainWindow::ignoreMissingData_clicked()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ int row_idx = idx.data(Qt::UserRole + 1).toInt();
+ ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
+ //QDir(info->absolutePath()).mkdir("textures");
+ info->testValid();
+ info->markValidated(true);
+ connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
+
+ emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1));
+ }
+ } else {
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+ //QDir(info->absolutePath()).mkdir("textures");
+ info->testValid();
+ info->markValidated(true);
+ connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
+
+ emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1));
+ }
+}
+
+void MainWindow::markConverted_clicked()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ int row_idx = idx.data(Qt::UserRole + 1).toInt();
+ ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
+ info->markConverted(true);
+ connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
+ emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1));
+ }
+ } else {
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+ info->markConverted(true);
+ connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
+ emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1));
+ }
+}
+
+
+void MainWindow::visitOnNexus_clicked()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ int count = selection->selectedRows().count();
+ if (count > 10) {
+ if (QMessageBox::question(this, tr("Opening Nexus Links"),
+ tr("You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?").arg(count),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+ 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 {
+ int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt();
+ QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString();
+ if (modID > 0) {
+ linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName));
+ } else {
+ 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();
+ if (count > 10) {
+ if (QMessageBox::question(this, tr("Opening Web Pages"),
+ tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(count),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+ 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 {
+ 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);
+ }
+ }
+}
+
+void MainWindow::openExplorer_clicked()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ 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);
+ }
+ }
+ else {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ }
+}
+
+void MainWindow::openOriginExplorer_clicked()
+{
+ QItemSelectionModel *selection = ui->espList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 0) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ QString fileName = idx.data().toString();
+ unsigned int modIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName));
+ if (modIndex == UINT_MAX) {
+ 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);
+ }
+ }
+ 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);
+ }
+}
+
+void MainWindow::openExplorer_activated()
+{
+ if (ui->modList->hasFocus()) {
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() == 1 ) {
+
+ QModelIndex idx = selection->currentIndex();
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
+ 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);
+ }
+
+ }
+ }
+
+ if (ui->espList->hasFocus()) {
+ QItemSelectionModel *selection = ui->espList->selectionModel();
+
+ if (selection->hasSelection() && selection->selectedRows().count() == 1) {
+
+ QModelIndex idx = selection->currentIndex();
+ QString fileName = idx.data().toString();
+
+
+ unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName));
+ if (modInfoIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex);
+ 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);
+ }
+ }
+ }
+ }
+}
+
+void MainWindow::refreshProfile_activated()
+{
+ m_OrganizerCore.profileRefresh();
+}
+
+void MainWindow::search_activated()
+{
+ if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) {
+ ui->modFilterEdit->setFocus();
+ ui->modFilterEdit->setSelection(0, INT_MAX);
+ }
+
+ else if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) {
+ ui->espFilterEdit->setFocus();
+ ui->espFilterEdit->setSelection(0, INT_MAX);
+ }
+
+ else if (ui->downloadView->hasFocus() || ui->downloadFilterEdit->hasFocus()) {
+ ui->downloadFilterEdit->setFocus();
+ ui->downloadFilterEdit->setSelection(0, INT_MAX);
+ }
+}
+
+void MainWindow::searchClear_activated()
+{
+ if (ui->modList->hasFocus() || ui->modFilterEdit->hasFocus()) {
+ ui->modFilterEdit->clear();
+ ui->modList->setFocus();
+ }
+
+ else if (ui->espList->hasFocus() || ui->espFilterEdit->hasFocus()) {
+ ui->espFilterEdit->clear();
+ ui->espList->setFocus();
+ }
+
+ else if (ui->downloadView->hasFocus() || ui->downloadFilterEdit->hasFocus()) {
+ ui->downloadFilterEdit->clear();
+ ui->downloadView->setFocus();
+ }
+}
+
+void MainWindow::updateModCount()
+{
+ int activeCount = 0;
+ int visActiveCount = 0;
+ int backupCount = 0;
+ int visBackupCount = 0;
+ int foreignCount = 0;
+ int visForeignCount = 0;
+ int separatorCount = 0;
+ int visSeparatorCount = 0;
+ int regularCount = 0;
+ int visRegularCount = 0;
+
+ QStringList allMods = m_OrganizerCore.modList()->allMods();
+
+ auto hasFlag = [](std::vector<ModInfo::EFlag> flags, ModInfo::EFlag filter) {
+ return std::find(flags.begin(), flags.end(), filter) != flags.end();
+ };
+
+ bool isEnabled;
+ bool isVisible;
+ for (QString mod : allMods) {
+ int modIndex = ModInfo::getIndex(mod);
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ std::vector<ModInfo::EFlag> modFlags = modInfo->getFlags();
+ isEnabled = m_OrganizerCore.currentProfile()->modEnabled(modIndex);
+ isVisible = m_ModListSortProxy->filterMatchesMod(modInfo, isEnabled);
+
+ for (auto flag : modFlags) {
+ switch (flag) {
+ case ModInfo::FLAG_BACKUP: backupCount++;
+ if (isVisible)
+ visBackupCount++;
+ break;
+ case ModInfo::FLAG_FOREIGN: foreignCount++;
+ if (isVisible)
+ visForeignCount++;
+ break;
+ case ModInfo::FLAG_SEPARATOR: separatorCount++;
+ if (isVisible)
+ visSeparatorCount++;
+ break;
+ }
+ }
+
+ if (!hasFlag(modFlags, ModInfo::FLAG_BACKUP) &&
+ !hasFlag(modFlags, ModInfo::FLAG_FOREIGN) &&
+ !hasFlag(modFlags, ModInfo::FLAG_SEPARATOR) &&
+ !hasFlag(modFlags, ModInfo::FLAG_OVERWRITE)) {
+ if (isEnabled) {
+ activeCount++;
+ if (isVisible)
+ visActiveCount++;
+ }
+ if (isVisible)
+ visRegularCount++;
+ regularCount++;
+ }
+ }
+
+ ui->activeModsCounter->display(visActiveCount);
+ ui->activeModsCounter->setToolTip(tr("<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>")
+ .arg(activeCount)
+ .arg(regularCount)
+ .arg(visActiveCount)
+ .arg(visRegularCount)
+ .arg(foreignCount)
+ .arg(visForeignCount)
+ .arg(backupCount)
+ .arg(visBackupCount)
+ .arg(separatorCount)
+ .arg(visSeparatorCount)
+ );
+}
+
+void MainWindow::updatePluginCount()
+{
+ int activeMasterCount = 0;
+ int activeLightMasterCount = 0;
+ int activeRegularCount = 0;
+ int masterCount = 0;
+ int lightMasterCount = 0;
+ int regularCount = 0;
+ int activeVisibleCount = 0;
+
+ PluginList *list = m_OrganizerCore.pluginList();
+ QString filter = ui->espFilterEdit->text();
+
+ for (QString plugin : list->pluginNames()) {
+ bool active = list->isEnabled(plugin);
+ bool visible = m_PluginListSortProxy->filterMatchesPlugin(plugin);
+ if (list->isLight(plugin) || list->isLightFlagged(plugin)) {
+ lightMasterCount++;
+ activeLightMasterCount += active;
+ activeVisibleCount += visible && active;
+ } else if (list->isMaster(plugin)) {
+ masterCount++;
+ activeMasterCount += active;
+ activeVisibleCount += visible && active;
+ } else {
+ regularCount++;
+ activeRegularCount += active;
+ activeVisibleCount += visible && active;
+ }
+ }
+
+ int activeCount = activeMasterCount + activeLightMasterCount + activeRegularCount;
+ int totalCount = masterCount + lightMasterCount + regularCount;
+
+ ui->activePluginsCounter->display(activeVisibleCount);
+ ui->activePluginsCounter->setToolTip(tr("<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>")
+ .arg(activeCount).arg(totalCount)
+ .arg(activeMasterCount).arg(masterCount)
+ .arg(activeLightMasterCount).arg(lightMasterCount)
+ .arg(activeRegularCount).arg(regularCount)
+ .arg(activeMasterCount+activeRegularCount).arg(masterCount+regularCount)
+ );
+}
+
+void MainWindow::information_clicked()
+{
+ try {
+ displayModInformation(m_ContextRow);
+ } catch (const std::exception &e) {
+ reportError(e.what());
+ }
+}
+
+void MainWindow::createEmptyMod_clicked()
+{
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+
+ while (name->isEmpty()) {
+ bool ok;
+ name.update(QInputDialog::getText(this, tr("Create Mod..."),
+ tr("This will create an empty mod.\n"
+ "Please enter a name:"), QLineEdit::Normal, "", &ok),
+ GUESS_USER);
+ if (!ok) {
+ return;
+ }
+ }
+
+ if (m_OrganizerCore.getMod(name) != nullptr) {
+ reportError(tr("A mod with this name already exists"));
+ return;
+ }
+
+ int newPriority = -1;
+ if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) {
+ newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow);
+ }
+
+ IModInterface *newMod = m_OrganizerCore.createMod(name);
+ if (newMod == nullptr) {
+ return;
+ }
+
+ m_OrganizerCore.refreshModList();
+
+ if (newPriority >= 0) {
+ m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
+ }
+}
+
+void MainWindow::createSeparator_clicked()
+{
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+ while (name->isEmpty())
+ {
+ bool ok;
+ name.update(QInputDialog::getText(this, tr("Create Separator..."),
+ tr("This will create a new separator.\n"
+ "Please enter a name:"), QLineEdit::Normal, "", &ok),
+ GUESS_USER);
+ if (!ok) { return; }
+ }
+ if (m_OrganizerCore.getMod(name) != nullptr)
+ {
+ reportError(tr("A separator with this name already exists"));
+ return;
+ }
+ name->append("_separator");
+ if (m_OrganizerCore.getMod(name) != nullptr)
+ {
+ return;
+ }
+
+ int newPriority = -1;
+ if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY)
+ {
+ newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow);
+ }
+
+ if (m_OrganizerCore.createMod(name) == nullptr) { return; }
+ m_OrganizerCore.refreshModList();
+
+ if (newPriority >= 0)
+ {
+ m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
+ }
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+ QColor previousColor = settings.value("previousSeparatorColor", QColor()).value<QColor>();
+ if (previousColor.isValid()) {
+ ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(previousColor);
+ }
+
+}
+
+void MainWindow::setColor_clicked()
+{
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ QColorDialog dialog(this);
+ dialog.setOption(QColorDialog::ShowAlphaChannel);
+ QColor currentColor = modInfo->getColor();
+ QColor previousColor = settings.value("previousSeparatorColor", QColor()).value<QColor>();
+ if (currentColor.isValid())
+ dialog.setCurrentColor(currentColor);
+ else
+ dialog.setCurrentColor(previousColor);
+ if (!dialog.exec())
+ return;
+ currentColor = dialog.currentColor();
+ if (!currentColor.isValid())
+ return;
+ settings.setValue("previousSeparatorColor", currentColor);
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
+ auto flags = info->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
+ {
+ info->setColor(currentColor);
+ }
+ }
+ }
+ else {
+ modInfo->setColor(currentColor);
+ }
+}
+
+void MainWindow::resetColor_clicked()
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ QColor color = QColor();
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
+ auto flags = info->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
+ {
+ info->setColor(color);
+ }
+ }
+ }
+ else {
+ modInfo->setColor(color);
+ }
+ Settings::instance().directInterface().remove("previousSeparatorColor");
+}
+
+void MainWindow::createModFromOverwrite()
+{
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+
+ while (name->isEmpty()) {
+ bool ok;
+ name.update(QInputDialog::getText(this, tr("Create Mod..."),
+ tr("This will move all files from overwrite into a new, regular mod.\n"
+ "Please enter a name:"), QLineEdit::Normal, "", &ok),
+ GUESS_USER);
+ if (!ok) {
+ return;
+ }
+ }
+
+ if (m_OrganizerCore.getMod(name) != nullptr) {
+ reportError(tr("A mod with this name already exists"));
+ return;
+ }
+
+ const IModInterface *newMod = m_OrganizerCore.createMod(name);
+ if (newMod == nullptr) {
+ return;
+ }
+
+ doMoveOverwriteContentToMod(newMod->absolutePath());
+}
+
+void MainWindow::moveOverwriteContentToExistingMod()
+{
+ QStringList mods;
+ auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority();
+ for (auto & iter : indexesByPriority) {
+ if ((iter.second != UINT_MAX)) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second);
+ if (!modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN) && !modInfo->hasFlag(ModInfo::FLAG_OVERWRITE)) {
+ mods << modInfo->name();
+ }
+ }
+ }
+
+ ListDialog dialog(this);
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+ QString key = QString("geometry/%1").arg(dialog.objectName());
+
+ dialog.setWindowTitle("Select a mod...");
+ dialog.setChoices(mods);
+
+ if (settings.contains(key)) {
+ dialog.restoreGeometry(settings.value(key).toByteArray());
+ }
+ if (dialog.exec() == QDialog::Accepted) {
+
+ QString result = dialog.getChoice();
+ if (!result.isEmpty()) {
+
+ QString modAbsolutePath;
+
+ for (const auto& mod : m_OrganizerCore.modsSortedByProfilePriority()) {
+ if (result.compare(mod) == 0) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod));
+ modAbsolutePath = modInfo->absolutePath();
+ break;
+ }
+ }
+
+ if (modAbsolutePath.isNull()) {
+ qWarning("Mod %s has not been found, for some reason", qUtf8Printable(result));
+ return;
+ }
+
+ doMoveOverwriteContentToMod(modAbsolutePath);
+ }
+ }
+ settings.setValue(key, dialog.saveGeometry());
+}
+
+void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath)
+{
+ unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
+ std::vector<ModInfo::EFlag> flags = mod->getFlags();
+ return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); });
+
+ ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex);
+ bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"),
+ (QDir::toNativeSeparators(modAbsolutePath)), false, this);
+
+ if (successful) {
+ MessageDialog::showMessage(tr("Move successful."), this);
+ }
+ else {
+ qCritical("Move operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError())));
+ }
+
+ m_OrganizerCore.refreshModList();
+}
+
+void MainWindow::clearOverwrite()
+{
+ unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
+ std::vector<ModInfo::EFlag> flags = mod->getFlags();
+ return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE)
+ != flags.end();
+ });
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex);
+ if (modInfo)
+ {
+ QDir overwriteDir(modInfo->absolutePath());
+ if (QMessageBox::question(this, tr("Are you sure?"),
+ tr("About to recursively delete:\n") + overwriteDir.absolutePath(),
+ QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok)
+ {
+ QStringList delList;
+ for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot))
+ delList.push_back(overwriteDir.absoluteFilePath(f));
+ if (shellDelete(delList, true)) {
+ updateProblemsButton();
+ m_OrganizerCore.refreshModList();
+ } else {
+ qCritical("Delete operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError())));
+ }
+ }
+ }
+}
+
+void MainWindow::cancelModListEditor()
+{
+ ui->modList->setEnabled(false);
+ ui->modList->setEnabled(true);
+}
+
+void MainWindow::on_modList_doubleClicked(const QModelIndex &index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ if (m_OrganizerCore.modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) {
+ // don't interpret double click if we only just checked a mod
+ return;
+ }
+
+ QModelIndex sourceIdx = mapToModel(m_OrganizerCore.modList(), index);
+ if (!sourceIdx.isValid()) {
+ return;
+ }
+
+ Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers();
+ if (modifiers.testFlag(Qt::ControlModifier)) {
+ try {
+ m_ContextRow = m_ModListSortProxy->mapToSource(index).row();
+ openExplorer_clicked();
+ // workaround to cancel the editor that might have opened because of
+ // selection-click
+ ui->modList->closePersistentEditor(index);
+ }
+ catch (const std::exception &e) {
+ reportError(e.what());
+ }
+ }
+ else {
+ try {
+ m_ContextRow = m_ModListSortProxy->mapToSource(index).row();
+ sourceIdx.column();
+ int tab = -1;
+ 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;
+ }
+ displayModInformation(sourceIdx.row(), tab);
+ // workaround to cancel the editor that might have opened because of
+ // selection-click
+ ui->modList->closePersistentEditor(index);
+ }
+ catch (const std::exception &e) {
+ reportError(e.what());
+ }
+ }
+}
+
+void MainWindow::on_listOptionsBtn_pressed()
+{
+ m_ContextRow = -1;
+}
+
+void MainWindow::openOriginInformation_clicked()
+{
+ try {
+ QItemSelectionModel *selection = ui->espList->selectionModel();
+ //we don't want to open multiple modinfodialogs.
+ /*if (selection->hasSelection() && selection->selectedRows().count() > 0) {
+
+ for (QModelIndex idx : selection->selectedRows()) {
+ QString fileName = idx.data().toString();
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+
+ if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
+ displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
+ }
+ }
+ }
+ 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();
+
+ if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
+ displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
+ }
+ }
+ catch (const std::exception &e) {
+ reportError(e.what());
+ }
+}
+
+void MainWindow::on_espList_doubleClicked(const QModelIndex &index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) {
+ // don't interpret double click if we only just checked a plugin
+ return;
+ }
+
+ QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index);
+ if (!sourceIdx.isValid()) {
+ return;
+ }
+ try {
+
+ QItemSelectionModel *selection = ui->espList->selectionModel();
+
+ if (selection->hasSelection() && selection->selectedRows().count() == 1) {
+
+ QModelIndex idx = selection->currentIndex();
+ QString fileName = idx.data().toString();
+
+ if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX)
+ return;
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+
+ if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
+
+ Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers();
+ if (modifiers.testFlag(Qt::ControlModifier)) {
+ openExplorer_activated();
+ // workaround to cancel the editor that might have opened because of
+ // selection-click
+ ui->espList->closePersistentEditor(index);
+ }
+ else {
+
+ displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
+ // workaround to cancel the editor that might have opened because of
+ // selection-click
+ ui->espList->closePersistentEditor(index);
+ }
+ }
+ }
+ }
+ catch (const std::exception &e) {
+ reportError(e.what());
+ }
+}
+
+bool MainWindow::populateMenuCategories(QMenu *menu, int targetID)
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+ const std::set<int> &categories = modInfo->getCategories();
+
+ bool childEnabled = false;
+
+ for (unsigned int i = 1; i < m_CategoryFactory.numCategories(); ++i) {
+ if (m_CategoryFactory.getParentID(i) == targetID) {
+ QMenu *targetMenu = menu;
+ if (m_CategoryFactory.hasChildren(i)) {
+ targetMenu = menu->addMenu(m_CategoryFactory.getCategoryName(i).replace('&', "&&"));
+ }
+
+ int id = m_CategoryFactory.getCategoryID(i);
+ QScopedPointer<QCheckBox> checkBox(new QCheckBox(targetMenu));
+ bool enabled = categories.find(id) != categories.end();
+ checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&"));
+ if (enabled) {
+ childEnabled = true;
+ }
+ checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked);
+
+ QScopedPointer<QWidgetAction> checkableAction(new QWidgetAction(targetMenu));
+ checkableAction->setDefaultWidget(checkBox.take());
+ checkableAction->setData(id);
+ targetMenu->addAction(checkableAction.take());
+
+ if (m_CategoryFactory.hasChildren(i)) {
+ if (populateMenuCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) {
+ targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png"));
+ }
+ }
+ }
+ }
+ return childEnabled;
+}
+
+void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow)
+{
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow);
+ for (QAction* action : menu->actions()) {
+ if (action->menu() != nullptr) {
+ replaceCategoriesFromMenu(action->menu(), modRow);
+ } else {
+ QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action);
+ if (widgetAction != nullptr) {
+ QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget());
+ modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked());
+ }
+ }
+ }
+}
+
+void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow)
+{
+ if (referenceRow != -1 && referenceRow != modRow) {
+ ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow);
+ for (QAction* action : menu->actions()) {
+ if (action->menu() != nullptr) {
+ addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow);
+ } else {
+ QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action);
+ if (widgetAction != nullptr) {
+ QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget());
+ int categoryId = widgetAction->data().toInt();
+ bool checkedBefore = editedModInfo->categorySet(categoryId);
+ bool checkedAfter = checkbox->isChecked();
+
+ if (checkedBefore != checkedAfter) { // only update if the category was changed on the edited mod
+ ModInfo::Ptr currentModInfo = ModInfo::getByIndex(modRow);
+ currentModInfo->setCategory(categoryId, checkedAfter);
+ }
+ }
+ }
+ }
+ } else {
+ replaceCategoriesFromMenu(menu, modRow);
+ }
+}
+
+void MainWindow::addRemoveCategories_MenuHandler() {
+ QMenu *menu = qobject_cast<QMenu*>(sender());
+ if (menu == nullptr) {
+ qCritical("not a menu?");
+ return;
+ }
+
+ QList<QPersistentModelIndex> selected;
+ for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) {
+ selected.append(QPersistentModelIndex(idx));
+ }
+
+ if (selected.size() > 0) {
+ int minRow = INT_MAX;
+ int maxRow = -1;
+
+ for (const QPersistentModelIndex &idx : selected) {
+ qDebug("change categories on: %s", qUtf8Printable(idx.data().toString()));
+ QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx);
+ if (modIdx.row() != m_ContextIdx.row()) {
+ addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row());
+ }
+ if (idx.row() < minRow) minRow = idx.row();
+ if (idx.row() > maxRow) maxRow = idx.row();
+ }
+ replaceCategoriesFromMenu(menu, m_ContextIdx.row());
+
+ m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1);
+
+ for (const QPersistentModelIndex &idx : selected) {
+ ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
+ } else {
+ //For single mod selections, just do a replace
+ replaceCategoriesFromMenu(menu, m_ContextRow);
+ m_OrganizerCore.modList()->notifyChange(m_ContextRow);
+ }
+
+ refreshFilters();
+}
+
+void MainWindow::replaceCategories_MenuHandler() {
+ QMenu *menu = qobject_cast<QMenu*>(sender());
+ if (menu == nullptr) {
+ qCritical("not a menu?");
+ return;
+ }
+
+ QList<QPersistentModelIndex> selected;
+ for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) {
+ selected.append(QPersistentModelIndex(idx));
+ }
+
+ if (selected.size() > 0) {
+ QStringList selectedMods;
+ int minRow = INT_MAX;
+ int maxRow = -1;
+ for (int i = 0; i < selected.size(); ++i) {
+ QModelIndex temp = mapToModel(m_OrganizerCore.modList(), selected.at(i));
+ selectedMods.append(temp.data().toString());
+ replaceCategoriesFromMenu(menu, mapToModel(m_OrganizerCore.modList(), selected.at(i)).row());
+ if (temp.row() < minRow) minRow = temp.row();
+ if (temp.row() > maxRow) maxRow = temp.row();
+ }
+
+ m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1);
+
+ // find mods by their name because indices are invalidated
+ QAbstractItemModel *model = ui->modList->model();
+ for (const QString &mod : selectedMods) {
+ QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1,
+ Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive);
+ if (matches.size() > 0) {
+ ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
+ }
+ } else {
+ //For single mod selections, just do a replace
+ replaceCategoriesFromMenu(menu, m_ContextRow);
+ m_OrganizerCore.modList()->notifyChange(m_ContextRow);
+ }
+
+ refreshFilters();
+}
+
+void MainWindow::saveArchiveList()
+{
+ if (m_OrganizerCore.isArchivesInit()) {
+ SafeWriteFile archiveFile(m_OrganizerCore.currentProfile()->getArchivesFileName());
+ for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) {
+ QTreeWidgetItem * tlItem = ui->bsaList->topLevelItem(i);
+ for (int j = 0; j < tlItem->childCount(); ++j) {
+ QTreeWidgetItem * item = tlItem->child(j);
+ if (item->checkState(0) == Qt::Checked) {
+ archiveFile->write(item->text(0).toUtf8().append("\r\n"));
+ }
+ }
+ }
+ if (archiveFile.commitIfDifferent(m_ArchiveListHash)) {
+ qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(m_OrganizerCore.currentProfile()->getArchivesFileName())));
+ }
+ } else {
+ qWarning("archive list not initialised");
+ }
+}
+
+void MainWindow::checkModsForUpdates()
+{
+ bool checkingModsForUpdate = false;
+ if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) {
+ checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this);
+ NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString());
+ NexusInterface::instance(&m_PluginContainer)->requestTrackingInfo(this, QVariant(), QString());
+ } else {
+ 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.");
+ }
+ }
+
+ bool updatesAvailable = false;
+ for (auto mod : m_OrganizerCore.modList()->allMods()) {
+ ModInfo::Ptr modInfo = ModInfo::getByName(mod);
+ if (modInfo->updateAvailable()) {
+ updatesAvailable = true;
+ break;
+ }
+ }
+
+ if (updatesAvailable || checkingModsForUpdate) {
+ m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE));
+ for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) {
+ if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) {
+ ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i));
+ break;
+ }
+ }
+ }
+}
+
+void MainWindow::changeVersioningScheme() {
+ if (QMessageBox::question(this, tr("Continue?"),
+ tr("The versioning scheme decides which version is considered newer than another.\n"
+ "This function will guess the versioning scheme under the assumption that the installed version is outdated."),
+ QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) {
+
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+
+ bool success = false;
+
+ static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS };
+
+ for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) {
+ VersionInfo verOld(info->getVersion().canonicalString(), schemes[i]);
+ VersionInfo verNew(info->getNewestVersion().canonicalString(), schemes[i]);
+ if (verOld < verNew) {
+ info->setVersion(verOld);
+ info->setNewestVersion(verNew);
+ success = true;
+ }
+ }
+ if (!success) {
+ QMessageBox::information(this, tr("Sorry"),
+ tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->getNewestVersion().canonicalString()).arg(info->getVersion().canonicalString()),
+ QMessageBox::Ok);
+ }
+ }
+}
+
+void MainWindow::ignoreUpdate() {
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
+ info->ignoreUpdate(true);
+ }
+ }
+ else {
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+ info->ignoreUpdate(true);
+ }
+ if (m_ModListSortProxy != nullptr)
+ m_ModListSortProxy->invalidate();
+}
+
+void MainWindow::checkModUpdates_clicked()
+{
+ std::multimap<QString, int> IDs;
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
+ IDs.insert(std::make_pair<QString, int>(info->getGameName(), info->getNexusID()));
+ }
+ } else {
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+ IDs.insert(std::make_pair<QString, int>(info->getGameName(), info->getNexusID()));
+ }
+ modUpdateCheck(IDs);
+}
+
+void MainWindow::unignoreUpdate()
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ for (QModelIndex idx : selection->selectedRows()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
+ info->ignoreUpdate(false);
+ }
+ }
+ else {
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+ info->ignoreUpdate(false);
+ }
+ if (m_ModListSortProxy != nullptr)
+ m_ModListSortProxy->invalidate();
+}
+
+void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu,
+ ModInfo::Ptr info) {
+ const std::set<int> &categories = info->getCategories();
+ for (int categoryID : categories) {
+ int catIdx = m_CategoryFactory.getCategoryIndex(categoryID);
+ QWidgetAction *action = new QWidgetAction(primaryCategoryMenu);
+ try {
+ QRadioButton *categoryBox = new QRadioButton(
+ m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"),
+ primaryCategoryMenu);
+ connect(categoryBox, &QRadioButton::toggled, [info, categoryID](bool enable) {
+ if (enable) {
+ info->setPrimaryCategory(categoryID);
+ }
+ });
+ categoryBox->setChecked(categoryID == info->getPrimaryCategory());
+ action->setDefaultWidget(categoryBox);
+ } catch (const std::exception &e) {
+ qCritical("failed to create category checkbox: %s", e.what());
+ }
+
+ action->setData(categoryID);
+ primaryCategoryMenu->addAction(action);
+ }
+}
+
+void MainWindow::addPrimaryCategoryCandidates()
+{
+ QMenu *menu = qobject_cast<QMenu*>(sender());
+ if (menu == nullptr) {
+ qCritical("not a menu?");
+ return;
+ }
+ menu->clear();
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+
+ addPrimaryCategoryCandidates(menu, modInfo);
+}
+
+void MainWindow::enableVisibleMods()
+{
+ if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all visible mods?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ m_ModListSortProxy->enableAllVisible();
+ }
+}
+
+void MainWindow::disableVisibleMods()
+{
+ if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all visible mods?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ m_ModListSortProxy->disableAllVisible();
+ }
+}
+
+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);
+}
+
+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);
+}
+
+void MainWindow::openInstallFolder()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(qApp->applicationDirPath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+void MainWindow::openPluginsFolder()
+{
+ QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
+ ::ShellExecuteW(nullptr, L"explore", ToWString(pluginsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+
+void MainWindow::openProfileFolder()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+void MainWindow::openIniFolder()
+{
+ if (m_OrganizerCore.currentProfile()->localSettingsEnabled())
+ {
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ }
+ else {
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ }
+}
+
+void MainWindow::openDownloadsFolder()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getDownloadDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+void MainWindow::openModsFolder()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getModDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+void MainWindow::openGameFolder()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+void MainWindow::openMyGamesFolder()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->documentsDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+
+void MainWindow::exportModListCSV()
+{
+ //SelectionDialog selection(tr("Choose what to export"));
+
+ //selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0);
+ //selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1);
+ //selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2);
+
+ QDialog selection(this);
+ QGridLayout *grid = new QGridLayout;
+ selection.setWindowTitle(tr("Export to csv"));
+
+ QLabel *csvDescription = new QLabel();
+ csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead."));
+ grid->addWidget(csvDescription);
+
+ QGroupBox *groupBoxRows = new QGroupBox(tr("Select what mods you want export:"));
+ QRadioButton *all = new QRadioButton(tr("All installed mods"));
+ QRadioButton *active = new QRadioButton(tr("Only active (checked) mods from your current profile"));
+ QRadioButton *visible = new QRadioButton(tr("All currently visible mods in the mod list"));
+
+ QVBoxLayout *vbox = new QVBoxLayout;
+ vbox->addWidget(all);
+ vbox->addWidget(active);
+ vbox->addWidget(visible);
+ vbox->addStretch(1);
+ groupBoxRows->setLayout(vbox);
+
+
+
+ grid->addWidget(groupBoxRows);
+
+ QButtonGroup *buttonGroupRows = new QButtonGroup();
+ buttonGroupRows->addButton(all, 0);
+ buttonGroupRows->addButton(active, 1);
+ buttonGroupRows->addButton(visible, 2);
+ buttonGroupRows->button(0)->setChecked(true);
+
+
+
+ QGroupBox *groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:"));
+ groupBoxColumns->setFlat(true);
+
+ QCheckBox *mod_Priority = new QCheckBox(tr("Mod_Priority"));
+ mod_Priority->setChecked(true);
+ QCheckBox *mod_Name = new QCheckBox(tr("Mod_Name"));
+ mod_Name->setChecked(true);
+ QCheckBox *mod_Note = new QCheckBox(tr("Notes_column"));
+ QCheckBox *mod_Status = new QCheckBox(tr("Mod_Status"));
+ 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"));
+ QCheckBox *mod_Version = new QCheckBox(tr("Mod_Version"));
+ QCheckBox *install_Date = new QCheckBox(tr("Install_Date"));
+ QCheckBox *download_File_Name = new QCheckBox(tr("Download_File_Name"));
+
+ QVBoxLayout *vbox1 = new QVBoxLayout;
+ vbox1->addWidget(mod_Priority);
+ vbox1->addWidget(mod_Name);
+ vbox1->addWidget(mod_Status);
+ vbox1->addWidget(mod_Note);
+ vbox1->addWidget(primary_Category);
+ vbox1->addWidget(nexus_ID);
+ vbox1->addWidget(mod_Nexus_URL);
+ vbox1->addWidget(mod_Version);
+ vbox1->addWidget(install_Date);
+ vbox1->addWidget(download_File_Name);
+ groupBoxColumns->setLayout(vbox1);
+
+ grid->addWidget(groupBoxColumns);
+
+ QPushButton *ok = new QPushButton("Ok");
+ QPushButton *cancel = new QPushButton("Cancel");
+ QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+
+ connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept()));
+ connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject()));
+
+ grid->addWidget(buttons);
+
+ selection.setLayout(grid);
+
+
+ if (selection.exec() == QDialog::Accepted) {
+
+ unsigned int numMods = ModInfo::getNumMods();
+ int selectedRowID = buttonGroupRows->checkedId();
+
+ try {
+ QBuffer buffer;
+ buffer.open(QIODevice::ReadWrite);
+ CSVBuilder builder(&buffer);
+ builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS);
+ 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_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())
+ fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING));
+ if (nexus_ID->isChecked())
+ fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER));
+ if (mod_Nexus_URL->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING));
+ if (mod_Version->isChecked())
+ fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING));
+ if (install_Date->isChecked())
+ fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING));
+ if (download_File_Name->isChecked())
+ fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING));
+
+ builder.setFields(fields);
+
+ builder.writeHeader();
+
+ for (unsigned int i = 0; i < numMods; ++i) {
+ ModInfo::Ptr info = ModInfo::getByIndex(i);
+ bool enabled = m_OrganizerCore.currentProfile()->modEnabled(i);
+ if ((selectedRowID == 1) && !enabled) {
+ continue;
+ }
+ else if ((selectedRowID == 2) && !m_ModListSortProxy->filterMatchesMod(info, enabled)) {
+ continue;
+ }
+ std::vector<ModInfo::EFlag> flags = info->getFlags();
+ 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')));
+ 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())
+ builder.setRowField("#Primary_Category", (m_CategoryFactory.categoryExists(info->getPrimaryCategory())) ? m_CategoryFactory.getCategoryName(info->getPrimaryCategory()) : "");
+ if (nexus_ID->isChecked())
+ builder.setRowField("#Nexus_ID", info->getNexusID());
+ if (mod_Nexus_URL->isChecked())
+ builder.setRowField("#Mod_Nexus_URL",(info->getNexusID()>0)? NexusInterface::instance(&m_PluginContainer)->getModURL(info->getNexusID(), info->getGameName()) : "");
+ if (mod_Version->isChecked())
+ builder.setRowField("#Mod_Version", info->getVersion().canonicalString());
+ if (install_Date->isChecked())
+ builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss"));
+ if (download_File_Name->isChecked())
+ builder.setRowField("#Download_File_Name", info->getInstallationFile());
+
+ builder.writeRow();
+ }
+ }
+
+ SaveTextAsDialog saveDialog(this);
+ saveDialog.setText(buffer.data());
+ saveDialog.exec();
+ }
+ catch (const std::exception &e) {
+ reportError(tr("export failed: %1").arg(e.what()));
+ }
+ }
+}
+
+static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu)
+{
+ QPushButton *pushBtn = new QPushButton(subMenu->title());
+ pushBtn->setMenu(subMenu);
+ QWidgetAction *action = new QWidgetAction(menu);
+ action->setDefaultWidget(pushBtn);
+ menu->addAction(action);
+}
+
+QMenu *MainWindow::openFolderMenu()
+{
+
+ QMenu *FolderMenu = new QMenu(this);
+
+ FolderMenu->addAction(tr("Open Game folder"), this, SLOT(openGameFolder()));
+
+ FolderMenu->addAction(tr("Open MyGames folder"), this, SLOT(openMyGamesFolder()));
+
+ FolderMenu->addAction(tr("Open INIs folder"), this, SLOT(openIniFolder()));
+
+ FolderMenu->addSeparator();
+
+ FolderMenu->addAction(tr("Open Instance folder"), this, SLOT(openInstanceFolder()));
+
+ FolderMenu->addAction(tr("Open Mods folder"), this, SLOT(openModsFolder()));
+
+ FolderMenu->addAction(tr("Open Profile folder"), this, SLOT(openProfileFolder()));
+
+ FolderMenu->addAction(tr("Open Downloads folder"), this, SLOT(openDownloadsFolder()));
+
+ FolderMenu->addSeparator();
+
+ FolderMenu->addAction(tr("Open MO2 Install folder"), this, SLOT(openInstallFolder()));
+
+ FolderMenu->addAction(tr("Open MO2 Plugins folder"), this, SLOT(openPluginsFolder()));
+
+ FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder()));
+
+
+ return FolderMenu;
+}
+
+void MainWindow::initModListContextMenu(QMenu *menu)
+{
+ menu->addAction(tr("Install Mod..."), this, SLOT(installMod_clicked()));
+ menu->addAction(tr("Create empty mod"), this, SLOT(createEmptyMod_clicked()));
+ menu->addAction(tr("Create Separator"), this, SLOT(createSeparator_clicked()));
+
+ menu->addSeparator();
+
+ menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods()));
+ menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods()));
+ menu->addAction(tr("Check for updates"), this, SLOT(checkModsForUpdates()));
+ menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh()));
+ menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV()));
+}
+
+void MainWindow::addModSendToContextMenu(QMenu *menu)
+{
+ if (m_ModListSortProxy->sortColumn() != ModList::COL_PRIORITY)
+ return;
+
+ QMenu *sub_menu = new QMenu(menu);
+ sub_menu->setTitle(tr("Send to"));
+ sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedModsToTop_clicked()));
+ sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedModsToBottom_clicked()));
+ sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedModsToPriority_clicked()));
+ sub_menu->addAction(tr("Separator..."), this, SLOT(sendSelectedModsToSeparator_clicked()));
+
+ menu->addMenu(sub_menu);
+ menu->addSeparator();
+}
+
+void MainWindow::addPluginSendToContextMenu(QMenu *menu)
+{
+ if (m_PluginListSortProxy->sortColumn() != PluginList::COL_PRIORITY)
+ return;
+
+ QMenu *sub_menu = new QMenu(this);
+ sub_menu->setTitle(tr("Send to"));
+ sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedPluginsToTop_clicked()));
+ sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedPluginsToBottom_clicked()));
+ sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedPluginsToPriority_clicked()));
+
+ menu->addMenu(sub_menu);
+ menu->addSeparator();
+}
+
+void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos)
+{
+ try {
+ QTreeView *modList = findChild<QTreeView*>("modList");
+
+ m_ContextIdx = mapToModel(m_OrganizerCore.modList(), modList->indexAt(pos));
+ m_ContextRow = m_ContextIdx.row();
+
+ if (m_ContextRow == -1) {
+ // no selection
+ QMenu menu(this);
+ initModListContextMenu(&menu);
+ menu.exec(modList->mapToGlobal(pos));
+ } else {
+ QMenu menu(this);
+
+ QMenu *allMods = new QMenu(&menu);
+ initModListContextMenu(allMods);
+ allMods->setTitle(tr("All Mods"));
+ menu.addMenu(allMods);
+ menu.addSeparator();
+
+ ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
+ std::vector<ModInfo::EFlag> flags = info->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) {
+ if (QDir(info->absolutePath()).count() > 2) {
+ menu.addAction(tr("Sync to Mods..."), &m_OrganizerCore, SLOT(syncOverwrite()));
+ menu.addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite()));
+ menu.addAction(tr("Move content to Mod..."), this, SLOT(moveOverwriteContentToExistingMod()));
+ menu.addAction(tr("Clear Overwrite..."), this, SLOT(clearOverwrite()));
+ }
+ menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked()));
+ } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) {
+ menu.addAction(tr("Restore Backup"), this, SLOT(restoreBackup_clicked()));
+ menu.addAction(tr("Remove Backup..."), this, SLOT(removeMod_clicked()));
+ } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()){
+ menu.addSeparator();
+ QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu);
+ populateMenuCategories(addRemoveCategoriesMenu, 0);
+ connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler()));
+ addMenuAsPushButton(&menu, addRemoveCategoriesMenu);
+ QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu);
+ connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates()));
+ addMenuAsPushButton(&menu, primaryCategoryMenu);
+ menu.addSeparator();
+ menu.addAction(tr("Rename Separator..."), this, SLOT(renameMod_clicked()));
+ menu.addAction(tr("Remove Separator..."), this, SLOT(removeMod_clicked()));
+ menu.addSeparator();
+ addModSendToContextMenu(&menu);
+ menu.addAction(tr("Select Color..."), this, SLOT(setColor_clicked()));
+ if(info->getColor().isValid())
+ menu.addAction(tr("Reset Color"), this, SLOT(resetColor_clicked()));
+ menu.addSeparator();
+ } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) {
+ addModSendToContextMenu(&menu);
+ } else {
+ QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu);
+ populateMenuCategories(addRemoveCategoriesMenu, 0);
+ connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler()));
+ addMenuAsPushButton(&menu, addRemoveCategoriesMenu);
+
+ QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu);
+ connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates()));
+ addMenuAsPushButton(&menu, primaryCategoryMenu);
+
+ menu.addSeparator();
+
+ if (info->downgradeAvailable()) {
+ menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme()));
+ }
+
+ if (info->getNexusID() > 0)
+ menu.addAction(tr("Force-check updates"), this, SLOT(checkModUpdates_clicked()));
+ if (info->updateIgnored()) {
+ menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate()));
+ } else {
+ if (info->updateAvailable() || info->downgradeAvailable()) {
+ menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate()));
+ }
+ }
+ menu.addSeparator();
+
+ menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedMods_clicked()));
+ menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedMods_clicked()));
+
+ menu.addSeparator();
+
+ addModSendToContextMenu(&menu);
+
+ menu.addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked()));
+ menu.addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked()));
+ menu.addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked()));
+ menu.addAction(tr("Create Backup"), this, SLOT(backupMod_clicked()));
+
+ menu.addSeparator();
+
+ if (info->getNexusID() > 0 && Settings::instance().endorsementIntegration()) {
+ switch (info->endorsedState()) {
+ case ModInfo::ENDORSED_TRUE: {
+ menu.addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked()));
+ } break;
+ case ModInfo::ENDORSED_FALSE: {
+ menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked()));
+ menu.addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked()));
+ } break;
+ case ModInfo::ENDORSED_NEVER: {
+ menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked()));
+ } break;
+ default: {
+ QAction *action = new QAction(tr("Endorsement state unknown"), &menu);
+ action->setEnabled(false);
+ menu.addAction(action);
+ } break;
+ }
+ }
+
+ if (info->getNexusID() > 0) {
+ switch (info->trackedState()) {
+ case ModInfo::TRACKED_FALSE: {
+ menu.addAction(tr("Start tracking"), this, SLOT(track_clicked()));
+ } break;
+ case ModInfo::TRACKED_TRUE: {
+ menu.addAction(tr("Stop tracking"), this, SLOT(untrack_clicked()));
+ } break;
+ default: {
+ QAction *action = new QAction(tr("Tracked state unknown"), &menu);
+ action->setEnabled(false);
+ menu.addAction(action);
+ } break;
+ }
+ }
+
+ menu.addSeparator();
+
+ std::vector<ModInfo::EFlag> flags = info->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) {
+ menu.addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked()));
+ }
+
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) {
+ menu.addAction(tr("Mark as converted/working"), this, SLOT(markConverted_clicked()));
+ }
+
+ 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()));
+ }
+
+ menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked()));
+ }
+
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) {
+ QAction *infoAction = menu.addAction(tr("Information..."), this, SLOT(information_clicked()));
+ menu.setDefaultAction(infoAction);
+ }
+
+ menu.exec(modList->mapToGlobal(pos));
+ }
+ } catch (const std::exception &e) {
+ reportError(tr("Exception: ").arg(e.what()));
+ } catch (...) {
+ reportError(tr("Unknown exception"));
+ }
+}
+
+
+void MainWindow::on_categoriesList_itemSelectionChanged()
+{
+ QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows();
+ std::vector<int> categories;
+ std::vector<int> content;
+ for (const QModelIndex &index : indices) {
+ int filterType = index.data(Qt::UserRole + 1).toInt();
+ if ((filterType == ModListSortProxy::TYPE_CATEGORY)
+ || (filterType == ModListSortProxy::TYPE_SPECIAL)) {
+ int categoryId = index.data(Qt::UserRole).toInt();
+ if (categoryId != CategoryFactory::CATEGORY_NONE) {
+ categories.push_back(categoryId);
+ }
+ } else if (filterType == ModListSortProxy::TYPE_CONTENT) {
+ int contentId = index.data(Qt::UserRole).toInt();
+ content.push_back(contentId);
+ }
+ }
+
+ m_ModListSortProxy->setCategoryFilter(categories);
+ m_ModListSortProxy->setContentFilter(content);
+ ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0);
+
+ if (indices.count() == 0) {
+ ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<All>")));
+ } else if (indices.count() > 1) {
+ ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<Multiple>")));
+ } else {
+ ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString()));
+ }
+ ui->modList->reset();
+}
+
+
+void MainWindow::deleteSavegame_clicked()
+{
+ SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
+
+ QString savesMsgLabel;
+ QStringList deleteFiles;
+
+ int count = 0;
+
+ for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) {
+ QString name = idx.data(Qt::UserRole).toString();
+
+ if (count < 10) {
+ savesMsgLabel += "<li>" + QFileInfo(name).completeBaseName() + "</li>";
+ }
+ ++count;
+
+ if (info == nullptr) {
+ deleteFiles.push_back(name);
+ } else {
+ ISaveGame const *save = info->getSaveGameInfo(name);
+ deleteFiles += save->allFiles();
+ delete save;
+ }
+ }
+
+ if (count > 10) {
+ savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
+ }
+
+ if (QMessageBox::question(this, tr("Confirm"),
+ tr("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.", "", count)
+ .arg(savesMsgLabel),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ shellDelete(deleteFiles, true); // recycle bin delete.
+ refreshSaveList();
+ }
+}
+
+
+void MainWindow::fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets)
+{
+ ActivateModsDialog dialog(missingAssets, this);
+ if (dialog.exec() == QDialog::Accepted) {
+ // activate the required mods, then enable all esps
+ std::set<QString> modsToActivate = dialog.getModsToActivate();
+ for (std::set<QString>::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) {
+ if ((*iter != "<data>") && (*iter != "<overwrite>")) {
+ unsigned int modIndex = ModInfo::getIndex(*iter);
+ m_OrganizerCore.currentProfile()->setModEnabled(modIndex, true);
+ }
+ }
+
+ m_OrganizerCore.currentProfile()->writeModlist();
+ m_OrganizerCore.refreshLists();
+
+ std::set<QString> espsToActivate = dialog.getESPsToActivate();
+ for (std::set<QString>::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) {
+ m_OrganizerCore.pluginList()->enableESP(*iter);
+ }
+ m_OrganizerCore.saveCurrentLists();
+ }
+}
+
+
+void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos)
+{
+ QItemSelectionModel *selection = ui->savegameList->selectionModel();
+
+ if (!selection->hasSelection()) {
+ return;
+ }
+
+ QMenu menu;
+ QAction *action = menu.addAction(tr("Enable Mods..."));
+ action->setEnabled(false);
+
+ if (selection->selectedIndexes().count() == 1) {
+ SaveGameInfo const *info = this->m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
+ if (info != nullptr) {
+ QString save = ui->savegameList->currentItem()->data(Qt::UserRole).toString();
+ SaveGameInfo::MissingAssets missing = info->getMissingAssets(save);
+ if (missing.size() != 0) {
+ connect(action, &QAction::triggered, this, [this, missing]{ fixMods_clicked(missing); });
+ action->setEnabled(true);
+ }
+ }
+ }
+
+ QString deleteMenuLabel = tr("Delete %n save(s)", "", selection->selectedIndexes().count());
+
+ menu.addAction(deleteMenuLabel, this, SLOT(deleteSavegame_clicked()));
+
+ menu.exec(ui->savegameList->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();
+}
+
+namespace {
+QString getLinkfile(const QString &dir, const Executable &exec)
+{
+ return QDir::fromNativeSeparators(dir) + "/" + exec.m_Title + ".lnk";
+}
+
+QString getDesktopLinkfile(const Executable &exec)
+{
+ return getLinkfile(getDesktopDirectory(), exec);
+}
+
+QString getStartMenuLinkfile(const Executable &exec)
+{
+ return getLinkfile(getStartMenuDirectory(), exec);
+}
+}
+
+void MainWindow::addWindowsLink(const ShortcutType mapping)
+{
+ const Executable &selectedExecutable(getSelectedExecutable());
+ QString const linkName = getLinkfile(mapping == ShortcutType::Desktop ? getDesktopDirectory() : getStartMenuDirectory(),
+ selectedExecutable);
+
+ 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());
+
+ 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()));
+
+ 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));
+ }
+ }
+}
+
+void MainWindow::linkDesktop()
+{
+ addWindowsLink(ShortcutType::Desktop);
+}
+
+void MainWindow::linkMenu()
+{
+ addWindowsLink(ShortcutType::StartMenu);
+}
+
+void MainWindow::on_actionSettings_triggered()
+{
+ Settings &settings = m_OrganizerCore.settings();
+
+ QString oldModDirectory(settings.getModDirectory());
+ QString oldCacheDirectory(settings.getCacheDirectory());
+ QString oldProfilesDirectory(settings.getProfileDirectory());
+ QString oldManagedGameDirectory(settings.getManagedGameDirectory());
+ bool oldDisplayForeign(settings.displayForeign());
+ bool proxy = settings.useProxy();
+ DownloadManager *dlManager = m_OrganizerCore.downloadManager();
+
+ settings.query(&m_PluginContainer, this);
+
+ if (oldManagedGameDirectory != settings.getManagedGameDirectory()) {
+ QMessageBox::about(this, tr("Restarting MO"),
+ tr("Changing the managed game directory requires restarting MO.\n"
+ "Any pending downloads will be paused.\n\n"
+ "Click OK to restart MO now."));
+ dlManager->pauseAll();
+ qApp->exit(INT_MAX);
+ }
+
+ InstallationManager *instManager = m_OrganizerCore.installationManager();
+ instManager->setModsDirectory(settings.getModDirectory());
+ instManager->setDownloadDirectory(settings.getDownloadDirectory());
+
+ fixCategories();
+ refreshFilters();
+
+ if (settings.getProfileDirectory() != oldProfilesDirectory) {
+ refreshProfiles();
+ }
+
+ if (dlManager->getOutputDirectory() != settings.getDownloadDirectory()) {
+ if (dlManager->downloadsInProgress()) {
+ MessageDialog::showMessage(tr("Can't change download directory while "
+ "downloads are in progress!"),
+ this);
+ } else {
+ dlManager->setOutputDirectory(settings.getDownloadDirectory());
+ }
+ }
+ dlManager->setPreferredServers(settings.getPreferredServers());
+
+ if ((settings.getModDirectory() != oldModDirectory)
+ || (settings.displayForeign() != oldDisplayForeign)) {
+ m_OrganizerCore.profileRefresh();
+ }
+
+ const auto state = settings.archiveParsing();
+ if (state != m_OrganizerCore.getArchiveParsing())
+ {
+ m_OrganizerCore.setArchiveParsing(state);
+ if (!state)
+ {
+ ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked);
+ ui->showArchiveDataCheckBox->setEnabled(false);
+ m_showArchiveData = false;
+ }
+ else
+ {
+ ui->showArchiveDataCheckBox->setCheckState(Qt::Checked);
+ ui->showArchiveDataCheckBox->setEnabled(true);
+ m_showArchiveData = true;
+ }
+ m_OrganizerCore.refreshModList();
+ m_OrganizerCore.refreshDirectoryStructure();
+ m_OrganizerCore.refreshLists();
+ }
+
+ if (settings.getCacheDirectory() != oldCacheDirectory) {
+ NexusInterface::instance(&m_PluginContainer)->setCacheDirectory(settings.getCacheDirectory());
+ }
+
+ if (proxy != settings.useProxy()) {
+ activateProxy(settings.useProxy());
+ }
+
+ ui->apiRequests->setVisible(!settings.hideAPICounter());
+
+ updateDownloadView();
+
+ m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist());
+ m_OrganizerCore.cycleDiagnostics();
+}
+
+
+void MainWindow::on_actionNexus_triggered()
+{
+ const IPluginGame *game = m_OrganizerCore.managedGame();
+ QString gameName = game->gameShortName();
+ if (game->gameNexusName().isEmpty() && game->primarySources().count())
+ gameName = game->primarySources()[0];
+ QDesktopServices::openUrl(QUrl(NexusInterface::instance(&m_PluginContainer)->getGameURL(gameName)));
+}
+
+
+void MainWindow::linkClicked(const QString &url)
+{
+ QDesktopServices::openUrl(QUrl(url));
+}
+
+
+void MainWindow::installTranslator(const QString &name)
+{
+ QTranslator *translator = new QTranslator(this);
+ QString fileName = name + "_" + m_CurrentLanguage;
+ if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) {
+ if (m_CurrentLanguage.contains(QRegularExpression("^.*_(EN|en)(-.*)?$"))) {
+ qDebug("localization file %s not found", qUtf8Printable(fileName));
+ } // we don't actually expect localization files for English (en, en-us, en-uk, and any variation thereof)
+ }
+
+ qApp->installTranslator(translator);
+ m_Translators.push_back(translator);
+}
+
+
+void MainWindow::languageChange(const QString &newLanguage)
+{
+ for (QTranslator *trans : m_Translators) {
+ qApp->removeTranslator(trans);
+ }
+ m_Translators.clear();
+
+ m_CurrentLanguage = newLanguage;
+
+ installTranslator("qt");
+ installTranslator("qtbase");
+ installTranslator(ToQString(AppConfig::translationPrefix()));
+ for (const QString &fileName : m_PluginContainer.pluginFileNames()) {
+ installTranslator(QFileInfo(fileName).baseName());
+ }
+ ui->retranslateUi(this);
+ qDebug("loaded language %s", qUtf8Printable(newLanguage));
+
+ ui->profileBox->setItemText(0, QObject::tr("<Manage...>"));
+
+ createHelpWidget();
+
+ updateDownloadView();
+ updateProblemsButton();
+
+ QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn);
+ initModListContextMenu(listOptionsMenu);
+ ui->listOptionsBtn->setMenu(listOptionsMenu);
+
+ ui->openFolderMenu->setMenu(openFolderMenu());
+}
+
+void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry)
+{
+ for (FileEntry::Ptr current : directoryEntry.getFiles()) {
+ bool isArchive = false;
+ int origin = current->getOrigin(isArchive);
+ if (isArchive) {
+ // TODO: don't list files from archives. maybe make this an option?
+ continue;
+ }
+ QString fullName = directory + "\\" + ToQString(current->getName());
+ file.write(fullName.toUtf8());
+
+ file.write("\t(");
+ file.write(ToQString(m_OrganizerCore.directoryStructure()->getOriginByID(origin).getName()).toUtf8());
+ file.write(")\r\n");
+ }
+
+ // recurse into subdirectories
+ std::vector<DirectoryEntry*>::const_iterator current, end;
+ directoryEntry.getSubDirectories(current, end);
+ for (; current != end; ++current) {
+ writeDataToFile(file, directory + "\\" + ToQString((*current)->getName()), **current);
+ }
+}
+
+void MainWindow::writeDataToFile()
+{
+ QString fileName = QFileDialog::getSaveFileName(this);
+ if (!fileName.isEmpty()) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::WriteOnly)) {
+ reportError(tr("failed to write to file %1").arg(fileName));
+ }
+
+ writeDataToFile(file, "data", *m_OrganizerCore.directoryStructure());
+ file.close();
+
+ MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this);
+ }
+}
+
+
+int MainWindow::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 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: {
+ 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);
+ refreshExecutablesList();
+ }
+ } break;
+ case 2: {
+ QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable."));
+ } break;
+ default: {
+ // nop
+ } break;
+ }
+ }
+}
+
+
+void MainWindow::originModified(int originID)
+{
+ FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
+ origin.enable(false);
+ m_OrganizerCore.directoryStructure()->addFromOrigin(origin.getName(), origin.getPath(), origin.getPriority());
+ DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure());
+}
+
+
+void MainWindow::hideFile()
+{
+ QString oldName = m_ContextItem->data(0, Qt::UserRole).toString();
+ QString newName = oldName + ModInfo::s_HiddenExt;
+
+ 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;
+ }
+ } else {
+ return;
+ }
+ }
+
+ if (QFile::rename(oldName, newName)) {
+ originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt());
+ refreshDataTreeKeepExpandedNodes();
+ } else {
+ reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName)));
+ }
+}
+
+
+void MainWindow::unhideFile()
+{
+ QString oldName = m_ContextItem->data(0, Qt::UserRole).toString();
+ 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;
+ }
+ } else {
+ return;
+ }
+ }
+ if (QFile::rename(oldName, newName)) {
+ originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt());
+ refreshDataTreeKeepExpandedNodes();
+ } else {
+ reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName)));
+ }
+}
+
+
+void MainWindow::enableSelectedPlugins_clicked()
+{
+ m_OrganizerCore.pluginList()->enableSelected(ui->espList->selectionModel());
+}
+
+
+void MainWindow::disableSelectedPlugins_clicked()
+{
+ m_OrganizerCore.pluginList()->disableSelected(ui->espList->selectionModel());
+}
+
+void MainWindow::sendSelectedPluginsToTop_clicked()
+{
+ m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), 0);
+}
+
+void MainWindow::sendSelectedPluginsToBottom_clicked()
+{
+ m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), INT_MAX);
+}
+
+void MainWindow::sendSelectedPluginsToPriority_clicked()
+{
+ bool ok;
+ int newPriority = QInputDialog::getInt(this,
+ tr("Set Priority"), tr("Set the priority of the selected plugins"),
+ 0, 0, INT_MAX, 1, &ok);
+ if (!ok) return;
+
+ m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), newPriority);
+}
+
+
+void MainWindow::enableSelectedMods_clicked()
+{
+ m_OrganizerCore.modList()->enableSelected(ui->modList->selectionModel());
+ if (m_ModListSortProxy != nullptr) {
+ m_ModListSortProxy->invalidate();
+ }
+}
+
+
+void MainWindow::disableSelectedMods_clicked()
+{
+ m_OrganizerCore.modList()->disableSelected(ui->modList->selectionModel());
+ if (m_ModListSortProxy != nullptr) {
+ m_ModListSortProxy->invalidate();
+ }
+}
+
+
+void MainWindow::previewDataFile()
+{
+ QString fileName = QDir::fromNativeSeparators(m_ContextItem->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
+
+ // 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);
+ }
+
+
+
+ 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) {
+ 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."));
+ }
+}
+
+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;
+ }
+ }
+}
+
+
+void MainWindow::updateAvailable()
+{
+ for (QAction *action : ui->toolBar->actions()) {
+ if (action->text() == tr("Update")) {
+ action->setEnabled(true);
+ action->setToolTip(tr("Update available"));
+ break;
+ }
+ }
+}
+
+
+void MainWindow::motdReceived(const QString &motd)
+{
+ // don't show motd after 5 seconds, may be annoying. Hopefully the user's
+ // internet connection is faster next time
+ if (m_StartTime.secsTo(QTime::currentTime()) < 5) {
+ uint hash = qHash(motd);
+ if (hash != m_OrganizerCore.settings().getMotDHash()) {
+ MotDDialog dialog(motd);
+ dialog.exec();
+ m_OrganizerCore.settings().setMotDHash(hash);
+ }
+ }
+}
+
+void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
+{
+ QTreeWidget *dataTree = findChild<QTreeWidget*>("dataTree");
+ m_ContextItem = dataTree->itemAt(pos.x(), pos.y());
+
+ QMenu menu;
+ if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0)
+ && (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) {
+ menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile()));
+ menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable()));
+
+ QString fileName = m_ContextItem->text(0);
+ if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) {
+ menu.addAction(tr("Preview"), this, SLOT(previewDataFile()));
+ }
+
+ // offer to hide/unhide file, but not for files from archives
+ if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) {
+ if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) {
+ menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile()));
+ } else {
+ menu.addAction(tr("Hide"), this, SLOT(hideFile()));
+ }
+ }
+
+ menu.addSeparator();
+ }
+ menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile()));
+ menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked()));
+
+ menu.exec(dataTree->mapToGlobal(pos));
+}
+
+void MainWindow::on_conflictsCheckBox_toggled(bool)
+{
+ refreshDataTreeKeepExpandedNodes();
+}
+
+
+void MainWindow::on_actionUpdate_triggered()
+{
+ m_OrganizerCore.startMOUpdate();
+}
+
+
+void MainWindow::actionEndorseMO()
+{
+ // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now
+ IPluginGame * game = m_OrganizerCore.getGame("skyrimse");
+ if (!game) return;
+
+ if (QMessageBox::question(this, tr("Endorse Mod Organizer"),
+ tr("Do you want to endorse Mod Organizer on %1 now?").arg(
+ NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement(
+ game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), true, this, QVariant(), QString());
+ }
+}
+
+void MainWindow::actionWontEndorseMO()
+{
+ // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now
+ IPluginGame * game = m_OrganizerCore.getGame("skyrimse");
+ if (!game) return;
+
+ if (QMessageBox::question(this, tr("Abstain from Endorsing Mod Organizer"),
+ tr("Are you sure you want to abstain from endorsing Mod Organizer 2?\n"
+ "You will have to visit the mod page on the %1 Nexus site to change your mind.").arg(
+ NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement(
+ game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), false, this, QVariant(), QString());
+ }
+}
+
+void MainWindow::initDownloadView()
+{
+ DownloadList *sourceModel = new DownloadList(m_OrganizerCore.downloadManager(), ui->downloadView);
+ DownloadListSortProxy *sortProxy = new DownloadListSortProxy(m_OrganizerCore.downloadManager(), ui->downloadView);
+ sortProxy->setSourceModel(sourceModel);
+ connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString)));
+ connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(downloadFilterChanged(QString)));
+
+ ui->downloadView->setSourceModel(sourceModel);
+ ui->downloadView->setModel(sortProxy);
+ ui->downloadView->setManager(m_OrganizerCore.downloadManager());
+ ui->downloadView->setItemDelegate(new DownloadProgressDelegate(m_OrganizerCore.downloadManager(), sortProxy, ui->downloadView));
+ updateDownloadView();
+
+ connect(ui->downloadView, SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int)));
+ connect(ui->downloadView, SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int)));
+ connect(ui->downloadView, SIGNAL(queryInfoMd5(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfoMd5(int)));
+ connect(ui->downloadView, SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int)));
+ connect(ui->downloadView, SIGNAL(openFile(int)), m_OrganizerCore.downloadManager(), SLOT(openFile(int)));
+ connect(ui->downloadView, SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int)));
+ connect(ui->downloadView, SIGNAL(removeDownload(int, bool)), m_OrganizerCore.downloadManager(), SLOT(removeDownload(int, bool)));
+ connect(ui->downloadView, SIGNAL(restoreDownload(int)), m_OrganizerCore.downloadManager(), SLOT(restoreDownload(int)));
+ connect(ui->downloadView, SIGNAL(cancelDownload(int)), m_OrganizerCore.downloadManager(), SLOT(cancelDownload(int)));
+ connect(ui->downloadView, SIGNAL(pauseDownload(int)), m_OrganizerCore.downloadManager(), SLOT(pauseDownload(int)));
+ connect(ui->downloadView, SIGNAL(resumeDownload(int)), this, SLOT(resumeDownload(int)));
+}
+
+void MainWindow::updateDownloadView()
+{
+ // set the view attribute and default row sizes
+ if (m_OrganizerCore.settings().compactDownloads()) {
+ ui->downloadView->setProperty("downloadView", "compact");
+ setStyleSheet("DownloadListWidget::item { padding: 4px 2px; }");
+ } else {
+ ui->downloadView->setProperty("downloadView", "standard");
+ setStyleSheet("DownloadListWidget::item { padding: 16px 4px; }");
+ }
+ //setStyleSheet("DownloadListWidget::item:hover { padding: 0px; }");
+ //setStyleSheet("DownloadListWidget::item:selected { padding: 0px; }");
+
+ // reapply global stylesheet on the widget level (!) to override the defaults
+ //ui->downloadView->setStyleSheet(styleSheet());
+
+ ui->downloadView->setMetaDisplay(m_OrganizerCore.settings().metaDownloads());
+ ui->downloadView->style()->unpolish(ui->downloadView);
+ ui->downloadView->style()->polish(ui->downloadView);
+ qobject_cast<DownloadListHeader*>(ui->downloadView->header())->customResizeSections();
+ m_OrganizerCore.downloadManager()->refreshList();
+}
+
+void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs)
+{
+ if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) {
+ ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs);
+ } else {
+ QString apiKey;
+ if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) {
+ m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); });
+ NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey);
+ } else
+ qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.");
+ }
+}
+
+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);
+ 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! :)"));
+ } 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!"));
+ }
+ } else {
+ ui->actionEndorseMO->setEnabled(true);
+ }
+ } else
+ ui->actionEndorseMO->setVisible(false);
+}
+
+void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int)
+{
+ QVariantList data = resultData.toList();
+ std::multimap<QString, std::pair<int, QString>> sorted;
+ QStringList games = m_OrganizerCore.managedGame()->validShortNames();
+ games += m_OrganizerCore.managedGame()->gameShortName();
+ bool searchedMO2NexusGame = false;
+ for (auto endorsementData : data) {
+ QVariantMap endorsement = endorsementData.toMap();
+ std::pair<int, QString> data = std::make_pair<int, QString>(endorsement["mod_id"].toInt(), endorsement["status"].toString());
+ sorted.insert(std::pair<QString, std::pair<int, QString>>(endorsement["domain_name"].toString(), data));
+ }
+ for (auto game : games) {
+ IPluginGame *gamePlugin = m_OrganizerCore.getGame(game);
+ if (gamePlugin != nullptr && gamePlugin->gameShortName().compare("SkyrimSE", Qt::CaseInsensitive) == 0)
+ searchedMO2NexusGame = true;
+ auto iter = sorted.equal_range(gamePlugin->gameNexusName());
+ for (auto result = iter.first; result != iter.second; ++result) {
+ std::vector<ModInfo::Ptr> modsList = ModInfo::getByModID(result->first, result->second.first);
+
+ for (auto mod : modsList) {
+ if (result->second.second == "Endorsed")
+ mod->setIsEndorsed(true);
+ else if (result->second.second == "Abstained")
+ mod->setNeverEndorse();
+ else
+ mod->setIsEndorsed(false);
+ }
+
+ if (Settings::instance().endorsementIntegration()) {
+ if (result->first == "skyrimspecialedition" && result->second.first == gamePlugin->nexusModOrganizerID()) {
+ Settings::instance().directInterface().setValue("endorse_state", result->second.second);
+ toggleMO2EndorseState();
+ }
+ }
+ }
+ }
+
+ if (!searchedMO2NexusGame && Settings::instance().endorsementIntegration()) {
+ auto gamePlugin = m_OrganizerCore.getGame("SkyrimSE");
+ if (gamePlugin) {
+ auto iter = sorted.equal_range(gamePlugin->gameNexusName());
+ for (auto result = iter.first; result != iter.second; ++result) {
+ if (result->second.first == gamePlugin->nexusModOrganizerID()) {
+ Settings::instance().directInterface().setValue("endorse_state", result->second.second);
+ toggleMO2EndorseState();
+ break;
+ }
+ }
+ }
+ }
+}
+
+void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int)
+{
+ QString gameNameReal;
+ for (IPluginGame *game : m_PluginContainer.plugins<IPluginGame>()) {
+ if (game->gameNexusName() == gameName) {
+ gameNameReal = game->gameShortName();
+ break;
+ }
+ }
+ QVariantList resultList = resultData.toList();
+
+ QFutureWatcher<std::set<QSharedPointer<ModInfo>>> *watcher = new QFutureWatcher<std::set<QSharedPointer<ModInfo>>>();
+ QObject::connect(watcher, &QFutureWatcher<std::set<QSharedPointer<ModInfo>>>::finished, this, &MainWindow::finishUpdateInfo);
+ QFuture<std::set<QSharedPointer<ModInfo>>> future = QtConcurrent::run([=]() -> std::set<QSharedPointer<ModInfo>> {
+ return ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true);
+ });
+ watcher->setFuture(future);
+ if (m_ModListSortProxy != nullptr)
+ m_ModListSortProxy->invalidate();
+}
+
+void MainWindow::finishUpdateInfo()
+{
+ QFutureWatcher<std::set<QSharedPointer<ModInfo>>> *watcher = static_cast<QFutureWatcher<std::set<QSharedPointer<ModInfo>>> *>(sender());
+
+ auto finalMods = watcher->result();
+
+ if (finalMods.empty()) {
+ qInfo("None of your mods appear to have had recent file updates.");
+ }
+
+ std::set<std::pair<QString, int>> organizedGames;
+ for (auto mod : finalMods) {
+ if (mod->canBeUpdated()) {
+ organizedGames.insert(std::make_pair<QString, int>(mod->getGameName().toLower(), mod->getNexusID()));
+ }
+ }
+
+ if (!finalMods.empty() && organizedGames.empty())
+ qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.");
+
+ for (auto game : organizedGames)
+ NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString());
+
+ disconnect(sender());
+ delete sender();
+}
+
+void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
+{
+ QVariantMap resultInfo = resultData.toMap();
+ QList files = resultInfo["files"].toList();
+ QList fileUpdates = resultInfo["file_updates"].toList();
+ QString gameNameReal;
+ for (IPluginGame *game : m_PluginContainer.plugins<IPluginGame>()) {
+ if (game->gameNexusName() == gameName) {
+ gameNameReal = game->gameShortName();
+ break;
+ }
+ }
+ std::vector<ModInfo::Ptr> modsList = ModInfo::getByModID(gameNameReal, modID);
+
+ bool requiresInfo = false;
+ for (auto mod : modsList) {
+ bool foundUpdate = false;
+ bool oldFile = false;
+ QString installedFile = mod->getInstallationFile();
+ QVariantMap foundFile;
+ for (auto file : files) {
+ QVariantMap fileData = file.toMap();
+ if (fileData["file_name"].toString().compare(installedFile, Qt::CaseInsensitive) == 0) {
+ foundFile = fileData;
+ mod->setNexusFileStatus(foundFile["category_id"].toInt());
+ if (mod->getNexusFileStatus() == 4 || mod->getNexusFileStatus() == 6)
+ oldFile = true;
+ }
+ }
+ for (auto update : fileUpdates) {
+ QVariantMap updateData = update.toMap();
+ // Locate the current install file as an update
+ if (installedFile == updateData["old_file_name"].toString()) {
+ int currentUpdate = updateData["new_file_id"].toInt();
+ bool finalUpdate = false;
+ // Crawl the updates to make sure we have the latest file version
+ while (!finalUpdate) {
+ finalUpdate = true;
+ for (auto updateScan : fileUpdates) {
+ QVariantMap updateScanData = updateScan.toMap();
+ if (currentUpdate == updateScanData["old_file_id"].toInt()) {
+ currentUpdate = updateScanData["new_file_id"].toInt();
+ finalUpdate = false;
+ // Apply the version data from the latest file
+ for (auto file : files) {
+ QVariantMap fileData = file.toMap();
+ if (fileData["file_id"].toInt() == currentUpdate) {
+ if (fileData["category_id"].toInt() != 6) {
+ mod->setNewestVersion(fileData["version"].toString());
+ foundUpdate = true;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ break;
+ } else if (installedFile == updateData["new_file_name"].toString()) {
+ // This is a safety mechanism if this is the latest update file so we don't use the mod version
+ if (!foundUpdate && !oldFile) {
+ foundUpdate = true;
+ mod->setNewestVersion(foundFile["version"].toString());
+ }
+ }
+ }
+
+ if (foundUpdate) {
+ // Just get the standard data updates for endorsements and descriptions
+ mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc());
+ if (m_ModListSortProxy != nullptr)
+ m_ModListSortProxy->invalidate();
+ } else {
+ // Scrape mod data here so we can use the mod version if no file update was located
+ requiresInfo = true;
+ }
+ }
+
+ if (requiresInfo)
+ NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameNameReal, modID, this, QVariant(), QString());
+}
+
+void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
+{
+ QVariantMap result = resultData.toMap();
+ bool foundUpdate = false;
+ QString gameNameReal;
+ for (IPluginGame *game : m_PluginContainer.plugins<IPluginGame>()) {
+ if (game->gameNexusName() == gameName) {
+ gameNameReal = game->gameShortName();
+ break;
+ }
+ }
+ std::vector<ModInfo::Ptr> modsList = ModInfo::getByModID(gameNameReal, modID);
+ for (auto mod : modsList) {
+ QDateTime now = QDateTime::currentDateTimeUtc();
+ QDateTime updateTarget = mod->getExpires();
+ if (now >= updateTarget) {
+ mod->setNewestVersion(result["version"].toString());
+ mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc());
+ foundUpdate = true;
+ }
+ mod->setNexusDescription(result["description"].toString());
+ if ((mod->endorsedState() != ModInfo::ENDORSED_NEVER) && (result.contains("endorsement"))) {
+ QVariantMap endorsement = result["endorsement"].toMap();
+ QString endorsementStatus = endorsement["endorse_status"].toString();
+ if (endorsementStatus.compare("Endorsed") == 00)
+ mod->setIsEndorsed(true);
+ else if (endorsementStatus.compare("Abstained") == 00)
+ mod->setNeverEndorse();
+ else
+ mod->setIsEndorsed(false);
+ }
+ mod->setLastNexusQuery(QDateTime::currentDateTimeUtc());
+ mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC));
+ mod->saveMeta();
+ }
+ if (foundUpdate && m_ModListSortProxy != nullptr)
+ m_ModListSortProxy->invalidate();
+}
+
+void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int)
+{
+ QMap results = resultData.toMap();
+ if (results["status"].toString().compare("Endorsed") == 0) {
+ QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!"));
+ Settings::instance().directInterface().setValue("endorse_state", "Endorsed");
+ } else if (results["status"].toString().compare("Abstained") == 0) {
+ QMessageBox::information(this, tr("Okay."), tr("This mod will not be endorsed and will no longer ask you to endorse."));
+ Settings::instance().directInterface().setValue("endorse_state", "Abstained");
+ }
+ toggleMO2EndorseState();
+ if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
+ this, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)))) {
+ qCritical("failed to disconnect endorsement slot");
+ }
+}
+
+void MainWindow::nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int)
+{
+ QMap<QString, QString> gameNames;
+ for (auto game : m_PluginContainer.plugins<IPluginGame>()) {
+ gameNames[game->gameNexusName()] = game->gameShortName();
+ }
+
+ for (unsigned int i = 0; i < ModInfo::getNumMods(); i++) {
+ auto modInfo = ModInfo::getByIndex(i);
+ if (modInfo->getNexusID() <= 0)
+ continue;
+
+ bool found = false;
+ auto resultsList = resultData.toList();
+ for (auto item : resultsList) {
+ auto results = item.toMap();
+ if ((gameNames[results["domain_name"].toString()].compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) &&
+ (results["mod_id"].toInt() == modInfo->getNexusID())) {
+ found = true;
+ break;
+ }
+ }
+
+ modInfo->setIsTracked(found);
+ modInfo->saveMeta();
+ }
+}
+
+void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultData, int)
+{
+ QVariantList serverList = resultData.toList();
+
+ QList<ServerInfo> servers;
+ for (const QVariant &server : serverList) {
+ QVariantMap serverInfo = server.toMap();
+ ServerInfo info;
+ info.name = serverInfo["short_name"].toString();
+ info.premium = serverInfo["name"].toString().contains("Premium", Qt::CaseInsensitive);
+ info.lastSeen = QDate::currentDate();
+ info.preferred = serverInfo["short_name"].toString().contains("CDN", Qt::CaseInsensitive);
+ servers.append(info);
+ }
+ m_OrganizerCore.settings().updateServers(servers);
+}
+
+
+void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString)
+{
+ if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) {
+ qDebug(qUtf8Printable(tr("Mod ID %1 no longer seems to be available on Nexus.").arg(modID)));
+ } else {
+ MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this);
+ }
+}
+
+
+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)
+{
+ QDir().mkdir(destination);
+ BSA::EErrorCode result = BSA::ERROR_NONE;
+ QString errorFile;
+
+ for (unsigned int i = 0; i < folder->getNumFiles(); ++i) {
+ BSA::File::Ptr file = folder->getFile(i);
+ BSA::EErrorCode res = archive.extract(file, qUtf8Printable(destination));
+ if (res != BSA::ERROR_NONE) {
+ reportError(tr("failed to read %1: %2").arg(file->getName().c_str()).arg(res));
+ result = res;
+ }
+ progress.setLabelText(file->getName().c_str());
+ progress.setValue(progress.value() + 1);
+ QCoreApplication::processEvents();
+ if (progress.wasCanceled()) {
+ result = BSA::ERROR_CANCELED;
+ }
+ }
+
+ if (result != BSA::ERROR_NONE) {
+ if (QMessageBox::critical(this, tr("Error"), tr("failed to extract %1 (errorcode %2)").arg(errorFile).arg(result),
+ QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) {
+ return result;
+ }
+ }
+
+ for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) {
+ BSA::Folder::Ptr subFolder = folder->getSubFolder(i);
+ BSA::EErrorCode res = extractBSA(archive, subFolder,
+ destination.mid(0).append("/").append(subFolder->getName().c_str()), progress);
+ if (res != BSA::ERROR_NONE) {
+ return res;
+ }
+ }
+ return BSA::ERROR_NONE;
+}
+
+
+bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std::string fileName)
+{
+ progress.setLabelText(fileName.c_str());
+ progress.setValue(percentage);
+ QCoreApplication::processEvents();
+ return !progress.wasCanceled();
+}
+
+
+void MainWindow::extractBSATriggered()
+{
+ QTreeWidgetItem *item = m_ContextItem;
+ QString origin;
+
+ QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA"));
+ QStringList archives = {};
+ if (!targetFolder.isEmpty()) {
+ if (!item->parent()) {
+ for (int i = 0; i < item->childCount(); ++i) {
+ archives.append(item->child(i)->text(0));
+ }
+ origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(0))).getPath()));
+ } else {
+ origin = QDir::fromNativeSeparators(ToQString(m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(item->text(1))).getPath()));
+ archives = QStringList({ item->text(0) });
+ }
+
+ for (auto archiveName : archives) {
+ BSA::Archive archive;
+ QString archivePath = QString("%1\\%2").arg(origin).arg(archiveName);
+ BSA::EErrorCode result = archive.read(archivePath.toLocal8Bit().constData(), true);
+ if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) {
+ reportError(tr("failed to read %1: %2").arg(archivePath).arg(result));
+ return;
+ }
+
+ QProgressDialog progress(this);
+ progress.setMaximum(100);
+ progress.setValue(0);
+ progress.show();
+ archive.extractAll(QDir::toNativeSeparators(targetFolder).toLocal8Bit().constData(),
+ boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2));
+ if (result == BSA::ERROR_INVALIDHASHES) {
+ reportError(tr("This archive contains invalid hashes. Some files may be broken."));
+ }
+ archive.close();
+ }
+ }
+}
+
+
+void MainWindow::displayColumnSelection(const QPoint &pos)
+{
+ QMenu menu;
+
+ // display a list of all headers as checkboxes
+ QAbstractItemModel *model = ui->modList->header()->model();
+ for (int i = 1; i < model->columnCount(); ++i) {
+ QString columnName = model->headerData(i, Qt::Horizontal).toString();
+ QCheckBox *checkBox = new QCheckBox(&menu);
+ checkBox->setText(columnName);
+ checkBox->setChecked(!ui->modList->header()->isSectionHidden(i));
+ QWidgetAction *checkableAction = new QWidgetAction(&menu);
+ checkableAction->setDefaultWidget(checkBox);
+ menu.addAction(checkableAction);
+ }
+ menu.exec(pos);
+
+ // view/hide columns depending on check-state
+ int i = 1;
+ for (const QAction *action : menu.actions()) {
+ const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction*>(action);
+ if (widgetAction != nullptr) {
+ const QCheckBox *checkBox = qobject_cast<const QCheckBox*>(widgetAction->defaultWidget());
+ if (checkBox != nullptr) {
+ ui->modList->header()->setSectionHidden(i, !checkBox->isChecked());
+ }
+ }
+ ++i;
+ }
+}
+
+void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos)
+{
+ m_ContextItem = ui->bsaList->itemAt(pos);
+
+// m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos));
+
+ QMenu menu;
+ menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered()));
+
+ menu.exec(ui->bsaList->mapToGlobal(pos));
+}
+
+void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int)
+{
+ m_ArchiveListWriter.write();
+ m_CheckBSATimer.start(500);
+}
+
+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();
+ }
+}
+
+void MainWindow::on_actionChange_Game_triggered()
+{
+ if (QMessageBox::question(this, tr("Are you sure?"),
+ tr("This will restart MO, continue?"),
+ QMessageBox::Yes | QMessageBox::Cancel)
+ == QMessageBox::Yes) {
+ InstanceManager::instance().clearCurrentInstance();
+ qApp->exit(INT_MAX);
+ }
+}
+
+void MainWindow::setCategoryListVisible(bool visible)
+{
+ if (visible) {
+ ui->categoriesGroup->show();
+ ui->displayCategoriesBtn->setText(ToQString(L"\u00ab"));
+ } else {
+ ui->categoriesGroup->hide();
+ ui->displayCategoriesBtn->setText(ToQString(L"\u00bb"));
+ }
+}
+
+void MainWindow::on_displayCategoriesBtn_toggled(bool checked)
+{
+ setCategoryListVisible(checked);
+}
+
+void MainWindow::editCategories()
+{
+ CategoriesDialog dialog(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) {
+ dialog.commitChanges();
+ }
+ settings.setValue(key, dialog.saveGeometry());
+
+}
+
+void MainWindow::deselectFilters()
+{
+ ui->categoriesList->clearSelection();
+}
+
+void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos)
+{
+ QMenu menu;
+ menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories()));
+ menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters()));
+
+ menu.exec(ui->categoriesList->mapToGlobal(pos));
+}
+
+
+void MainWindow::updateESPLock(bool locked)
+{
+ QItemSelection currentSelection = ui->espList->selectionModel()->selection();
+ if (currentSelection.count() == 0) {
+ // this path is probably useless
+ m_OrganizerCore.pluginList()->lockESPIndex(m_ContextRow, locked);
+ } else {
+ Q_FOREACH (const QModelIndex &idx, currentSelection.indexes()) {
+ if (m_OrganizerCore.pluginList()->isEnabled(mapToModel(m_OrganizerCore.pluginList(), idx).row())) {
+ m_OrganizerCore.pluginList()->lockESPIndex(mapToModel(m_OrganizerCore.pluginList(), idx).row(), locked);
+ }
+ }
+ }
+}
+
+
+void MainWindow::lockESPIndex()
+{
+ updateESPLock(true);
+}
+
+void MainWindow::unlockESPIndex()
+{
+ updateESPLock(false);
+}
+
+
+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");
+ }
+
+ updateToolBar();
+}
+
+
+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.exec(ui->toolBar->mapToGlobal(point));
+ }
+ }
+}
+
+void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos)
+{
+ m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row();
+
+ QMenu menu;
+ menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedPlugins_clicked()));
+ menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedPlugins_clicked()));
+
+ menu.addSeparator();
+
+ menu.addAction(tr("Enable all"), m_OrganizerCore.pluginList(), SLOT(enableAll()));
+ menu.addAction(tr("Disable all"), m_OrganizerCore.pluginList(), SLOT(disableAll()));
+
+ menu.addSeparator();
+
+ addPluginSendToContextMenu(&menu);
+
+ QItemSelection currentSelection = ui->espList->selectionModel()->selection();
+ bool hasLocked = false;
+ bool hasUnlocked = false;
+ for (const QModelIndex &idx : currentSelection.indexes()) {
+ int row = m_PluginListSortProxy->mapToSource(idx).row();
+ if (m_OrganizerCore.pluginList()->isEnabled(row)) {
+ if (m_OrganizerCore.pluginList()->isESPLocked(row)) {
+ hasLocked = true;
+ } else {
+ hasUnlocked = true;
+ }
+ }
+ }
+
+ if (hasLocked) {
+ menu.addAction(tr("Unlock load order"), this, SLOT(unlockESPIndex()));
+ }
+ if (hasUnlocked) {
+ menu.addAction(tr("Lock load order"), this, SLOT(lockESPIndex()));
+ }
+
+ menu.addSeparator();
+
+
+ QModelIndex idx = ui->espList->selectionModel()->currentIndex();
+ 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()));
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex);
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) {
+ QAction *infoAction = menu.addAction(tr("Open Origin Info..."), this, SLOT(openOriginInformation_clicked()));
+ menu.setDefaultAction(infoAction);
+ }
+ }
+
+ try {
+ menu.exec(ui->espList->mapToGlobal(pos));
+ } catch (const std::exception &e) {
+ reportError(tr("Exception: ").arg(e.what()));
+ } catch (...) {
+ reportError(tr("Unknown exception"));
+ }
+}
+
+void MainWindow::on_groupCombo_currentIndexChanged(int index)
+{
+ if (m_ModListSortProxy == nullptr) {
+ return;
+ }
+ QAbstractProxyModel *newModel = nullptr;
+ switch (index) {
+ case 1: {
+ newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_CATEGORY, Qt::UserRole,
+ 0, Qt::UserRole + 2);
+ } break;
+ case 2: {
+ newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_MODID, Qt::DisplayRole,
+ QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE,
+ Qt::UserRole + 2);
+ } break;
+ default: {
+ newModel = nullptr;
+ } break;
+ }
+
+ if (newModel != nullptr) {
+#ifdef TEST_MODELS
+ new ModelTest(newModel, this);
+#endif // TEST_MODELS
+ m_ModListSortProxy->setSourceModel(newModel);
+ connect(ui->modList, SIGNAL(expanded(QModelIndex)),newModel, SLOT(expanded(QModelIndex)));
+ connect(ui->modList, SIGNAL(collapsed(QModelIndex)), newModel, SLOT(collapsed(QModelIndex)));
+ connect(newModel, SIGNAL(expandItem(QModelIndex)), this, SLOT(expandModList(QModelIndex)));
+ } else {
+ m_ModListSortProxy->setSourceModel(m_OrganizerCore.modList());
+ }
+ modFilterActive(m_ModListSortProxy->isFilterActive());
+}
+
+const Executable &MainWindow::getSelectedExecutable() const
+{
+ QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex());
+ return m_OrganizerCore.executablesList()->find(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);
+}
+
+void MainWindow::on_showHiddenBox_toggled(bool checked)
+{
+ m_OrganizerCore.downloadManager()->setShowHidden(checked);
+}
+
+
+void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite)
+{
+ SECURITY_ATTRIBUTES secAttributes;
+ secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
+ secAttributes.bInheritHandle = TRUE;
+ secAttributes.lpSecurityDescriptor = nullptr;
+
+ if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) {
+ qCritical("failed to create stdout reroute");
+ }
+
+ if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) {
+ qCritical("failed to correctly set up the stdout reroute");
+ *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE;
+ }
+}
+
+std::string MainWindow::readFromPipe(HANDLE stdOutRead)
+{
+ static const int chunkSize = 128;
+ std::string result;
+
+ char buffer[chunkSize + 1];
+ buffer[chunkSize] = '\0';
+
+ DWORD read = 1;
+ while (read > 0) {
+ if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) {
+ break;
+ }
+ if (read > 0) {
+ result.append(buffer, read);
+ if (read < chunkSize) {
+ break;
+ }
+ }
+ }
+ return result;
+}
+
+void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog)
+{
+ std::vector<std::string> lines;
+ boost::split(lines, lootOut, boost::is_any_of("\r\n"));
+
+ std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\.");
+ std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\.");
+
+ for (const std::string &line : lines) {
+ if (line.length() > 0) {
+ size_t progidx = line.find("[progress]");
+ size_t erroridx = line.find("[error]");
+ if (progidx != std::string::npos) {
+ dialog.setLabelText(line.substr(progidx + 11).c_str());
+ } else if (erroridx != std::string::npos) {
+ qWarning("%s", line.c_str());
+ errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n");
+ } else {
+ std::smatch match;
+ if (std::regex_match(line, match, exRequires)) {
+ std::string modName(match[1].first, match[1].second);
+ std::string dependency(match[2].first, match[2].second);
+ m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str()));
+ } else if (std::regex_match(line, match, exIncompatible)) {
+ std::string modName(match[1].first, match[1].second);
+ std::string dependency(match[2].first, match[2].second);
+ m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str()));
+ } else {
+ qDebug("[loot] %s", line.c_str());
+ }
+ }
+ }
+ }
+}
+
+void MainWindow::on_bossButton_clicked()
+{
+ std::string reportURL;
+ std::string errorMessages;
+
+ //m_OrganizerCore.currentProfile()->writeModlistNow();
+ m_OrganizerCore.savePluginList();
+ //Create a backup of the load orders w/ LOOT in name
+ //to make sure that any sorting is easily undo-able.
+ //Need to figure out how I want to do that.
+
+ bool success = false;
+
+ try {
+ setEnabled(false);
+ ON_BLOCK_EXIT([&] () { setEnabled(true); });
+ QProgressDialog dialog(this);
+ dialog.setLabelText(tr("Please wait while LOOT is running"));
+ dialog.setMaximum(0);
+ dialog.show();
+
+ QString outPath = QDir::temp().absoluteFilePath("lootreport.json");
+
+ QStringList parameters;
+ parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName()
+ << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath())
+ << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath())
+ << "--out" << QString("\"%1\"").arg(outPath);
+
+ if (m_DidUpdateMasterList) {
+ parameters << "--skipUpdateMasterlist";
+ }
+ HANDLE stdOutWrite = INVALID_HANDLE_VALUE;
+ HANDLE stdOutRead = INVALID_HANDLE_VALUE;
+ createStdoutPipe(&stdOutRead, &stdOutWrite);
+ try {
+ m_OrganizerCore.prepareVFS();
+ } catch (const UsvfsConnectorException &e) {
+ qDebug(e.what());
+ return;
+ } catch (const std::exception &e) {
+ QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what());
+ return;
+ }
+
+ HANDLE loot = startBinary(QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"),
+ parameters.join(" "),
+ qApp->applicationDirPath() + "/loot",
+ true,
+ stdOutWrite);
+
+ // we don't use the write end
+ ::CloseHandle(stdOutWrite);
+
+ m_OrganizerCore.pluginList()->clearAdditionalInformation();
+
+ DWORD retLen;
+ JOBOBJECT_BASIC_PROCESS_ID_LIST info;
+ HANDLE processHandle = loot;
+
+ if (loot != INVALID_HANDLE_VALUE) {
+ bool isJobHandle = true;
+ ULONG lastProcessID;
+ DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE);
+ while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) {
+ if (isJobHandle) {
+ if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) {
+ if (info.NumberOfProcessIdsInList == 0) {
+ qDebug("no more processes in job");
+ break;
+ } else {
+ if (lastProcessID != info.ProcessIdList[0]) {
+ lastProcessID = info.ProcessIdList[0];
+ if (processHandle != loot) {
+ ::CloseHandle(processHandle);
+ }
+ processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID);
+ }
+ }
+ } else {
+ // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there
+ // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running.
+ // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without
+ // the right to break out.
+ if (::GetLastError() != ERROR_MORE_DATA) {
+ isJobHandle = false;
+ }
+ }
+ }
+
+ if (dialog.wasCanceled()) {
+ if (isJobHandle) {
+ ::TerminateJobObject(loot, 1);
+ } else {
+ ::TerminateProcess(loot, 1);
+ }
+ }
+
+ // keep processing events so the app doesn't appear dead
+ QCoreApplication::processEvents();
+ std::string lootOut = readFromPipe(stdOutRead);
+ processLOOTOut(lootOut, errorMessages, dialog);
+
+ res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE);
+ }
+
+ std::string remainder = readFromPipe(stdOutRead).c_str();
+ if (remainder.length() > 0) {
+ processLOOTOut(remainder, errorMessages, dialog);
+ }
+ DWORD exitCode = 0UL;
+ ::GetExitCodeProcess(processHandle, &exitCode);
+ ::CloseHandle(processHandle);
+ if (exitCode != 0UL) {
+ reportError(tr("loot failed. Exit code was: %1").arg(exitCode));
+ return;
+ } else {
+ success = true;
+ QFile outFile(outPath);
+ outFile.open(QIODevice::ReadOnly);
+ QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll());
+ QJsonArray array = doc.array();
+ for (auto iter = array.begin(); iter != array.end(); ++iter) {
+ QJsonObject pluginObj = (*iter).toObject();
+ QJsonArray pluginMessages = pluginObj["messages"].toArray();
+ for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) {
+ QJsonObject msg = (*msgIter).toObject();
+ m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(),
+ QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString()));
+ }
+ if (pluginObj["dirty"].toString() == "yes")
+ m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty");
+ }
+
+ }
+ } else {
+ reportError(tr("failed to start loot"));
+ }
+ } catch (const std::exception &e) {
+ reportError(tr("failed to run loot: %1").arg(e.what()));
+ }
+
+ if (errorMessages.length() > 0) {
+ QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occured"), errorMessages.c_str(), QMessageBox::Ok, this);
+ warn->setModal(false);
+ warn->show();
+ }
+
+ if (success) {
+ m_DidUpdateMasterList = true;
+ if (reportURL.length() > 0) {
+ m_IntegratedBrowser.setWindowTitle("LOOT Report");
+ QString report(reportURL.c_str());
+ QStringList temp = report.split("?");
+ QUrl url = QUrl::fromLocalFile(temp.at(0));
+ if (temp.size() > 1) {
+ url.setQuery(temp.at(1).toUtf8());
+ }
+ m_IntegratedBrowser.openUrl(url);
+ }
+ m_OrganizerCore.refreshESPList(false);
+ m_OrganizerCore.savePluginList();
+ }
+}
+
+
+const char *MainWindow::PATTERN_BACKUP_GLOB = ".????_??_??_??_??_??";
+const char *MainWindow::PATTERN_BACKUP_REGEX = "\\.(\\d\\d\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d)";
+const char *MainWindow::PATTERN_BACKUP_DATE = "yyyy_MM_dd_hh_mm_ss";
+
+
+bool MainWindow::createBackup(const QString &filePath, const QDateTime &time)
+{
+ QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE);
+ if (shellCopy(QStringList(filePath), QStringList(outPath), this)) {
+ QFileInfo fileInfo(filePath);
+ removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 10, QDir::Name);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void MainWindow::on_saveButton_clicked()
+{
+ m_OrganizerCore.savePluginList();
+ QDateTime now = QDateTime::currentDateTime();
+ if (createBackup(m_OrganizerCore.currentProfile()->getPluginsFileName(), now)
+ && createBackup(m_OrganizerCore.currentProfile()->getLoadOrderFileName(), now)
+ && createBackup(m_OrganizerCore.currentProfile()->getLockedOrderFileName(), now)) {
+ MessageDialog::showMessage(tr("Backup of load order created"), this);
+ }
+}
+
+QString MainWindow::queryRestore(const QString &filePath)
+{
+ QFileInfo pluginFileInfo(filePath);
+ QString pattern = pluginFileInfo.fileName() + ".*";
+ QFileInfoList files = pluginFileInfo.absoluteDir().entryInfoList(QStringList(pattern), QDir::Files, QDir::Name);
+
+ SelectionDialog dialog(tr("Choose backup to restore"), this);
+ QRegExp exp(pluginFileInfo.fileName() + PATTERN_BACKUP_REGEX);
+ QRegExp exp2(pluginFileInfo.fileName() + "\\.(.*)");
+ for(const QFileInfo &info : boost::adaptors::reverse(files)) {
+ if (exp.exactMatch(info.fileName())) {
+ QDateTime time = QDateTime::fromString(exp.cap(1), PATTERN_BACKUP_DATE);
+ dialog.addChoice(time.toString(), "", exp.cap(1));
+ } else if (exp2.exactMatch(info.fileName())) {
+ dialog.addChoice(exp2.cap(1), "", exp2.cap(1));
+ }
+ }
+
+ if (dialog.numChoices() == 0) {
+ QMessageBox::information(this, tr("No Backups"), tr("There are no backups to restore"));
+ return QString();
+ }
+
+ if (dialog.exec() == QDialog::Accepted) {
+ return dialog.getChoiceData().toString();
+ } else {
+ return QString();
+ }
+}
+
+void MainWindow::on_restoreButton_clicked()
+{
+ QString pluginName = m_OrganizerCore.currentProfile()->getPluginsFileName();
+ QString choice = queryRestore(pluginName);
+ if (!choice.isEmpty()) {
+ QString loadOrderName = m_OrganizerCore.currentProfile()->getLoadOrderFileName();
+ QString lockedName = m_OrganizerCore.currentProfile()->getLockedOrderFileName();
+ if (!shellCopy(pluginName + "." + choice, pluginName, true, this) ||
+ !shellCopy(loadOrderName + "." + choice, loadOrderName, true, this) ||
+ !shellCopy(lockedName + "." + choice, lockedName, true, this)) {
+ QMessageBox::critical(this, tr("Restore failed"),
+ tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError())));
+ }
+ m_OrganizerCore.refreshESPList(true);
+ }
+}
+
+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);
+ }
+}
+
+void MainWindow::on_restoreModsButton_clicked()
+{
+ QString modlistName = m_OrganizerCore.currentProfile()->getModlistFileName();
+ QString choice = queryRestore(modlistName);
+ if (!choice.isEmpty()) {
+ if (!shellCopy(modlistName + "." + choice, modlistName, true, this)) {
+ QMessageBox::critical(this, tr("Restore failed"),
+ tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError())));
+ }
+ m_OrganizerCore.refreshModList(false);
+ }
+}
+
+void MainWindow::on_actionCopy_Log_to_Clipboard_triggered()
+{
+ QStringList lines;
+ QAbstractItemModel *model = ui->logList->model();
+ for (int i = 0; i < model->rowCount(); ++i) {
+ lines.append(QString("%1 [%2] %3").arg(model->index(i, 0).data().toString())
+ .arg(model->index(i, 1).data(Qt::UserRole).toString())
+ .arg(model->index(i, 1).data().toString()));
+ }
+ QApplication::clipboard()->setText(lines.join("\n"));
+}
+
+void MainWindow::on_categoriesAndBtn_toggled(bool checked)
+{
+ if (checked) {
+ m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_AND);
+ }
+}
+
+void MainWindow::on_categoriesOrBtn_toggled(bool checked)
+{
+ if (checked) {
+ m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_OR);
+ }
+}
+
+void MainWindow::on_managedArchiveLabel_linkHovered(const QString&)
+{
+ QToolTip::showText(QCursor::pos(),
+ ui->managedArchiveLabel->toolTip());
+}
+
+void MainWindow::dragEnterEvent(QDragEnterEvent *event)
+{
+ //Accept copy or move drags to the download window. Link drags are not
+ //meaningful (Well, they are - we could drop a link in the download folder,
+ //but you need privileges to do that).
+ if (ui->downloadTab->isVisible() &&
+ (event->proposedAction() == Qt::CopyAction ||
+ event->proposedAction() == Qt::MoveAction) &&
+ event->answerRect().intersects(ui->downloadTab->rect())) {
+
+ //If I read the documentation right, this won't work under a motif windows
+ //manager and the check needs to be done at the drop. However, that means
+ //the user might be allowed to drop things which we can't sanely process
+ QMimeData const *data = event->mimeData();
+
+ if (data->hasUrls()) {
+ QStringList extensions =
+ m_OrganizerCore.installationManager()->getSupportedExtensions();
+
+ //This is probably OK - scan to see if these are moderately sane archive
+ //types
+ QList<QUrl> urls = data->urls();
+ bool ok = true;
+ for (const QUrl &url : urls) {
+ if (url.isLocalFile()) {
+ QString local = url.toLocalFile();
+ bool fok = false;
+ for (auto ext : extensions) {
+ if (local.endsWith(ext, Qt::CaseInsensitive)) {
+ fok = true;
+ break;
+ }
+ }
+ if (! fok) {
+ ok = false;
+ break;
+ }
+ }
+ }
+ if (ok) {
+ event->accept();
+ }
+ }
+ }
+}
+
+void MainWindow::dropLocalFile(const QUrl &url, const QString &outputDir, bool move)
+{
+ QFileInfo file(url.toLocalFile());
+ if (!file.exists()) {
+ qWarning("invalid source file: %s", qUtf8Printable(file.absoluteFilePath()));
+ return;
+ }
+ QString target = outputDir + "/" + file.fileName();
+ if (QFile::exists(target)) {
+ QMessageBox box(QMessageBox::Question,
+ file.fileName(),
+ tr("A file with the same name has already been downloaded. "
+ "What would you like to do?"));
+ box.addButton(tr("Overwrite"), QMessageBox::ActionRole);
+ box.addButton(tr("Rename new file"), QMessageBox::YesRole);
+ box.addButton(tr("Ignore file"), QMessageBox::RejectRole);
+
+ box.exec();
+ switch (box.buttonRole(box.clickedButton())) {
+ case QMessageBox::RejectRole:
+ return;
+ case QMessageBox::ActionRole:
+ break;
+ default:
+ case QMessageBox::YesRole:
+ target = m_OrganizerCore.downloadManager()->getDownloadFileName(file.fileName());
+ break;
+ }
+ }
+
+ bool success = false;
+ if (move) {
+ success = shellMove(file.absoluteFilePath(), target, true, this);
+ } else {
+ success = shellCopy(file.absoluteFilePath(), target, true, this);
+ }
+ if (!success) {
+ qCritical("file operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError())));
+ }
+}
+
+bool MainWindow::registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName) {
+ // register the view so it's geometry gets saved at exit
+ m_PersistedGeometry.push_back(std::make_pair(name, view));
+
+ // also, restore the geometry if it was saved before
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+
+ QString key = QString("geometry/%1").arg(name);
+ QByteArray data;
+
+ if ((oldSettingName != nullptr) && settings.contains(oldSettingName)) {
+ data = settings.value(oldSettingName).toByteArray();
+ settings.remove(oldSettingName);
+ } else if (settings.contains(key)) {
+ data = settings.value(key).toByteArray();
+ }
+
+ if (!data.isEmpty()) {
+ view->restoreState(data);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void MainWindow::dropEvent(QDropEvent *event)
+{
+ Qt::DropAction action = event->proposedAction();
+ QString outputDir = m_OrganizerCore.downloadManager()->getOutputDirectory();
+ if (action == Qt::MoveAction) {
+ //Tell windows I'm taking control and will delete the source of a move.
+ event->setDropAction(Qt::TargetMoveAction);
+ }
+ for (const QUrl &url : event->mimeData()->urls()) {
+ if (url.isLocalFile()) {
+ dropLocalFile(url, outputDir, action == Qt::MoveAction);
+ } else {
+ m_OrganizerCore.downloadManager()->startDownloadURLs(QStringList() << url.url());
+ }
+ }
+ event->accept();
+}
+
+
+void MainWindow::on_clickBlankButton_clicked()
+{
+ deselectFilters();
+}
+
+void MainWindow::on_clearFiltersButton_clicked()
+{
+ ui->modFilterEdit->clear();
+ deselectFilters();
+}
+
+void MainWindow::sendSelectedModsToPriority(int newPriority)
+{
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ std::vector<int> modsToMove;
+ for (auto idx : selection->selectedRows(ModList::COL_PRIORITY)) {
+ modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt()));
+ }
+ m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority);
+ } else {
+ m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority);
+ }
+}
+
+void MainWindow::sendSelectedModsToTop_clicked()
+{
+ sendSelectedModsToPriority(0);
+}
+
+void MainWindow::sendSelectedModsToBottom_clicked()
+{
+ sendSelectedModsToPriority(INT_MAX);
+}
+
+void MainWindow::sendSelectedModsToPriority_clicked()
+{
+ bool ok;
+ int newPriority = QInputDialog::getInt(this,
+ tr("Set Priority"), tr("Set the priority of the selected mods"),
+ 0, 0, INT_MAX, 1, &ok);
+ if (!ok) return;
+
+ sendSelectedModsToPriority(newPriority);
+}
+
+void MainWindow::sendSelectedModsToSeparator_clicked()
+{
+ QStringList separators;
+ auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority();
+ for (auto iter = indexesByPriority.begin(); iter != indexesByPriority.end(); iter++) {
+ if ((iter->second != UINT_MAX)) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second);
+ if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) {
+ separators << modInfo->name().chopped(10); // Chops the "_separator" away from the name
+ }
+ }
+ }
+
+ ListDialog dialog(this);
+ QSettings &settings = m_OrganizerCore.settings().directInterface();
+ QString key = QString("geometry/%1").arg(dialog.objectName());
+
+ dialog.setWindowTitle("Select a separator...");
+ dialog.setChoices(separators);
+ dialog.restoreGeometry(settings.value(key).toByteArray());
+ if (dialog.exec() == QDialog::Accepted) {
+ QString result = dialog.getChoice();
+ if (!result.isEmpty()) {
+ result += "_separator";
+
+ int newPriority = INT_MAX;
+ bool foundSection = false;
+ for (auto mod : m_OrganizerCore.modsSortedByProfilePriority()) {
+ unsigned int modIndex = ModInfo::getIndex(mod);
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ if (!foundSection && result.compare(mod) == 0) {
+ foundSection = true;
+ } else if (foundSection && modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) {
+ newPriority = m_OrganizerCore.currentProfile()->getModPriority(modIndex);
+ break;
+ }
+ }
+
+ QItemSelectionModel *selection = ui->modList->selectionModel();
+ if (selection->hasSelection() && selection->selectedRows().count() > 1) {
+ std::vector<int> modsToMove;
+ for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) {
+ modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt()));
+ }
+ m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority);
+ } else {
+ int oldPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow);
+ if (oldPriority < newPriority)
+ --newPriority;
+ m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority);
+ }
+ }
+ }
+ settings.setValue(key, dialog.saveGeometry());
+}
+
+void MainWindow::on_showArchiveDataCheckBox_toggled(const bool checked)
+{
+ if (m_OrganizerCore.getArchiveParsing() && checked)
+ {
+ m_showArchiveData = checked;
+ }
+ else
+ {
+ m_showArchiveData = false;
+ }
+ refreshDataTree();
+}
+
diff --git a/src/mainwindow.h b/src/mainwindow.h
index e799a64a..d119f49c 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -1,636 +1,670 @@
-/*
-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/>.
-*/
-
-#ifndef MAINWINDOW_H
-#define MAINWINDOW_H
-
-#include "bsafolder.h"
-#include "browserdialog.h"
-#include "delayedfilewriter.h"
-#include "errorcodes.h"
-#include "imoinfo.h"
-#include "iuserinterface.h"
-#include "modinfo.h"
-#include "modlistsortproxy.h"
-#include "savegameinfo.h"
-#include "tutorialcontrol.h"
-
-//Note the commented headers here can be replaced with forward references,
-//when I get round to cleaning up main.cpp
-struct Executable;
-class CategoryFactory;
-class LockedDialogBase;
-class OrganizerCore;
-#include "plugincontainer.h" //class PluginContainer;
-class PluginListSortProxy;
-namespace BSA { class Archive; }
-#include "iplugingame.h" //namespace MOBase { class IPluginGame; }
-namespace MOBase { class IPluginModPage; }
-namespace MOBase { class IPluginTool; }
-namespace MOBase { class ISaveGame; }
-
-namespace MOShared { class DirectoryEntry; }
-
-#include <QByteArray>
-#include <QDir>
-#include <QFileInfo>
-#include <QFileSystemWatcher>
-#include <QList>
-#include <QMainWindow>
-#include <QObject>
-#include <QPersistentModelIndex>
-#include <QProcess>
-#include <QString>
-#include <QStringList>
-#include <QTime>
-#include <QTimer>
-#include <QHeaderView>
-#include <QVariant>
-#include <Qt>
-#include <QtConcurrent/QtConcurrentRun>
-
-class QAction;
-class QAbstractItemModel;
-class QDateTime;
-class QEvent;
-class QFile;
-class QListWidgetItem;
-class QMenu;
-class QModelIndex;
-class QPoint;
-class QProgressBar;
-class QProgressDialog;
-class QTranslator;
-class QTreeWidgetItem;
-class QUrl;
-class QSettings;
-class QWidget;
-
-#ifndef Q_MOC_RUN
-#include <boost/signals2.hpp>
-#endif
-
-//Sigh - just for HANDLE
-#define WIN32_LEAN_AND_MEAN
-#include <Windows.h>
-
-#include <functional>
-#include <set>
-#include <string>
-#include <vector>
-
-namespace Ui {
- class MainWindow;
-}
-
-
-
-class MainWindow : public QMainWindow, public IUserInterface
-{
- Q_OBJECT
-
- friend class OrganizerProxy;
-
-public:
-
- explicit MainWindow(QSettings &initSettings,
- OrganizerCore &organizerCore, PluginContainer &pluginContainer,
- QWidget *parent = 0);
- ~MainWindow();
-
- void storeSettings(QSettings &settings) override;
- void readSettings();
- void processUpdates();
-
- virtual ILockedWaitingForProcess* lock() override;
- virtual void unlock() override;
-
- bool addProfile();
- void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives);
- void refreshDataTree();
- void refreshDataTreeKeepExpandedNodes();
- void refreshSaveList();
-
- void setModListSorting(int index);
- void setESPListSorting(int index);
-
- void saveArchiveList();
-
- void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr);
- void registerPluginTools(std::vector<MOBase::IPluginTool *> toolPlugins);
- void registerModPage(MOBase::IPluginModPage *modPage);
-
- void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info);
-
- void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite);
- std::string readFromPipe(HANDLE stdOutRead);
- void processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog);
-
- void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
-
- QString getOriginDisplayName(int originID);
-
- void installTranslator(const QString &name);
-
- virtual void disconnectPlugins();
-
- void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab);
-
- virtual bool closeWindow();
- virtual void setWindowEnabled(bool enabled);
-
- virtual MOBase::DelayedFileWriterBase &archivesWriter() override { return m_ArchiveListWriter; }
-
-public slots:
-
- void displayColumnSelection(const QPoint &pos);
-
- void modorder_changed();
- void refresher_progress(int percent);
- void directory_refreshed();
-
- void toolPluginInvoke();
- void modPagePluginInvoke();
-
-signals:
-
- /**
- * @brief emitted after the information dialog has been closed
- */
- void modInfoDisplayed();
-
- /**
- * @brief emitted when the selected style changes
- */
- void styleChanged(const QString &styleFile);
-
-
- void modListDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
-
-protected:
-
- virtual void showEvent(QShowEvent *event);
- virtual void closeEvent(QCloseEvent *event);
- virtual bool eventFilter(QObject *obj, QEvent *event);
- virtual void resizeEvent(QResizeEvent *event);
- virtual void dragEnterEvent(QDragEnterEvent *event);
- virtual void dropEvent(QDropEvent *event);
-
-private slots:
- void on_actionChange_Game_triggered();
-
-private slots:
- void on_clickBlankButton_clicked();
-
-private:
-
- void cleanup();
-
- void actionToToolButton(QAction *&sourceAction);
-
- void updateToolBar();
- void activateSelectedProfile();
-
- void setExecutableIndex(int index);
-
- void startSteam();
-
- void updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const MOShared::DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon);
- bool refreshProfiles(bool selectProfile = true);
- void refreshExecutablesList();
- void installMod(QString fileName = "");
-
- 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 testExtractBSA(int modIndex);
-
- void writeDataToFile(QFile &file, const QString &directory, const MOShared::DirectoryEntry &directoryEntry);
-
- void refreshFilters();
-
- /**
- * Sets category selections from menu; for multiple mods, this will only apply
- * the changes made in the menu (which is the delta between the current menu selection and the reference mod)
- * @param menu the menu after editing by the user
- * @param modRow index of the mod to edit
- * @param referenceRow row of the reference mod
- */
- void addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow);
-
- /**
- * Sets category selections from menu; for multiple mods, this will completely
- * replace the current set of categories on each selected with those selected in the menu
- * @param menu the menu after editing by the user
- * @param modRow index of the mod to edit
- */
- void replaceCategoriesFromMenu(QMenu *menu, int modRow);
-
- bool populateMenuCategories(QMenu *menu, int targetID);
-
- void updateDownloadListDelegate();
-
- // remove invalid category-references from mods
- void fixCategories();
-
- 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);
-
- void setCategoryListVisible(bool visible);
-
- void displaySaveGameInfo(QListWidgetItem *newItem);
-
- HANDLE nextChildProcess();
-
- bool errorReported(QString &logFile);
-
- void updateESPLock(bool locked);
-
- static void setupNetworkProxy(bool activate);
- void activateProxy(bool activate);
- void setBrowserGeometry(const QByteArray &geometry);
-
- bool createBackup(const QString &filePath, const QDateTime &time);
- QString queryRestore(const QString &filePath);
-
- QMenu *modListContextMenu();
- void addModSendToContextMenu(QMenu *menu);
- void addPluginSendToContextMenu(QMenu *menu);
-
- QMenu *openFolderMenu();
-
- std::set<QString> enabledArchives();
-
- void scheduleUpdateButton();
-
- QDir currentSavesDir() const;
-
- void startMonitorSaves();
- void stopMonitorSaves();
-
- void dropLocalFile(const QUrl &url, const QString &outputDir, bool move);
-
- bool registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName = nullptr);
-
- void sendSelectedModsToPriority(int newPriority);
- void sendSelectedPluginsToPriority(int newPriority);
-
-private:
-
- static const char *PATTERN_BACKUP_GLOB;
- static const char *PATTERN_BACKUP_REGEX;
- static const char *PATTERN_BACKUP_DATE;
-
-private:
-
- Ui::MainWindow *ui;
-
- QAction *m_Sep; // Executable Shortcuts are added after this. Non owning.
-
- bool m_WasVisible;
-
- 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;
-
- QAbstractItemModel *m_ModListGroupingProxy;
- ModListSortProxy *m_ModListSortProxy;
-
- PluginListSortProxy *m_PluginListSortProxy;
-
- int m_OldExecutableIndex;
-
- int m_ContextRow;
- QPersistentModelIndex m_ContextIdx;
- QTreeWidgetItem *m_ContextItem;
- QAction *m_ContextAction;
-
- CategoryFactory &m_CategoryFactory;
-
- int m_ModsToUpdate;
-
- bool m_LoginAttempted;
-
- QTimer m_CheckBSATimer;
- QTimer m_SaveMetaTimer;
- QTimer m_UpdateProblemsTimer;
-
- QFuture<void> m_MetaSave;
-
- QTime m_StartTime;
- //SaveGameInfoWidget *m_CurrentSaveView;
- MOBase::ISaveGameInfoWidget *m_CurrentSaveView;
-
- OrganizerCore &m_OrganizerCore;
- PluginContainer &m_PluginContainer;
-
- QString m_CurrentLanguage;
- std::vector<QTranslator*> m_Translators;
-
- BrowserDialog m_IntegratedBrowser;
-
- QFileSystemWatcher m_SavesWatcher;
-
- std::vector<QTreeWidgetItem*> m_RemoveWidget;
-
- QByteArray m_ArchiveListHash;
-
- bool m_DidUpdateMasterList;
-
- LockedDialogBase *m_LockDialog { nullptr };
- uint64_t m_LockCount { 0 };
-
- bool m_closing{ false };
-
- std::vector<std::pair<QString, QHeaderView*>> m_PersistedGeometry;
-
- MOBase::DelayedFileWriter m_ArchiveListWriter;
-
- enum class ShortcutType {
- Toolbar,
- Desktop,
- StartMenu
- };
-
- void addWindowsLink(ShortcutType const);
-
- Executable const &getSelectedExecutable() const;
- Executable &getSelectedExecutable();
-
-private slots:
-
- void updateWindowTitle(const QString &accountName, bool premium);
-
- void showMessage(const QString &message);
- void showError(const QString &message);
-
-
- // main window actions
- void helpTriggered();
- void issueTriggered();
- void wikiTriggered();
- void tutorialTriggered();
- void extractBSATriggered();
-
- //modlist shortcuts
- void openExplorer_activated();
- void refreshProfile_activated();
-
- // modlist context menu
- void installMod_clicked();
- void createEmptyMod_clicked();
- void createSeparator_clicked();
- void restoreBackup_clicked();
- void renameMod_clicked();
- void removeMod_clicked();
- void setColor_clicked();
- void resetColor_clicked();
- void backupMod_clicked();
- void reinstallMod_clicked();
- void endorse_clicked();
- void dontendorse_clicked();
- void unendorse_clicked();
- void ignoreMissingData_clicked();
- void markConverted_clicked();
- void visitOnNexus_clicked();
- void visitWebPage_clicked();
- void openExplorer_clicked();
- void openOriginExplorer_clicked();
- void openOriginInformation_clicked();
- void information_clicked();
- void enableSelectedMods_clicked();
- void disableSelectedMods_clicked();
- void sendSelectedModsToTop_clicked();
- void sendSelectedModsToBottom_clicked();
- void sendSelectedModsToPriority_clicked();
- void sendSelectedModsToSeparator_clicked();
- // savegame context menu
- void deleteSavegame_clicked();
- void fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets);
- // data-tree context menu
- void writeDataToFile();
- void openDataFile();
- void addAsExecutable();
- void previewDataFile();
- void hideFile();
- void unhideFile();
-
- // pluginlist context menu
- void enableSelectedPlugins_clicked();
- void disableSelectedPlugins_clicked();
- void sendSelectedPluginsToTop_clicked();
- void sendSelectedPluginsToBottom_clicked();
- void sendSelectedPluginsToPriority_clicked();
-
- void linkToolbar();
- void linkDesktop();
- void linkMenu();
-
- void languageChange(const QString &newLanguage);
- void saveSelectionChanged(QListWidgetItem *newItem);
-
- void windowTutorialFinished(const QString &windowName);
-
- BSA::EErrorCode extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &extractProgress);
-
- void createModFromOverwrite();
- void clearOverwrite();
-
- void procError(QProcess::ProcessError error);
- void procFinished(int exitCode, QProcess::ExitStatus exitStatus);
-
- // nexus related
- void checkModsForUpdates();
-
- void loginFailed(const QString &message);
-
- void linkClicked(const QString &url);
-
- void updateAvailable();
-
- void motdReceived(const QString &motd);
- void notEndorsedYet();
-
- void originModified(int originID);
-
- void addRemoveCategories_MenuHandler();
- void replaceCategories_MenuHandler();
-
- void addPrimaryCategoryCandidates();
-
- void modDetailsUpdated(bool success);
-
- void modInstalled(const QString &modName);
-
- void nxmUpdatesAvailable(const std::vector<int> &modIDs, QVariant userData, QVariant resultData, int requestID);
- void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int);
- void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
- void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, int requestID, const QString &errorString);
-
- void editCategories();
- void deselectFilters();
-
- void displayModInformation(const QString &modName, int tab);
- void modOpenNext(int tab=-1);
- void modOpenPrev(int tab=-1);
-
- void modRenamed(const QString &oldName, const QString &newName);
- void modRemoved(const QString &fileName);
-
- void hideSaveGameInfo();
-
- void hookUpWindowTutorials();
-
- void resumeDownload(int downloadIndex);
- void endorseMod(ModInfo::Ptr mod);
- void cancelModListEditor();
-
- void lockESPIndex();
- void unlockESPIndex();
-
- void enableVisibleMods();
- void disableVisibleMods();
- void exportModListCSV();
- void openInstanceFolder();
- void openLogsFolder();
- void openInstallFolder();
- void openPluginsFolder();
- void openDownloadsFolder();
- void openModsFolder();
- void openProfileFolder();
- void openIniFolder();
- void openGameFolder();
- void openMyGamesFolder();
- void startExeAction();
-
- void checkBSAList();
-
- void updateProblemsButton();
-
- void saveModMetas();
-
- void updateStyle(const QString &style);
-
- void modlistChanged(const QModelIndex &index, int role);
- void fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName);
-
-
- void modFilterActive(bool active);
- void espFilterChanged(const QString &filter);
- void downloadFilterChanged(const QString &filter);
-
- void expandModList(const QModelIndex &index);
-
- /**
- * @brief resize columns in mod list and plugin list to content
- */
- void resizeLists(bool modListCustom, bool pluginListCustom);
-
- /**
- * @brief allow columns in mod list and plugin list to be resized
- */
- void allowListResize();
-
- void toolBar_customContextMenuRequested(const QPoint &point);
- void removeFromToolbar();
- void overwriteClosed(int);
-
- void changeVersioningScheme();
- void ignoreUpdate();
- void unignoreUpdate();
-
- void refreshSavesIfOpen();
- void expandDataTreeItem(QTreeWidgetItem *item);
- void about();
- void delayedRemove();
-
- void modlistSelectionChanged(const QModelIndex &current, const QModelIndex &previous);
- void modListSortIndicatorChanged(int column, Qt::SortOrder order);
-
- void modlistSelectionsChanged(const QItemSelection &current);
- void esplistSelectionsChanged(const QItemSelection &current);
-
- void search_activated();
- void searchClear_activated();
-
-private slots: // ui slots
- // actions
- void on_actionAdd_Profile_triggered();
- void on_actionInstallMod_triggered();
- void on_actionModify_Executables_triggered();
- void on_actionNexus_triggered();
- void on_actionProblems_triggered();
- void on_actionSettings_triggered();
- void on_actionUpdate_triggered();
- void on_actionEndorseMO_triggered();
-
- void on_bsaList_customContextMenuRequested(const QPoint &pos);
- void on_clearFiltersButton_clicked();
- void on_btnRefreshData_clicked();
- void on_btnRefreshDownloads_clicked();
- void on_categoriesList_customContextMenuRequested(const QPoint &pos);
- void on_conflictsCheckBox_toggled(bool checked);
- void on_dataTree_customContextMenuRequested(const QPoint &pos);
- void on_executablesListBox_currentIndexChanged(int index);
- void on_modList_customContextMenuRequested(const QPoint &pos);
- void on_modList_doubleClicked(const QModelIndex &index);
- void on_listOptionsBtn_pressed();
- void on_espList_doubleClicked(const QModelIndex &index);
- void on_profileBox_currentIndexChanged(int index);
- void on_savegameList_customContextMenuRequested(const QPoint &pos);
- void on_startButton_clicked();
- void on_tabWidget_currentChanged(int index);
-
- void on_espList_customContextMenuRequested(const QPoint &pos);
- void on_displayCategoriesBtn_toggled(bool checked);
- void on_groupCombo_currentIndexChanged(int index);
- void on_categoriesList_itemSelectionChanged();
- void on_linkButton_pressed();
- void on_showHiddenBox_toggled(bool checked);
- void on_bsaList_itemChanged(QTreeWidgetItem *item, int column);
- void on_bossButton_clicked();
-
- void on_saveButton_clicked();
- void on_restoreButton_clicked();
- void on_restoreModsButton_clicked();
- void on_saveModsButton_clicked();
- void on_actionCopy_Log_to_Clipboard_triggered();
- void on_categoriesAndBtn_toggled(bool checked);
- void on_categoriesOrBtn_toggled(bool checked);
- void on_managedArchiveLabel_linkHovered(const QString &link);
-};
-
-
-
-#endif // MAINWINDOW_H
+/*
+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/>.
+*/
+
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include "bsafolder.h"
+#include "browserdialog.h"
+#include "delayedfilewriter.h"
+#include "errorcodes.h"
+#include "imoinfo.h"
+#include "iuserinterface.h"
+#include "modinfo.h"
+#include "modlistsortproxy.h"
+#include "savegameinfo.h"
+#include "tutorialcontrol.h"
+
+//Note the commented headers here can be replaced with forward references,
+//when I get round to cleaning up main.cpp
+struct Executable;
+class CategoryFactory;
+class LockedDialogBase;
+class OrganizerCore;
+#include "plugincontainer.h" //class PluginContainer;
+class PluginListSortProxy;
+namespace BSA { class Archive; }
+#include "iplugingame.h" //namespace MOBase { class IPluginGame; }
+namespace MOBase { class IPluginModPage; }
+namespace MOBase { class IPluginTool; }
+namespace MOBase { class ISaveGame; }
+
+namespace MOShared { class DirectoryEntry; }
+
+#include <QByteArray>
+#include <QDir>
+#include <QFileInfo>
+#include <QFileSystemWatcher>
+#include <QList>
+#include <QMainWindow>
+#include <QObject>
+#include <QPersistentModelIndex>
+#include <QProcess>
+#include <QString>
+#include <QStringList>
+#include <QTime>
+#include <QTimer>
+#include <QHeaderView>
+#include <QVariant>
+#include <Qt>
+#include <QtConcurrent/QtConcurrentRun>
+#include <QFutureWatcher>
+
+class QAction;
+class QAbstractItemModel;
+class QDateTime;
+class QEvent;
+class QFile;
+class QListWidgetItem;
+class QMenu;
+class QModelIndex;
+class QPoint;
+class QProgressBar;
+class QProgressDialog;
+class QTranslator;
+class QTreeWidgetItem;
+class QUrl;
+class QSettings;
+class QWidget;
+
+#ifndef Q_MOC_RUN
+#include <boost/signals2.hpp>
+#endif
+
+//Sigh - just for HANDLE
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+
+#include <functional>
+#include <set>
+#include <string>
+#include <vector>
+
+namespace Ui {
+ class MainWindow;
+}
+
+
+
+class MainWindow : public QMainWindow, public IUserInterface
+{
+ Q_OBJECT
+
+ friend class OrganizerProxy;
+
+public:
+
+ explicit MainWindow(QSettings &initSettings,
+ OrganizerCore &organizerCore, PluginContainer &pluginContainer,
+ QWidget *parent = 0);
+ ~MainWindow();
+
+ void storeSettings(QSettings &settings) override;
+ void readSettings();
+ void processUpdates();
+
+ virtual ILockedWaitingForProcess* lock() override;
+ virtual void unlock() override;
+
+ bool addProfile();
+ void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives);
+ void refreshDataTree();
+ void refreshDataTreeKeepExpandedNodes();
+ void refreshSaveList();
+
+ void setModListSorting(int index);
+ void setESPListSorting(int index);
+
+ void saveArchiveList();
+
+ void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr);
+ void registerPluginTools(std::vector<MOBase::IPluginTool *> toolPlugins);
+ void registerModPage(MOBase::IPluginModPage *modPage);
+
+ void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info);
+
+ void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite);
+ std::string readFromPipe(HANDLE stdOutRead);
+ void processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog);
+
+ void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
+
+ QString getOriginDisplayName(int originID);
+
+ void installTranslator(const QString &name);
+
+ virtual void disconnectPlugins();
+
+ void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab);
+
+ virtual bool closeWindow();
+ virtual void setWindowEnabled(bool enabled);
+
+ virtual MOBase::DelayedFileWriterBase &archivesWriter() override { return m_ArchiveListWriter; }
+
+public slots:
+
+ void displayColumnSelection(const QPoint &pos);
+
+ void modorder_changed();
+ void esplist_changed();
+ void refresher_progress(int percent);
+ void directory_refreshed();
+
+ void toolPluginInvoke();
+ void modPagePluginInvoke();
+
+signals:
+
+ /**
+ * @brief emitted after the information dialog has been closed
+ */
+ void modInfoDisplayed();
+
+ /**
+ * @brief emitted when the selected style changes
+ */
+ void styleChanged(const QString &styleFile);
+
+
+ void modListDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
+
+protected:
+
+ virtual void showEvent(QShowEvent *event);
+ virtual void closeEvent(QCloseEvent *event);
+ virtual bool eventFilter(QObject *obj, QEvent *event);
+ virtual void resizeEvent(QResizeEvent *event);
+ virtual void dragEnterEvent(QDragEnterEvent *event);
+ virtual void dropEvent(QDropEvent *event);
+
+private slots:
+ void on_actionChange_Game_triggered();
+
+private slots:
+ void on_clickBlankButton_clicked();
+
+private:
+
+ void cleanup();
+
+ void actionToToolButton(QAction *&sourceAction);
+
+ void updateToolBar();
+ void activateSelectedProfile();
+
+ void setExecutableIndex(int index);
+
+ void startSteam();
+
+ void updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const MOShared::DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon);
+ bool refreshProfiles(bool selectProfile = true);
+ void refreshExecutablesList();
+ void installMod(QString fileName = "");
+
+ 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 testExtractBSA(int modIndex);
+
+ void writeDataToFile(QFile &file, const QString &directory, const MOShared::DirectoryEntry &directoryEntry);
+
+ void refreshFilters();
+
+ /**
+ * Sets category selections from menu; for multiple mods, this will only apply
+ * the changes made in the menu (which is the delta between the current menu selection and the reference mod)
+ * @param menu the menu after editing by the user
+ * @param modRow index of the mod to edit
+ * @param referenceRow row of the reference mod
+ */
+ void addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow);
+
+ /**
+ * Sets category selections from menu; for multiple mods, this will completely
+ * replace the current set of categories on each selected with those selected in the menu
+ * @param menu the menu after editing by the user
+ * @param modRow index of the mod to edit
+ */
+ void replaceCategoriesFromMenu(QMenu *menu, int modRow);
+
+ bool populateMenuCategories(QMenu *menu, int targetID);
+
+ void initDownloadView();
+ void updateDownloadView();
+
+ // 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);
+
+ void setCategoryListVisible(bool visible);
+
+ void displaySaveGameInfo(QListWidgetItem *newItem);
+
+ HANDLE nextChildProcess();
+
+ bool errorReported(QString &logFile);
+
+ void updateESPLock(bool locked);
+
+ static void setupNetworkProxy(bool activate);
+ void activateProxy(bool activate);
+ void setBrowserGeometry(const QByteArray &geometry);
+
+ bool createBackup(const QString &filePath, const QDateTime &time);
+ QString queryRestore(const QString &filePath);
+
+ void initModListContextMenu(QMenu *menu);
+ void addModSendToContextMenu(QMenu *menu);
+ void addPluginSendToContextMenu(QMenu *menu);
+
+ QMenu *openFolderMenu();
+
+ std::set<QString> enabledArchives();
+
+ void scheduleUpdateButton();
+
+ QDir currentSavesDir() const;
+
+ void startMonitorSaves();
+ void stopMonitorSaves();
+
+ void dropLocalFile(const QUrl &url, const QString &outputDir, bool move);
+
+ bool registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName = nullptr);
+
+ void sendSelectedModsToPriority(int newPriority);
+ void sendSelectedPluginsToPriority(int newPriority);
+
+ void toggleMO2EndorseState();
+
+private:
+
+ static const char *PATTERN_BACKUP_GLOB;
+ static const char *PATTERN_BACKUP_REGEX;
+ static const char *PATTERN_BACKUP_DATE;
+
+private:
+
+ Ui::MainWindow *ui;
+
+ QAction *m_Sep; // Executable Shortcuts are added after this. Non owning.
+
+ bool m_WasVisible;
+
+ 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;
+
+ QAbstractItemModel *m_ModListGroupingProxy;
+ ModListSortProxy *m_ModListSortProxy;
+
+ PluginListSortProxy *m_PluginListSortProxy;
+
+ int m_OldExecutableIndex;
+
+ int m_ContextRow;
+ QPersistentModelIndex m_ContextIdx;
+ QTreeWidgetItem *m_ContextItem;
+ QAction *m_ContextAction;
+
+ CategoryFactory &m_CategoryFactory;
+
+ bool m_LoginAttempted;
+
+ QTimer m_CheckBSATimer;
+ QTimer m_SaveMetaTimer;
+ QTimer m_UpdateProblemsTimer;
+
+ QFuture<void> m_MetaSave;
+
+ QTime m_StartTime;
+ //SaveGameInfoWidget *m_CurrentSaveView;
+ MOBase::ISaveGameInfoWidget *m_CurrentSaveView;
+
+ OrganizerCore &m_OrganizerCore;
+ PluginContainer &m_PluginContainer;
+
+ QString m_CurrentLanguage;
+ std::vector<QTranslator*> m_Translators;
+
+ BrowserDialog m_IntegratedBrowser;
+
+ QFileSystemWatcher m_SavesWatcher;
+
+ std::vector<QTreeWidgetItem*> m_RemoveWidget;
+
+ QByteArray m_ArchiveListHash;
+
+ bool m_DidUpdateMasterList;
+
+ LockedDialogBase *m_LockDialog { nullptr };
+ uint64_t m_LockCount { 0 };
+
+ bool m_closing{ false };
+
+ bool m_showArchiveData{ true };
+
+ std::vector<std::pair<QString, QHeaderView*>> m_PersistedGeometry;
+
+ MOBase::DelayedFileWriter m_ArchiveListWriter;
+
+ enum class ShortcutType {
+ Toolbar,
+ Desktop,
+ StartMenu
+ };
+
+ void addWindowsLink(ShortcutType const);
+
+ Executable const &getSelectedExecutable() const;
+ Executable &getSelectedExecutable();
+
+private slots:
+
+ void updateWindowTitle(const QString &accountName, bool premium);
+
+ void showMessage(const QString &message);
+ void showError(const QString &message);
+
+
+ // main window actions
+ void helpTriggered();
+ void issueTriggered();
+ void wikiTriggered();
+ void discordTriggered();
+ void tutorialTriggered();
+ void extractBSATriggered();
+
+ //modlist shortcuts
+ void openExplorer_activated();
+ void refreshProfile_activated();
+
+ // modlist context menu
+ void installMod_clicked();
+ void createEmptyMod_clicked();
+ void createSeparator_clicked();
+ void restoreBackup_clicked();
+ void renameMod_clicked();
+ void removeMod_clicked();
+ void setColor_clicked();
+ void resetColor_clicked();
+ void backupMod_clicked();
+ void reinstallMod_clicked();
+ void endorse_clicked();
+ void dontendorse_clicked();
+ void unendorse_clicked();
+ void track_clicked();
+ void untrack_clicked();
+ void ignoreMissingData_clicked();
+ void markConverted_clicked();
+ void visitOnNexus_clicked();
+ void visitWebPage_clicked();
+ void openExplorer_clicked();
+ void openOriginExplorer_clicked();
+ void openOriginInformation_clicked();
+ void information_clicked();
+ void enableSelectedMods_clicked();
+ void disableSelectedMods_clicked();
+ void sendSelectedModsToTop_clicked();
+ void sendSelectedModsToBottom_clicked();
+ void sendSelectedModsToPriority_clicked();
+ void sendSelectedModsToSeparator_clicked();
+ // savegame context menu
+ void deleteSavegame_clicked();
+ void fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets);
+ // data-tree context menu
+ void writeDataToFile();
+ void openDataFile();
+ void addAsExecutable();
+ void previewDataFile();
+ void hideFile();
+ void unhideFile();
+
+ // pluginlist context menu
+ void enableSelectedPlugins_clicked();
+ void disableSelectedPlugins_clicked();
+ void sendSelectedPluginsToTop_clicked();
+ void sendSelectedPluginsToBottom_clicked();
+ void sendSelectedPluginsToPriority_clicked();
+
+ void linkToolbar();
+ void linkDesktop();
+ void linkMenu();
+
+ void languageChange(const QString &newLanguage);
+ void saveSelectionChanged(QListWidgetItem *newItem);
+
+ void windowTutorialFinished(const QString &windowName);
+
+ BSA::EErrorCode extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &extractProgress);
+
+ void createModFromOverwrite();
+ /**
+ * @brief sends the content of the overwrite folder to an already existing mod
+ */
+ void moveOverwriteContentToExistingMod();
+ /**
+ * @brief actually sends the content of the overwrite folder to specified mod
+ */
+ void doMoveOverwriteContentToMod(const QString &modAbsolutePath);
+ void clearOverwrite();
+
+ void procError(QProcess::ProcessError error);
+ void procFinished(int exitCode, QProcess::ExitStatus exitStatus);
+
+ // nexus related
+ void checkModsForUpdates();
+
+ void validationFailed(const QString &message);
+
+ void linkClicked(const QString &url);
+
+ void updateAvailable();
+
+ void actionEndorseMO();
+ void actionWontEndorseMO();
+
+ void motdReceived(const QString &motd);
+
+ void originModified(int originID);
+
+ void addRemoveCategories_MenuHandler();
+ void replaceCategories_MenuHandler();
+
+ void addPrimaryCategoryCandidates();
+
+ void modInstalled(const QString &modName);
+
+ void modUpdateCheck(std::multimap<QString, int> IDs);
+
+ void finishUpdateInfo();
+
+ void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int);
+ void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID);
+ void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int);
+ void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int);
+ 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 editCategories();
+ void deselectFilters();
+
+ void displayModInformation(const QString &modName, int tab);
+ void modOpenNext(int tab=-1);
+ void modOpenPrev(int tab=-1);
+
+ void modRenamed(const QString &oldName, const QString &newName);
+ void modRemoved(const QString &fileName);
+
+ void hideSaveGameInfo();
+
+ void hookUpWindowTutorials();
+
+ void resumeDownload(int downloadIndex);
+ void endorseMod(ModInfo::Ptr mod);
+ void unendorseMod(ModInfo::Ptr mod);
+ void trackMod(ModInfo::Ptr mod, bool doTrack);
+ void cancelModListEditor();
+
+ void lockESPIndex();
+ void unlockESPIndex();
+
+ void enableVisibleMods();
+ void disableVisibleMods();
+ void exportModListCSV();
+ void openInstanceFolder();
+ void openLogsFolder();
+ void openInstallFolder();
+ void openPluginsFolder();
+ void openDownloadsFolder();
+ void openModsFolder();
+ void openProfileFolder();
+ void openIniFolder();
+ void openGameFolder();
+ void openMyGamesFolder();
+ void startExeAction();
+
+ void checkBSAList();
+
+ void updateProblemsButton();
+
+ void saveModMetas();
+
+ void updateStyle(const QString &style);
+
+ void modlistChanged(const QModelIndex &index, int role);
+ void modlistChanged(const QModelIndexList &indicies, int role);
+ void fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName);
+
+
+ void modFilterActive(bool active);
+ void espFilterChanged(const QString &filter);
+ void downloadFilterChanged(const QString &filter);
+
+ void expandModList(const QModelIndex &index);
+
+ /**
+ * @brief resize columns in mod list and plugin list to content
+ */
+ void resizeLists(bool modListCustom, bool pluginListCustom);
+
+ /**
+ * @brief allow columns in mod list and plugin list to be resized
+ */
+ void allowListResize();
+
+ void toolBar_customContextMenuRequested(const QPoint &point);
+ void removeFromToolbar();
+ void overwriteClosed(int);
+
+ void changeVersioningScheme();
+ void checkModUpdates_clicked();
+ void ignoreUpdate();
+ void unignoreUpdate();
+
+ void refreshSavesIfOpen();
+ void expandDataTreeItem(QTreeWidgetItem *item);
+ void about();
+ void delayedRemove();
+
+ void modListSortIndicatorChanged(int column, Qt::SortOrder order);
+ void modListSectionResized(int logicalIndex, int oldSize, int newSize);
+
+ void modlistSelectionsChanged(const QItemSelection &current);
+ void esplistSelectionsChanged(const QItemSelection &current);
+
+ void search_activated();
+ void searchClear_activated();
+
+ void updateModCount();
+ void updatePluginCount();
+
+private slots: // ui slots
+ // actions
+ void on_actionAdd_Profile_triggered();
+ void on_actionInstallMod_triggered();
+ void on_actionModify_Executables_triggered();
+ void on_actionNexus_triggered();
+ void on_actionNotifications_triggered();
+ void on_actionSettings_triggered();
+ void on_actionUpdate_triggered();
+
+ void on_bsaList_customContextMenuRequested(const QPoint &pos);
+ void on_clearFiltersButton_clicked();
+ void on_btnRefreshData_clicked();
+ void on_btnRefreshDownloads_clicked();
+ void on_categoriesList_customContextMenuRequested(const QPoint &pos);
+ void on_conflictsCheckBox_toggled(bool checked);
+ void on_showArchiveDataCheckBox_toggled(bool checked);
+ void on_dataTree_customContextMenuRequested(const QPoint &pos);
+ void on_executablesListBox_currentIndexChanged(int index);
+ void on_modList_customContextMenuRequested(const QPoint &pos);
+ void on_modList_doubleClicked(const QModelIndex &index);
+ void on_listOptionsBtn_pressed();
+ void on_espList_doubleClicked(const QModelIndex &index);
+ void on_profileBox_currentIndexChanged(int index);
+ void on_savegameList_customContextMenuRequested(const QPoint &pos);
+ void on_startButton_clicked();
+ void on_tabWidget_currentChanged(int index);
+
+ void on_espList_customContextMenuRequested(const QPoint &pos);
+ void on_displayCategoriesBtn_toggled(bool checked);
+ void on_groupCombo_currentIndexChanged(int index);
+ void on_categoriesList_itemSelectionChanged();
+ void on_linkButton_pressed();
+ void on_showHiddenBox_toggled(bool checked);
+ void on_bsaList_itemChanged(QTreeWidgetItem *item, int column);
+ void on_bossButton_clicked();
+
+ void on_saveButton_clicked();
+ void on_restoreButton_clicked();
+ void on_restoreModsButton_clicked();
+ void on_saveModsButton_clicked();
+ void on_actionCopy_Log_to_Clipboard_triggered();
+ void on_categoriesAndBtn_toggled(bool checked);
+ void on_categoriesOrBtn_toggled(bool checked);
+ void on_managedArchiveLabel_linkHovered(const QString &link);
+};
+
+
+
+#endif // MAINWINDOW_H
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index e42bed27..d876b54a 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -1,1628 +1,1601 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>MainWindow</class>
- <widget class="QMainWindow" name="MainWindow">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>926</width>
- <height>710</height>
- </rect>
- </property>
- <property name="acceptDrops">
- <bool>true</bool>
- </property>
- <property name="windowTitle">
- <string notr="true">Mod Organizer</string>
- </property>
- <property name="windowIcon">
- <iconset>
- <normaloff>mo_icon.ico</normaloff>mo_icon.ico</iconset>
- </property>
- <property name="documentMode">
- <bool>false</bool>
- </property>
- <widget class="QWidget" name="centralWidget">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout_8">
- <item>
- <widget class="QSplitter" name="topLevelSplitter">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <widget class="QWidget" name="horizontalLayoutWidget_2">
- <layout class="QHBoxLayout" name="horizontalLayout_10" stretch="0,2">
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_8">
- <item>
- <widget class="QGroupBox" name="categoriesGroup">
- <property name="title">
- <string>Categories</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_10" stretch="6,0,0">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="leftMargin">
- <number>3</number>
- </property>
- <property name="topMargin">
- <number>7</number>
- </property>
- <property name="rightMargin">
- <number>3</number>
- </property>
- <property name="bottomMargin">
- <number>1</number>
- </property>
- <item>
- <widget class="QTreeWidget" name="categoriesList">
- <property name="minimumSize">
- <size>
- <width>120</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>214</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <attribute name="headerVisible">
- <bool>false</bool>
- </attribute>
- <column>
- <property name="text">
- <string notr="true">1</string>
- </property>
- </column>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="clickBlankButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="text">
- <string>Clear</string>
- </property>
- <property name="flat">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="widget" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout_11">
- <item>
- <widget class="QRadioButton" name="categoriesAndBtn">
- <property name="toolTip">
- <string>If checked, only mods that match all selected categories are displayed.</string>
- </property>
- <property name="text">
- <string>And</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QRadioButton" name="categoriesOrBtn">
- <property name="toolTip">
- <string>If checked, all mods that match at least one of the selected categories are displayed.</string>
- </property>
- <property name="text">
- <string>Or</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QSplitter" name="splitter">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <widget class="QWidget" name="layoutWidget">
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>2</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,1,0,0,0,0,0">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Profile</string>
- </property>
- <property name="buddy">
- <cstring>profileBox</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="profileBox">
- <property name="toolTip">
- <string>Pick a module collection</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;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.&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Please note that right now your esp load order is not kept seperate for different profiles.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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="listOptionsBtn">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Open list options...</string>
- </property>
- <property name="whatsThis">
- <string>Refresh list. This is usually not necessary unless you modified data outside the program.</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>16</width>
- <height>16</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="openFolderMenu">
- <property name="toolTip">
- <string>Show Open Folders menu...</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/open_folder</normaloff>:/MO/gui/open_folder</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="restoreModsButton">
- <property name="toolTip">
- <string>Restore Backup...</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="saveModsButton">
- <property name="toolTip">
- <string>Create Backup</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="ModListView" name="modList">
- <property name="minimumSize">
- <size>
- <width>330</width>
- <height>400</height>
- </size>
- </property>
- <property name="palette">
- <palette>
- <active>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>64</red>
- <green>64</green>
- <blue>64</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Link">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="LinkVisited">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>170</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>64</red>
- <green>64</green>
- <blue>64</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Link">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="LinkVisited">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>170</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>120</red>
- <green>120</green>
- <blue>120</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Link">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="LinkVisited">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>170</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>List of available mods.</string>
- </property>
- <property name="whatsThis">
- <string>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag &amp; drop mods to change their &quot;installation&quot; orders.</string>
- </property>
- <property name="styleSheet">
- <string notr="true"/>
- </property>
- <property name="editTriggers">
- <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>true</bool>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="indentation">
- <number>20</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="itemsExpandable">
- <bool>true</bool>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- <property name="expandsOnDoubleClick">
- <bool>false</bool>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>20</number>
- </attribute>
- <attribute name="headerShowSortIndicator" stdset="0">
- <bool>true</bool>
- </attribute>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,1,1,1">
- <item>
- <widget class="QPushButton" name="displayCategoriesBtn">
- <property name="maximumSize">
- <size>
- <width>20</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string notr="true">x</string>
- </property>
- <property name="iconSize">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="currentCategoryLabel">
- <property name="font">
- <font>
- <pointsize>8</pointsize>
- <italic>true</italic>
- </font>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_5">
- <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 alignment="Qt::AlignLeft">
- <widget class="QPushButton" name="clearFiltersButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>22</height>
- </size>
- </property>
- <property name="baseSize">
- <size>
- <width>95</width>
- <height>0</height>
- </size>
- </property>
- <property name="visible">
- <bool>false</bool>
- </property>
- <property name="layoutDirection">
- <enum>Qt::RightToLeft</enum>
- </property>
- <property name="styleSheet">
- <string notr="true">border:1px solid #ff0000;</string>
- </property>
- <property name="text">
- <string>Clear all Filters</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>12</width>
- <height>12</height>
- </size>
- </property>
- </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>
- <width>220</width>
- <height>0</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::ClickFocus</enum>
- </property>
- <item>
- <property name="text">
- <string>No groups</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Categories</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Nexus IDs</string>
- </property>
- </item>
- </widget>
- </item>
- <item>
- <widget class="MOBase::LineEditClear" name="modFilterEdit">
- <property name="baseSize">
- <size>
- <width>220</width>
- <height>0</height>
- </size>
- </property>
- <property name="placeholderText">
- <string>Namefilter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="layoutWidget_2">
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QFrame" name="startGroup">
- <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
- <item>
- <widget class="QComboBox" name="executablesListBox">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>40</height>
- </size>
- </property>
- <property name="font">
- <font>
- <pointsize>9</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="toolTip">
- <string>Pick a program to run.</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;You can add new Tools to this list, but I can't promise tools I haven't tested will work.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="iconSize">
- <size>
- <width>32</width>
- <height>32</height>
- </size>
- </property>
- <property name="frame">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
- <item>
- <widget class="QPushButton" name="startButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>120</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="font">
- <font>
- <pointsize>10</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="toolTip">
- <string>Run program</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Run the selected program with ModOrganizer enabled.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="locale">
- <locale language="English" country="UnitedStates"/>
- </property>
- <property name="text">
- <string>Run</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/run</normaloff>:/MO/gui/run</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>36</width>
- <height>36</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="linkButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>140</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="baseSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Create a shortcut in your start menu or on the desktop to the specified program</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This creates a start menu shortcut that directly starts the selected program with the MO active.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>Shortcut</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/link</normaloff>:/MO/gui/link</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QTabWidget" name="tabWidget">
- <property name="minimumSize">
- <size>
- <width>340</width>
- <height>250</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::NoContextMenu</enum>
- </property>
- <property name="tabShape">
- <enum>QTabWidget::Rounded</enum>
- </property>
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="espTab">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <attribute name="title">
- <string>Plugins</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <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>
- <layout class="QHBoxLayout" name="horizontalLayout_7">
- <item>
- <widget class="QPushButton" name="bossButton">
- <property name="text">
- <string>Sort</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/sort</normaloff>:/MO/gui/sort</iconset>
- </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="restoreButton">
- <property name="toolTip">
- <string>Restore Backup...</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>16</width>
- <height>16</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="saveButton">
- <property name="toolTip">
- <string>Create Backup</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="PluginListView" name="espList">
- <property name="minimumSize">
- <size>
- <width>250</width>
- <height>250</height>
- </size>
- </property>
- <property name="palette">
- <palette>
- <active>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>64</red>
- <green>64</green>
- <blue>64</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>64</red>
- <green>64</green>
- <blue>64</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>120</red>
- <green>120</green>
- <blue>120</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>List of available esp/esm files</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.&lt;br /&gt;There is a great tool named &amp;quot;BOSS&amp;quot; to automatically sort these files.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="editTriggers">
- <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropOverwriteMode">
- <bool>false</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::InternalMove</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- <property name="expandsOnDoubleClick">
- <bool>false</bool>
- </property>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="MOBase::LineEditClear" name="espFilterEdit">
- <property name="text">
- <string/>
- </property>
- <property name="placeholderText">
- <string>Namefilter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="bsaTab">
- <property name="visible">
- <bool>false</bool>
- </property>
- <attribute name="title">
- <string>Archives</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_9">
- <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>6</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0">
- <item>
- <widget class="QLabel" name="managedArchiveLabel">
- <property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. &lt;p&gt;Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).&lt;/p&gt;&lt;p&gt;If there is a matching plugin, the game will load them no matter what.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;What is an archive?&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QTreeWidget" name="bsaList">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</string>
- </property>
- <property name="whatsThis">
- <string>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they &quot;compete&quot; 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!
-
- BSAs checked here are loaded in such a way that your installation order is obeyed properly.</string>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>false</bool>
- </property>
- <property name="dragEnabled">
- <bool>false</bool>
- </property>
- <property name="dragDropOverwriteMode">
- <bool>false</bool>
- </property>
- <property name="indentation">
- <number>20</number>
- </property>
- <property name="itemsExpandable">
- <bool>true</bool>
- </property>
- <property name="columnCount">
- <number>1</number>
- </property>
- <column>
- <property name="text">
- <string notr="true">Archives</string>
- </property>
- </column>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="dataTab">
- <attribute name="title">
- <string>Data</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <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>6</number>
- </property>
- <item>
- <widget class="QPushButton" name="btnRefreshData">
- <property name="toolTip">
- <string>refresh data-directory overview</string>
- </property>
- <property name="whatsThis">
- <string>Refresh the overview. This may take a moment.</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QTreeWidget" name="dataTree">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="whatsThis">
- <string>This is an overview of your data directory as visible to the game (and tools). </string>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="animated">
- <bool>true</bool>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>400</number>
- </attribute>
- <column>
- <property name="text">
- <string>File</string>
- </property>
- </column>
- <column>
- <property name="text">
- <string>Mod</string>
- </property>
- </column>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QCheckBox" name="conflictsCheckBox">
- <property name="toolTip">
- <string>Filter the above list so that only conflicts are displayed.</string>
- </property>
- <property name="whatsThis">
- <string>Filter the above list so that only conflicts are displayed.</string>
- </property>
- <property name="text">
- <string>Show only conflicts</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="savesTab">
- <attribute name="title">
- <string>Saves</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <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>6</number>
- </property>
- <item>
- <widget class="QListWidget" name="savegameList">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string notr="true"/>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This is a list of all save games for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;&quot;&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;If you click &amp;quot;Fix Mods...&amp;quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="downloadTab">
- <attribute name="title">
- <string>Downloads</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_7">
- <property name="leftMargin">
- <number>2</number>
- </property>
- <property name="topMargin">
- <number>2</number>
- </property>
- <property name="rightMargin">
- <number>2</number>
- </property>
- <property name="bottomMargin">
- <number>2</number>
- </property>
- <item>
- <widget class="QPushButton" name="btnRefreshDownloads">
- <property name="toolTip">
- <string>Refresh downloads view</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="downloadLayout">
- <item>
- <widget class="QTreeView" name="downloadView">
- <property name="minimumSize">
- <size>
- <width>320</width>
- <height>0</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::PreventContextMenu</enum>
- </property>
- <property name="acceptDrops">
- <bool>true</bool>
- </property>
- <property name="toolTip">
- <string/>
- </property>
- <property name="whatsThis">
- <string>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</string>
- </property>
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
- <property name="horizontalScrollBarPolicy">
- <enum>Qt::ScrollBarAsNeeded</enum>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::SingleSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="verticalScrollMode">
- <enum>QAbstractItemView::ScrollPerPixel</enum>
- </property>
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>50</number>
- </attribute>
- <attribute name="headerMinimumSectionSize">
- <number>15</number>
- </attribute>
- <attribute name="headerStretchLastSection">
- <bool>true</bool>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,2">
- <item>
- <widget class="QCheckBox" name="showHiddenBox">
- <property name="text">
- <string>Show Hidden</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="MOBase::LineEditClear" name="downloadFilterEdit">
- <property name="placeholderText">
- <string>Namefilter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </widget>
- </item>
- </layout>
- </widget>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QTreeView" name="logList">
- <property name="contextMenuPolicy">
- <enum>Qt::ActionsContextMenu</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::NoSelection</enum>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="headerHidden">
- <bool>true</bool>
- </property>
- </widget>
- </widget>
- </item>
- </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>
- </property>
- <property name="movable">
- <bool>false</bool>
- </property>
- <property name="iconSize">
- <size>
- <width>42</width>
- <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>
- <attribute name="toolBarBreak">
- <bool>false</bool>
- </attribute>
- <addaction name="actionChange_Game"/>
- <addaction name="actionInstallMod"/>
- <addaction name="actionNexus"/>
- <addaction name="actionAdd_Profile"/>
- <addaction name="actionModify_Executables"/>
- <addaction name="actionTool"/>
- <addaction name="actionSettings"/>
- <addaction name="separator"/>
- <addaction name="actionEndorseMO"/>
- <addaction name="actionProblems"/>
- <addaction name="actionUpdate"/>
- <addaction name="actionHelp"/>
- </widget>
- <widget class="QStatusBar" name="statusBar"/>
- <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>
- </property>
- <property name="iconText">
- <string>Install &amp;Mod</string>
- </property>
- <property name="toolTip">
- <string>Install a new mod from an archive</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+M</string>
- </property>
- </action>
- <action name="actionAdd_Profile">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/profiles</normaloff>:/MO/gui/profiles</iconset>
- </property>
- <property name="text">
- <string>Profiles</string>
- </property>
- <property name="iconText">
- <string>&amp;Profiles</string>
- </property>
- <property name="toolTip">
- <string>Configure Profiles</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+P</string>
- </property>
- </action>
- <action name="actionModify_Executables">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/icon_executable</normaloff>:/MO/gui/icon_executable</iconset>
- </property>
- <property name="text">
- <string>Executables</string>
- </property>
- <property name="iconText">
- <string>&amp;Executables</string>
- </property>
- <property name="toolTip">
- <string>Configure the executables that can be started through Mod Organizer</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+E</string>
- </property>
- </action>
- <action name="actionTool">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/plugins</normaloff>:/MO/gui/plugins</iconset>
- </property>
- <property name="text">
- <string>Tools</string>
- </property>
- <property name="iconText">
- <string>&amp;Tools</string>
- </property>
- <property name="toolTip">
- <string>Tools</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+I</string>
- </property>
- </action>
- <action name="actionSettings">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
- </property>
- <property name="text">
- <string>Settings</string>
- </property>
- <property name="iconText">
- <string>&amp;Settings</string>
- </property>
- <property name="toolTip">
- <string>Configure settings and workarounds</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+S</string>
- </property>
- </action>
- <action name="actionNexus">
- <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>
- <property name="text">
- <string>Nexus</string>
- </property>
- <property name="toolTip">
- <string>Search nexus network for more mods</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+N</string>
- </property>
- </action>
- <action name="actionUpdate">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/update</normaloff>:/MO/gui/update</iconset>
- </property>
- <property name="text">
- <string>Update</string>
- </property>
- <property name="toolTip">
- <string>Mod Organizer is up-to-date</string>
- </property>
- </action>
- <action name="actionProblems">
- <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 Problems</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.
-
-!Work in progress!
-Right now this has very limited functionality</string>
- </property>
- </action>
- <action name="actionHelp">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/help</normaloff>:/MO/gui/help</iconset>
- </property>
- <property name="text">
- <string>Help</string>
- </property>
- <property name="toolTip">
- <string>Help</string>
- </property>
- <property name="shortcut">
- <string>Ctrl+H</string>
- </property>
- </action>
- <action name="actionEndorseMO">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/icon_favorite</normaloff>:/MO/gui/icon_favorite</iconset>
- </property>
- <property name="text">
- <string>Endorse MO</string>
- </property>
- <property name="toolTip">
- <string>Endorse Mod Organizer</string>
- </property>
- </action>
- <action name="actionCopy_Log_to_Clipboard">
- <property name="text">
- <string>Copy Log to Clipboard</string>
- </property>
- </action>
- <action name="actionChange_Game">
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/instance_switch</normaloff>:/MO/gui/instance_switch</iconset>
- </property>
- <property name="text">
- <string>Change Game</string>
- </property>
- <property name="toolTip">
- <string>Open the Instance selection dialog to manage a different Game</string>
- </property>
- </action>
- </widget>
- <layoutdefault spacing="6" margin="11"/>
- <customwidgets>
- <customwidget>
- <class>MOBase::LineEditClear</class>
- <extends>QLineEdit</extends>
- <header>lineeditclear.h</header>
- </customwidget>
- <customwidget>
- <class>ModListView</class>
- <extends>QTreeView</extends>
- <header>modlistview.h</header>
- </customwidget>
- <customwidget>
- <class>PluginListView</class>
- <extends>QTreeView</extends>
- <header>pluginlistview.h</header>
- </customwidget>
- </customwidgets>
- <resources>
- <include location="resources.qrc"/>
- </resources>
- <connections/>
-</ui>
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>926</width>
+ <height>710</height>
+ </rect>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">Mod Organizer</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>mo_icon.ico</normaloff>mo_icon.ico</iconset>
+ </property>
+ <property name="documentMode">
+ <bool>false</bool>
+ </property>
+ <widget class="QWidget" name="centralWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QSplitter" name="topLevelSplitter">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <widget class="QWidget" name="horizontalLayoutWidget_2">
+ <layout class="QHBoxLayout" name="horizontalLayout_10" stretch="0,2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QGroupBox" name="categoriesGroup">
+ <property name="title">
+ <string>Categories</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10" stretch="6,0,0">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <property name="topMargin">
+ <number>7</number>
+ </property>
+ <property name="rightMargin">
+ <number>3</number>
+ </property>
+ <property name="bottomMargin">
+ <number>1</number>
+ </property>
+ <item>
+ <widget class="QTreeWidget" name="categoriesList">
+ <property name="minimumSize">
+ <size>
+ <width>120</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>214</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string notr="true">1</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="clickBlankButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Clear</string>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_11">
+ <item>
+ <widget class="QRadioButton" name="categoriesAndBtn">
+ <property name="toolTip">
+ <string>If checked, only mods that match all selected categories are displayed.</string>
+ </property>
+ <property name="text">
+ <string>And</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="categoriesOrBtn">
+ <property name="toolTip">
+ <string>If checked, all mods that match at least one of the selected categories are displayed.</string>
+ </property>
+ <property name="text">
+ <string>Or</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QSplitter" name="splitter">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <widget class="QWidget" name="layoutWidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,1,0,0,0,0,0,0,0">
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Profile</string>
+ </property>
+ <property name="buddy">
+ <cstring>profileBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="profileBox">
+ <property name="toolTip">
+ <string>Pick a module collection</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;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.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Please note that right now your esp load order is not kept seperate for different profiles.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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="listOptionsBtn">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Open list options...</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh list. This is usually not necessary unless you modified data outside the program.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="openFolderMenu">
+ <property name="toolTip">
+ <string>Show Open Folders menu...</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/open_folder</normaloff>:/MO/gui/open_folder</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="restoreModsButton">
+ <property name="toolTip">
+ <string>Restore Backup...</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="saveModsButton">
+ <property name="toolTip">
+ <string>Create Backup</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="activeModslabel">
+ <property name="text">
+ <string>Active:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="LCDNumber" name="activeModsCounter">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>26</height>
+ </size>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="digitCount">
+ <number>5</number>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="ModListView" name="modList">
+ <property name="minimumSize">
+ <size>
+ <width>330</width>
+ <height>400</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available mods.</string>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag &amp; drop mods to change their &quot;installation&quot; orders.</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>true</bool>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="indentation">
+ <number>20</number>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="expandsOnDoubleClick">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>35</number>
+ </attribute>
+ <attribute name="headerShowSortIndicator" stdset="0">
+ <bool>true</bool>
+ </attribute>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,0,1,1,1">
+ <item>
+ <widget class="QPushButton" name="displayCategoriesBtn">
+ <property name="maximumSize">
+ <size>
+ <width>20</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">x</string>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>20</width>
+ <height>20</height>
+ </size>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="currentCategoryLabel">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ <italic>true</italic>
+ </font>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <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="apiRequests">
+ <property name="toolTip">
+ <string>Nexus API Queued and Remaining Requests</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This tracks the number of queued Nexus API requests on the left (&lt;span style=&quot; font-weight:600;&quot;&gt;Q&lt;/span&gt;) and the remaining daily (&lt;span style=&quot; font-weight:600;&quot;&gt;D&lt;/span&gt;) and hourly (&lt;span style=&quot; font-weight:600;&quot;&gt;H&lt;/span&gt;) 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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>22</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>95</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <property name="layoutDirection">
+ <enum>Qt::RightToLeft</enum>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border:1px solid #ff0000;</string>
+ </property>
+ <property name="text">
+ <string>Clear all Filters</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>12</width>
+ <height>12</height>
+ </size>
+ </property>
+ </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>
+ <width>220</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::ClickFocus</enum>
+ </property>
+ <item>
+ <property name="text">
+ <string>No groups</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Categories</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Nexus IDs</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="modFilterEdit">
+ <property name="baseSize">
+ <size>
+ <width>220</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="layoutWidget_2">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QFrame" name="startGroup">
+ <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
+ <item>
+ <widget class="QComboBox" name="executablesListBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>40</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>9</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="toolTip">
+ <string>Pick a program to run.</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;You can add new Tools to this list, but I can't promise tools I haven't tested will work.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="frame">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
+ <item>
+ <widget class="QPushButton" name="startButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>120</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="toolTip">
+ <string>Run program</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Run the selected program with ModOrganizer enabled.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="locale">
+ <locale language="English" country="UnitedStates"/>
+ </property>
+ <property name="text">
+ <string>Run</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/run</normaloff>:/MO/gui/run</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>36</width>
+ <height>36</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="linkButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>140</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Create a shortcut in your start menu or on the desktop to the specified program</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This creates a start menu shortcut that directly starts the selected program with the MO active.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Shortcut</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/link</normaloff>:/MO/gui/link</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="minimumSize">
+ <size>
+ <width>340</width>
+ <height>250</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::NoContextMenu</enum>
+ </property>
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="espTab">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <attribute name="title">
+ <string>Plugins</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <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>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <item>
+ <widget class="QPushButton" name="bossButton">
+ <property name="visible">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Sort</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/sort</normaloff>:/MO/gui/sort</iconset>
+ </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="restoreButton">
+ <property name="toolTip">
+ <string>Restore Backup...</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="saveButton">
+ <property name="toolTip">
+ <string>Create Backup</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="activePluginsLabel">
+ <property name="text">
+ <string>Active:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="LCDNumber" name="activePluginsCounter">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>26</height>
+ </size>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="digitCount">
+ <number>4</number>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="PluginListView" name="espList">
+ <property name="minimumSize">
+ <size>
+ <width>250</width>
+ <height>250</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available esp/esm files</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.&lt;br /&gt;There is a great tool named &amp;quot;BOSS&amp;quot; to automatically sort these files.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropOverwriteMode">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::InternalMove</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="expandsOnDoubleClick">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="MOBase::LineEditClear" name="espFilterEdit">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="bsaTab">
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <attribute name="title">
+ <string>Archives</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <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>6</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0">
+ <item>
+ <widget class="QLabel" name="managedArchiveLabel">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. &lt;p&gt;Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).&lt;/p&gt;&lt;p&gt;If there is a matching plugin, the game will load them no matter what.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;What is an archive?&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="MOBase::SortableTreeWidget" name="bsaList" native="true">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</string>
+ </property>
+ <property name="whatsThis">
+ <string>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they &quot;compete&quot; 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!
+
+ BSAs checked here are loaded in such a way that your installation order is obeyed properly.</string>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="dragEnabled" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropOverwriteMode" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="indentation" stdset="0">
+ <number>20</number>
+ </property>
+ <property name="itemsExpandable" stdset="0">
+ <bool>true</bool>
+ </property>
+ <property name="columnCount" stdset="0">
+ <number>1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="dataTab">
+ <attribute name="title">
+ <string>Data</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <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>6</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="btnRefreshData">
+ <property name="toolTip">
+ <string>refresh data-directory overview</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh the overview. This may take a moment.</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QTreeWidget" name="dataTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="whatsThis">
+ <string>This is an overview of your data directory as visible to the game (and tools). </string>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="animated">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>400</number>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>File</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Mod</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_12">
+ <item>
+ <widget class="QCheckBox" name="conflictsCheckBox">
+ <property name="toolTip">
+ <string>Filters the above list so that only conflicts are displayed.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Filters the above list so that only conflicts are displayed.</string>
+ </property>
+ <property name="text">
+ <string>Show only conflicts</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showArchiveDataCheckBox">
+ <property name="toolTip">
+ <string>Filters the above list so that files from archives are not shown</string>
+ </property>
+ <property name="statusTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string>Filters the above list so that files from archives are not shown</string>
+ </property>
+ <property name="text">
+ <string>Show files from Archives</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="savesTab">
+ <attribute name="title">
+ <string>Saves</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <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>6</number>
+ </property>
+ <item>
+ <widget class="QListWidget" name="savegameList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This is a list of all save games for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;&quot;&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;If you click &amp;quot;Fix Mods...&amp;quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="downloadTab">
+ <attribute name="title">
+ <string>Downloads</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="btnRefreshDownloads">
+ <property name="toolTip">
+ <string>Refresh downloads view</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="downloadLayout">
+ <item>
+ <widget class="DownloadListWidget" name="downloadView">
+ <property name="minimumSize">
+ <size>
+ <width>320</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</string>
+ </property>
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="verticalScrollMode">
+ <enum>QAbstractItemView::ScrollPerPixel</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,2">
+ <item>
+ <widget class="QCheckBox" name="showHiddenBox">
+ <property name="text">
+ <string>Show Hidden</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="MOBase::LineEditClear" name="downloadFilterEdit">
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QTreeView" name="logList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::ActionsContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::NoSelection</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="headerHidden">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </widget>
+ </item>
+ </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>
+ </property>
+ <property name="movable">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>42</width>
+ <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>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionChange_Game"/>
+ <addaction name="actionInstallMod"/>
+ <addaction name="actionNexus"/>
+ <addaction name="actionAdd_Profile"/>
+ <addaction name="actionModify_Executables"/>
+ <addaction name="actionTool"/>
+ <addaction name="actionSettings"/>
+ <addaction name="separator"/>
+ <addaction name="actionEndorseMO"/>
+ <addaction name="actionNotifications"/>
+ <addaction name="actionUpdate"/>
+ <addaction name="actionHelp"/>
+ </widget>
+ <widget class="QStatusBar" name="statusBar"/>
+ <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>
+ </property>
+ <property name="iconText">
+ <string>Install &amp;Mod</string>
+ </property>
+ <property name="toolTip">
+ <string>Install a new mod from an archive</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+M</string>
+ </property>
+ </action>
+ <action name="actionAdd_Profile">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/profiles</normaloff>:/MO/gui/profiles</iconset>
+ </property>
+ <property name="text">
+ <string>Profiles</string>
+ </property>
+ <property name="iconText">
+ <string>&amp;Profiles</string>
+ </property>
+ <property name="toolTip">
+ <string>Configure Profiles</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+P</string>
+ </property>
+ </action>
+ <action name="actionModify_Executables">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/icon_executable</normaloff>:/MO/gui/icon_executable</iconset>
+ </property>
+ <property name="text">
+ <string>Executables</string>
+ </property>
+ <property name="iconText">
+ <string>&amp;Executables</string>
+ </property>
+ <property name="toolTip">
+ <string>Configure the executables that can be started through Mod Organizer</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+E</string>
+ </property>
+ </action>
+ <action name="actionTool">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/plugins</normaloff>:/MO/gui/plugins</iconset>
+ </property>
+ <property name="text">
+ <string>Tools</string>
+ </property>
+ <property name="iconText">
+ <string>&amp;Tools</string>
+ </property>
+ <property name="toolTip">
+ <string>Tools</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+I</string>
+ </property>
+ </action>
+ <action name="actionSettings">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
+ </property>
+ <property name="text">
+ <string>Settings</string>
+ </property>
+ <property name="iconText">
+ <string>&amp;Settings</string>
+ </property>
+ <property name="toolTip">
+ <string>Configure settings and workarounds</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+S</string>
+ </property>
+ </action>
+ <action name="actionNexus">
+ <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>
+ <property name="text">
+ <string>Nexus</string>
+ </property>
+ <property name="toolTip">
+ <string>Search nexus network for more mods</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+N</string>
+ </property>
+ </action>
+ <action name="actionUpdate">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/update</normaloff>:/MO/gui/update</iconset>
+ </property>
+ <property name="text">
+ <string>Update</string>
+ </property>
+ <property name="toolTip">
+ <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>
+ </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>
+ </property>
+ </action>
+ <action name="actionHelp">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/help</normaloff>:/MO/gui/help</iconset>
+ </property>
+ <property name="text">
+ <string>Help</string>
+ </property>
+ <property name="toolTip">
+ <string>Help</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+H</string>
+ </property>
+ </action>
+ <action name="actionEndorseMO">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/icon_favorite</normaloff>:/MO/gui/icon_favorite</iconset>
+ </property>
+ <property name="text">
+ <string>Endorse MO</string>
+ </property>
+ <property name="toolTip">
+ <string>Endorse Mod Organizer</string>
+ </property>
+ </action>
+ <action name="actionCopy_Log_to_Clipboard">
+ <property name="text">
+ <string>Copy Log to Clipboard</string>
+ </property>
+ </action>
+ <action name="actionChange_Game">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/instance_switch</normaloff>:/MO/gui/instance_switch</iconset>
+ </property>
+ <property name="text">
+ <string>Change Game</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the Instance selection dialog to manage a different Game</string>
+ </property>
+ </action>
+ </widget>
+ <layoutdefault spacing="6" margin="11"/>
+ <customwidgets>
+ <customwidget>
+ <class>MOBase::LineEditClear</class>
+ <extends>QLineEdit</extends>
+ <header>lineeditclear.h</header>
+ </customwidget>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>modlistview.h</header>
+ </customwidget>
+ <customwidget>
+ <class>PluginListView</class>
+ <extends>QTreeView</extends>
+ <header>pluginlistview.h</header>
+ </customwidget>
+ <customwidget>
+ <class>LCDNumber</class>
+ <extends>QLCDNumber</extends>
+ <header>lcdnumber.h</header>
+ </customwidget>
+ <customwidget>
+ <class>DownloadListWidget</class>
+ <extends>QTreeView</extends>
+ <header>downloadlistwidget.h</header>
+ </customwidget>
+ <customwidget>
+ <class>MOBase::SortableTreeWidget</class>
+ <extends>QWidget</extends>
+ <header>sortabletreewidget.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/messagedialog.cpp b/src/messagedialog.cpp
index 90b0a9d3..6c6de3e7 100644
--- a/src/messagedialog.cpp
+++ b/src/messagedialog.cpp
@@ -81,7 +81,7 @@ void MessageDialog::resizeEvent(QResizeEvent *event)
void MessageDialog::showMessage(const QString &text, QWidget *reference, bool bringToFront)
{
- qDebug("%s", qPrintable(text));
+ qDebug("%s", qUtf8Printable(text));
if (reference != nullptr) {
if (bringToFront || (qApp->activeWindow() != nullptr)) {
MessageDialog *dialog = new MessageDialog(text, reference);
diff --git a/src/messagedialog.ui b/src/messagedialog.ui
index 469888ff..86879ab9 100644
--- a/src/messagedialog.ui
+++ b/src/messagedialog.ui
@@ -28,129 +28,20 @@
<height>0</height>
</size>
</property>
- <property name="palette">
- <palette>
- <active>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>220</red>
- <green>220</green>
- <blue>220</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Text">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>221</red>
- <green>221</green>
- <blue>221</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Base">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>255</green>
- <blue>255</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Window">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>67</red>
- <green>67</green>
- <blue>67</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>220</red>
- <green>220</green>
- <blue>220</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Text">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>221</red>
- <green>221</green>
- <blue>221</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Base">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>255</green>
- <blue>255</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Window">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>67</red>
- <green>67</green>
- <blue>67</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="WindowText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>120</red>
- <green>120</green>
- <blue>120</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Text">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>120</red>
- <green>120</green>
- <blue>120</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Base">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>67</red>
- <green>67</green>
- <blue>67</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Window">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>67</red>
- <green>67</green>
- <blue>67</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
- </property>
<property name="windowTitle">
<string>Placeholder</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
- <property name="margin">
+ <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>
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index 9ddd9e54..3a791827 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -137,7 +137,7 @@ void MOApplication::updateStyle(const QString &fileName)
if (QFile::exists(fileName)) {
setStyleSheet(QString("file:///%1").arg(fileName));
} else {
- qWarning("invalid stylesheet: %s", qPrintable(fileName));
+ qWarning("invalid stylesheet: %s", qUtf8Printable(fileName));
}
}
}
diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp
index 1995964b..c3142962 100644
--- a/src/modflagicondelegate.cpp
+++ b/src/modflagicondelegate.cpp
@@ -1,4 +1,4 @@
- #include "modflagicondelegate.h"
+#include "modflagicondelegate.h"
#include <QList>
@@ -7,9 +7,26 @@ ModInfo::EFlag ModFlagIconDelegate::m_ConflictFlags[4] = { ModInfo::FLAG_CONFLIC
, ModInfo::FLAG_CONFLICT_OVERWRITTEN
, ModInfo::FLAG_CONFLICT_REDUNDANT };
-ModFlagIconDelegate::ModFlagIconDelegate(QObject *parent)
+ModInfo::EFlag ModFlagIconDelegate::m_ArchiveLooseConflictFlags[2] = { ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE
+ , ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN };
+
+ModInfo::EFlag ModFlagIconDelegate::m_ArchiveConflictFlags[3] = { ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED
+ , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE
+ , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN };
+
+ModFlagIconDelegate::ModFlagIconDelegate(QObject *parent, int logicalIndex, int compactSize)
: IconDelegate(parent)
+ , m_LogicalIndex(logicalIndex)
+ , m_CompactSize(compactSize)
+ , m_Compact(false)
+{
+}
+
+void ModFlagIconDelegate::columnResized(int logicalIndex, int, int newSize)
{
+ if (logicalIndex == m_LogicalIndex) {
+ m_Compact = newSize < m_CompactSize;
+ }
}
QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const {
@@ -19,19 +36,59 @@ QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const {
ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt());
std::vector<ModInfo::EFlag> flags = info->getFlags();
- { // insert conflict icon first to provide nicer alignment
+ // Don't do flags for overwrite
+ if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE) != flags.end())
+ return result;
+
+ // insert conflict icons to provide nicer alignment
+ { // insert loose file conflicts first
auto iter = std::find_first_of(flags.begin(), flags.end(),
m_ConflictFlags, m_ConflictFlags + 4);
if (iter != flags.end()) {
result.append(getFlagIcon(*iter));
flags.erase(iter);
- } else {
+ } else if (!m_Compact) {
+ result.append(QString());
+ }
+ }
+
+ { // insert loose vs archive overwrite second
+ auto iter = std::find(flags.begin(), flags.end(),
+ ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!m_Compact) {
+ result.append(QString());
+ }
+ }
+
+ { // insert loose vs archive overwritten third
+ auto iter = std::find_first_of(flags.begin(), flags.end(),
+ m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!m_Compact) {
+ result.append(QString());
+ }
+ }
+
+ { // insert archive conflicts last
+ auto iter = std::find_first_of(flags.begin(), flags.end(),
+ m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!m_Compact) {
result.append(QString());
}
}
for (auto iter = flags.begin(); iter != flags.end(); ++iter) {
- result.append(getFlagIcon(*iter));
+ auto iconPath = getFlagIcon(*iter);
+ if (!iconPath.isEmpty())
+ result.append(iconPath);
}
}
return result;
@@ -40,16 +97,28 @@ QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const {
QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) const
{
switch (flag) {
- case ModInfo::FLAG_BACKUP: return ":/MO/gui/emblem_backup";
- case ModInfo::FLAG_INVALID: return ":/MO/gui/problem";
- case ModInfo::FLAG_NOTENDORSED: return ":/MO/gui/emblem_notendorsed";
- case ModInfo::FLAG_NOTES: return ":/MO/gui/emblem_notes";
- case ModInfo::FLAG_CONFLICT_OVERWRITE: return ":/MO/gui/emblem_conflict_overwrite";
- case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return ":/MO/gui/emblem_conflict_overwritten";
- case ModInfo::FLAG_CONFLICT_MIXED: return ":/MO/gui/emblem_conflict_mixed";
- case ModInfo::FLAG_CONFLICT_REDUNDANT: return ":MO/gui/emblem_conflict_redundant";
- case ModInfo::FLAG_ALTERNATE_GAME: return ":MO/gui/alternate_game";
- default: return QString();
+ case ModInfo::FLAG_BACKUP: return QStringLiteral(":/MO/gui/emblem_backup");
+ case ModInfo::FLAG_INVALID: return QStringLiteral(":/MO/gui/problem");
+ case ModInfo::FLAG_NOTENDORSED: return QStringLiteral(":/MO/gui/emblem_notendorsed");
+ case ModInfo::FLAG_NOTES: return QStringLiteral(":/MO/gui/emblem_notes");
+ case ModInfo::FLAG_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/emblem_conflict_mixed");
+ case ModInfo::FLAG_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/emblem_conflict_overwrite");
+ case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/emblem_conflict_overwritten");
+ case ModInfo::FLAG_CONFLICT_REDUNDANT: return QStringLiteral(":/MO/gui/emblem_conflict_redundant");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwrite");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwritten");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/archive_conflict_mixed");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_conflict_winner");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_conflict_loser");
+ case ModInfo::FLAG_ALTERNATE_GAME: return QStringLiteral(":/MO/gui/alternate_game");
+ case ModInfo::FLAG_FOREIGN: return QString();
+ case ModInfo::FLAG_SEPARATOR: return QString();
+ case ModInfo::FLAG_OVERWRITE: return QString();
+ case ModInfo::FLAG_PLUGIN_SELECTED: return QString();
+ case ModInfo::FLAG_TRACKED: return QStringLiteral(":/MO/gui/tracked");
+ default:
+ qWarning("ModInfo flag %d has no defined icon", flag);
+ return QString();
}
}
diff --git a/src/modflagicondelegate.h b/src/modflagicondelegate.h
index b31a250b..eb6a76ab 100644
--- a/src/modflagicondelegate.h
+++ b/src/modflagicondelegate.h
@@ -5,16 +5,29 @@
class ModFlagIconDelegate : public IconDelegate
{
+Q_OBJECT
+
public:
- explicit ModFlagIconDelegate(QObject *parent = 0);
+ explicit ModFlagIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 120);
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
+
+public slots:
+ void columnResized(int logicalIndex, int oldSize, int newSize);
+
private:
virtual QList<QString> getIcons(const QModelIndex &index) const;
virtual size_t getNumIcons(const QModelIndex &index) const;
QString getFlagIcon(ModInfo::EFlag flag) const;
+
private:
static ModInfo::EFlag m_ConflictFlags[4];
+ static ModInfo::EFlag m_ArchiveLooseConflictFlags[2];
+ static ModInfo::EFlag m_ArchiveConflictFlags[3];
+
+ int m_LogicalIndex;
+ int m_CompactSize;
+ bool m_Compact;
};
#endif // MODFLAGICONDELEGATE_H
diff --git a/src/modinfo.cpp b/src/modinfo.cpp
index 1c6139f6..c1974152 100644
--- a/src/modinfo.cpp
+++ b/src/modinfo.cpp
@@ -110,15 +110,16 @@ QString ModInfo::getContentTypeName(int contentType)
case CONTENT_INI: return tr("INI files");
case CONTENT_MODGROUP: return tr("ModGroup files");
- default: throw MyException(tr("invalid content type %1").arg(contentType));
+ default: throw MyException(tr("invalid content type: %1").arg(contentType));
}
}
-void ModInfo::createFromOverwrite(PluginContainer *pluginContainer)
+void ModInfo::createFromOverwrite(PluginContainer *pluginContainer,
+ MOShared::DirectoryEntry **directoryStructure)
{
QMutexLocker locker(&s_Mutex);
- s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite(pluginContainer)));
+ s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite(pluginContainer, directoryStructure)));
}
unsigned int ModInfo::getNumMods()
@@ -133,7 +134,7 @@ ModInfo::Ptr ModInfo::getByIndex(unsigned int index)
QMutexLocker locker(&s_Mutex);
if (index >= s_Collection.size() && index != ULONG_MAX) {
- throw MyException(tr("invalid mod index %1").arg(index));
+ throw MyException(tr("invalid mod index: %1").arg(index));
}
if (index == ULONG_MAX) return s_Collection[ModInfo::getIndex("Overwrite")];
return s_Collection[index];
@@ -144,20 +145,35 @@ std::vector<ModInfo::Ptr> ModInfo::getByModID(QString game, int modID)
{
QMutexLocker locker(&s_Mutex);
- auto iter = s_ModsByModID.find(std::pair<QString, int>(game, modID));
- if (iter == s_ModsByModID.end()) {
+ std::vector<unsigned int> match;
+ for (auto iter : s_ModsByModID) {
+ if (iter.first.second == modID) {
+ if (iter.first.first.compare(game, Qt::CaseInsensitive) == 0) {
+ match.insert(match.end(), iter.second.begin(), iter.second.end());
+ }
+ }
+ }
+ if (match.empty()) {
return std::vector<ModInfo::Ptr>();
}
std::vector<ModInfo::Ptr> result;
- for (auto idxIter = iter->second.begin(); idxIter != iter->second.end(); ++idxIter) {
- result.push_back(getByIndex(*idxIter));
+ for (auto iter : match) {
+ result.push_back(getByIndex(iter));
}
return result;
}
+ModInfo::Ptr ModInfo::getByName(const QString &name)
+{
+ QMutexLocker locker(&s_Mutex);
+
+ return s_Collection[ModInfo::getIndex(name)];
+}
+
+
bool ModInfo::removeMod(unsigned int index)
{
QMutexLocker locker(&s_Mutex);
@@ -248,7 +264,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory,
}
}
- createFromOverwrite(pluginContainer);
+ createFromOverwrite(pluginContainer, directoryStructure);
std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName);
@@ -277,59 +293,136 @@ ModInfo::ModInfo(PluginContainer *pluginContainer)
}
-void ModInfo::checkChunkForUpdate(PluginContainer *pluginContainer, const std::vector<int> &modIDs, QObject *receiver, QString gameName)
+bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver)
{
- if (modIDs.size() != 0) {
- NexusInterface::instance(pluginContainer)->requestUpdates(modIDs, receiver, QVariant(), gameName, QString());
+ bool updatesAvailable = true;
+
+ QDateTime earliest = QDateTime::currentDateTimeUtc();
+ QDateTime latest;
+ std::set<QString> games;
+ for (auto mod : s_Collection) {
+ if (mod->canBeUpdated()) {
+ if (mod->getLastNexusUpdate() < earliest)
+ earliest = mod->getLastNexusUpdate();
+ if (mod->getLastNexusUpdate() > latest)
+ latest = mod->getLastNexusUpdate();
+ games.insert(mod->getGameName().toLower());
+ }
}
-}
+ if (latest < QDateTime::currentDateTimeUtc().addDays(-30)) {
+ std::set<std::pair<QString, int>> organizedGames;
+ for (auto mod : s_Collection) {
+ if (mod->canBeUpdated()) {
+ 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.");
+ updatesAvailable = false;
+ }
-int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver)
-{
- // technically this should be 255 but those requests can take nexus fairly long, produce
- // large output and may have been the cause of issue #1166
- static const int chunkSize = 64;
+ for (auto game : organizedGames) {
+ NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString());
+ }
+ } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-30)) {
+ for (auto gameName : games)
+ NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString());
+ } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) {
+ for (auto gameName : games)
+ NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString());
+ } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-1)) {
+ for (auto gameName : games)
+ NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString());
+ } else {
+ for (auto gameName : games)
+ NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString());
+ }
- int result = 0;
- std::vector<int> modIDs;
+ return updatesAvailable;
+}
- // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now
- IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition");
- if (game && game->nexusModOrganizerID()) {
- modIDs.push_back(game->nexusModOrganizerID());
- checkChunkForUpdate(pluginContainer, modIDs, receiver, game->gameShortName());
- modIDs.clear();
+std::set<QSharedPointer<ModInfo>> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated)
+{
+ std::set<QSharedPointer<ModInfo>> finalMods;
+ for (QVariant result : updateData) {
+ QVariantMap update = result.toMap();
+ std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer<ModInfo> info) -> bool {
+ if (info->getNexusID() == update["mod_id"].toInt() && info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0)
+ if (info->getLastNexusUpdate().addSecs(-3600) < QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC))
+ return true;
+ return false;
+ });
}
- std::multimap<QString, QSharedPointer<ModInfo>> organizedGames;
- for (auto mod : s_Collection) {
- if (mod->canBeUpdated()) {
- organizedGames.insert(std::pair<QString, QSharedPointer<ModInfo>>(mod->getGameName(), mod));
+ if (addOldMods)
+ for (auto mod : s_Collection)
+ if (mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addDays(-30) && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0)
+ finalMods.insert(mod);
+
+ if (markUpdated) {
+ std::set<QSharedPointer<ModInfo>> updates;
+ std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(updates, updates.end()), [=](QSharedPointer<ModInfo> info) -> bool {
+ if (info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && info->canBeUpdated())
+ return true;
+ return false;
+ });
+ std::set<QSharedPointer<ModInfo>> diff;
+ std::set_difference(updates.begin(), updates.end(), finalMods.begin(), finalMods.end(), std::inserter(diff, diff.end()));
+ for (auto skipped : diff) {
+ skipped->setLastNexusUpdate(QDateTime::currentDateTimeUtc());
}
}
+ return finalMods;
+}
+
+void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap<QString, int> IDs)
+{
+ std::vector<QSharedPointer<ModInfo>> mods;
+ std::set<std::pair<QString, int>> organizedGames;
- QString currentGame = "";
- for (auto game : organizedGames) {
- if (currentGame != game.first) {
- if (currentGame != "") {
- checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame);
- modIDs.clear();
+ for (auto ID : IDs) {
+ for (auto matchedMod : getByModID(ID.first, ID.second)) {
+ bool alreadyMatched = false;
+ for (auto mod : mods) {
+ if (mod == matchedMod) {
+ alreadyMatched = true;
+ break;
+ }
}
- currentGame = game.first;
- }
- modIDs.push_back(game.second->getNexusID());
- if (modIDs.size() >= chunkSize) {
- checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame);
- modIDs.clear();
+ if (!alreadyMatched)
+ mods.push_back(matchedMod);
}
}
+ mods.erase(
+ std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return mod->getNexusID() <= 0; }),
+ mods.end()
+ );
+ for (auto mod : mods) {
+ mod->setLastNexusUpdate(QDateTime());
+ }
- checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame);
+ std::sort(mods.begin(), mods.end(), [](QSharedPointer<ModInfo> a, QSharedPointer<ModInfo> b) -> bool {
+ return a->getLastNexusUpdate() < b->getLastNexusUpdate();
+ });
- return result;
+ if (mods.size()) {
+ qInfo("Checking updates for %d mods...", mods.size());
+
+ for (auto mod : mods) {
+ organizedGames.insert(std::make_pair<QString, int>(mod->getGameName().toLower(), mod->getNexusID()));
+ }
+
+ for (auto game : organizedGames) {
+ NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString());
+ }
+ } else {
+ qInfo("None of the selected mods can be updated.");
+ }
}
+
void ModInfo::setVersion(const VersionInfo &version)
{
m_Version = version;
diff --git a/src/modinfo.h b/src/modinfo.h
index 5a69185d..f1d816fe 100644
--- a/src/modinfo.h
+++ b/src/modinfo.h
@@ -1,686 +1,789 @@
-/*
-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/>.
-*/
-
-#ifndef MODINFO_H
-#define MODINFO_H
-
-#include "imodinterface.h"
-#include "versioninfo.h"
-
-class PluginContainer;
-
-class QDateTime;
-class QDir;
-#include <QMutex>
-#include <QSharedPointer>
-#include <QString>
-#include <QStringList>
-#include <QColor>
-
-#include <boost/function.hpp>
-
-#include <map>
-#include <set>
-#include <vector>
-
-namespace MOBase { class IPluginGame; }
-namespace MOShared { class DirectoryEntry; }
-
-/**
- * @brief Represents meta information about a single mod.
- *
- * Represents meta information about a single mod. The class interface is used
- * to manage the mod collection
- *
- **/
-class ModInfo : public QObject, public MOBase::IModInterface
-{
-
- Q_OBJECT
-
-public:
-
- typedef QSharedPointer<ModInfo> Ptr;
-
- static QString s_HiddenExt;
-
- enum EFlag {
- FLAG_INVALID,
- FLAG_BACKUP,
- FLAG_SEPARATOR,
- FLAG_OVERWRITE,
- FLAG_FOREIGN,
- FLAG_NOTENDORSED,
- FLAG_NOTES,
- FLAG_CONFLICT_OVERWRITE,
- FLAG_CONFLICT_OVERWRITTEN,
- FLAG_CONFLICT_MIXED,
- FLAG_CONFLICT_REDUNDANT,
- FLAG_PLUGIN_SELECTED,
- FLAG_ALTERNATE_GAME
- };
-
- enum EContent {
- CONTENT_PLUGIN,
- CONTENT_TEXTURE,
- CONTENT_MESH,
- CONTENT_BSA,
- CONTENT_INTERFACE,
- CONTENT_SOUND,
- CONTENT_SCRIPT,
- CONTENT_SKSE,
- CONTENT_SKSEFILES,
- CONTENT_SKYPROC,
- CONTENT_MCM,
- CONTENT_INI,
- CONTENT_MODGROUP
-
- };
-
- static const int NUM_CONTENT_TYPES = CONTENT_MODGROUP + 1;
-
- enum EHighlight {
- HIGHLIGHT_NONE = 0,
- HIGHLIGHT_INVALID = 1,
- HIGHLIGHT_CENTER = 2,
- HIGHLIGHT_IMPORTANT = 4,
- HIGHLIGHT_PLUGIN = 8
- };
-
- enum EEndorsedState {
- ENDORSED_FALSE,
- ENDORSED_TRUE,
- ENDORSED_UNKNOWN,
- ENDORSED_NEVER
- };
-
- enum EModType {
- MOD_DEFAULT,
- MOD_DLC,
- MOD_CC
- };
-
-
-public:
-
- /**
- * @brief read the mod directory and Mod ModInfo objects for all subdirectories
- **/
- static void updateFromDisc(const QString &modDirectory,
- MOShared::DirectoryEntry **directoryStructure,
- PluginContainer *pluginContainer,
- bool displayForeign,
- MOBase::IPluginGame const *game);
-
- static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); }
-
- /**
- * @brief retrieve the number of mods
- *
- * @return number of mods
- **/
- static unsigned int getNumMods();
-
- /**
- * @brief retrieve a ModInfo object based on its index
- *
- * @param index the index to look up. the maximum is getNumMods() - 1
- * @return a reference counting pointer to the mod info.
- * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread
- **/
- static ModInfo::Ptr getByIndex(unsigned int index);
-
- /**
- * @brief retrieve a ModInfo object based on its nexus mod id
- *
- * @param modID the nexus mod id to look up
- * @return a reference counting pointer to the mod info
- * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id,
- * this function will return only one of them
- **/
- static std::vector<ModInfo::Ptr> getByModID(QString game, int modID);
-
- /**
- * @brief remove a mod by index
- *
- * this physically deletes the specified mod from the disc and updates the ModInfo collection
- * but not other structures that reference mods
- * @param index index of the mod to delete
- * @return true if removal was successful, fals otherwise
- **/
- static bool removeMod(unsigned int index);
-
- /**
- * @brief retrieve the mod index by the mod name
- *
- * @param name name of the mod to look up
- * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned
- **/
- static unsigned int getIndex(const QString &name);
-
- /**
- * @brief find the first mod that fulfills the filter function (after no particular order)
- * @param filter a function to filter by. should return true for a match
- * @return index of the matching mod or UINT_MAX if there wasn't a match
- */
- static unsigned int findMod(const boost::function<bool (ModInfo::Ptr)> &filter);
-
- /**
- * @brief check a bunch of mods for updates
- * @param modIDs list of mods (Nexus Mod IDs) to check for updates
- * @return
- */
- static void checkChunkForUpdate(PluginContainer *pluginContainer, const std::vector<int> &modIDs, QObject *receiver, QString gameName);
-
- /**
- * @brief query nexus information for every mod and update the "newest version" information
- **/
- static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver);
-
- /**
- * @brief create a new mod from the specified directory and add it to the collection
- * @param dir directory to create from
- * @return pointer to the info-structure of the newly created/added mod
- */
- static ModInfo::Ptr createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, MOShared::DirectoryEntry **directoryStructure);
-
- /**
- * @brief create a new "foreign-managed" mod from a tuple of plugin and archives
- * @param espName name of the plugin
- * @param bsaNames names of archives
- * @return a new mod
- */
- static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer);
-
- /**
- * @brief retieve a name for one of the CONTENT_ enums
- * @param contentType the content value
- * @return a display string
- */
- static QString getContentTypeName(int contentType);
-
- virtual bool isRegular() const { return false; }
-
- virtual bool isEmpty() const { return false; }
-
- /**
- * @brief test if there is a newer version of the mod
- *
- * test if there is a newer version of the mod. This does NOT cause
- * information to be retrieved from the nexus, it will only test version information already
- * available locally. Use checkAllForUpdate() to update this version information
- *
- * @return true if there is a newer version
- **/
- virtual bool updateAvailable() const = 0;
-
- /**
- * @return true if the update currently available is ignored
- */
- virtual bool updateIgnored() const = 0;
-
- /**
- * @brief test if the "newest" version of the mod is older than the installed version
- *
- * test if there is a newer version of the mod. This does NOT cause
- * information to be retrieved from the nexus, it will only test version information already
- * available locally. Use checkAllForUpdate() to update this version information
- *
- * @return true if the newest version is older than the installed one
- **/
- virtual bool downgradeAvailable() const = 0;
-
- /**
- * @brief request an update of nexus description for this mod.
- *
- * This requests mod information from the nexus. This is an asynchronous request,
- * so there is no immediate effect of this call.
- * Right now, Mod Organizer interprets the "newest version" and "description" from the
- * response, though the description is only stored in memory
- *
- **/
- virtual bool updateNXMInfo() = 0;
-
- /**
- * @brief assign or unassign the specified category
- *
- * Every mod can have an arbitrary number of categories assigned to it
- *
- * @param categoryID id of the category to set
- * @param active determines wheter the category is assigned or unassigned
- * @note this function does not test whether categoryID actually identifies a valid category
- **/
- virtual void setCategory(int categoryID, bool active) = 0;
-
- /**
- * @brief changes the comments (manually set information displayed in the mod list) for this mod
- * @param comments new comments
- */
- virtual void setComments(const QString &comments) = 0;
-
- /**
- * @brief change the notes (manually set information) for this mod
- * @param notes new notes
- */
- virtual void setNotes(const QString &notes) = 0;
-
- /**
- * @brief set/change the source game of this mod
- *
- * @param gameName the source game shortName
- */
- virtual void setGameName(QString gameName) = 0;
-
- /**
- * @brief set/change the nexus mod id of this mod
- *
- * @param modID the nexus mod id
- **/
- virtual void setNexusID(int modID) = 0;
-
- /**
- * @brief set/change the version of this mod
- * @param version new version of the mod
- */
- virtual void setVersion(const MOBase::VersionInfo &version);
-
- /**
- * @brief Controls if mod should be highlighted based on plugin selection
- * @param isSelected whether or not the plugin has a selected mod
- **/
- virtual void setPluginSelected(const bool &isSelected);
-
- /**
- * @brief set the newest version of this mod on the nexus
- *
- * this can be used to overwrite the version of a mod without actually
- * updating the mod
- *
- * @param version the new version to use
- * @todo this function should be made obsolete. All queries for mod information should go through
- * this class so no public function for this change is required
- **/
- virtual void setNewestVersion(const MOBase::VersionInfo &version) = 0;
-
- /**
- * @brief sets the repository that was used to download the mod
- */
- virtual void setRepository(const QString &) {}
-
- /**
- * @brief changes/updates the nexus description text
- * @param description the current description text
- */
- virtual void setNexusDescription(const QString &description) = 0;
-
- /**
- * @brief sets the file this mod was installed from
- * @param fileName name of the file
- */
- virtual void setInstallationFile(const QString &fileName) = 0;
-
- /**
- * @brief sets the category id from a nexus category id. Conversion to MO id happens internally
- * @param categoryID the nexus category id
- * @note if a mapping is not possible, the category is set to the default value
- */
- virtual void addNexusCategory(int categoryID) = 0;
-
- virtual void addCategory(const QString &categoryName) override;
- virtual bool removeCategory(const QString &categoryName) override;
- virtual QStringList categories() override;
-
- /**
- * update the endorsement state for the mod. This only changes the
- * buffered state, it does not sync with Nexus
- * @param endorsed the new endorsement state
- */
- virtual void setIsEndorsed(bool endorsed) = 0;
-
- /**
- * set the mod to "i don't intend to endorse". The mod will not show as unendorsed but can still be endorsed
- */
- virtual void setNeverEndorse() = 0;
-
- /**
- * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices
- * @return true if the mod was successfully removed
- **/
- virtual bool remove() = 0;
-
- /**
- * @brief endorse or un-endorse the mod. This will sync with nexus!
- * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed.
- * @note if doEndorse doesn't differ from the current value, nothing happens.
- */
- virtual void endorse(bool doEndorse) = 0;
-
- /**
- * @brief clear all caches held for this mod
- */
- virtual void clearCaches() {}
-
- /**
- * @brief getter for the mod name
- *
- * @return the mod name
- **/
- virtual QString name() const = 0;
-
- /**
- * @brief getter for an internal name. This is usually the same as the regular name, but with special mod types it might be
- * this is used to distinguish between mods that have the same visible name
- * @return internal mod name
- */
- virtual QString internalName() const { return name(); }
-
- /**
- * @brief getter for the mod path
- *
- * @return the (absolute) path to the mod
- **/
- virtual QString absolutePath() const = 0;
-
- /**
- * @brief getter for the installation file
- *
- * @return file used to install this mod from
- */
- virtual QString getInstallationFile() const = 0;
-
- /**
- * @return version object for machine based comparisons
- **/
- virtual MOBase::VersionInfo getVersion() const { return m_Version; }
-
- /**
- * @brief getter for the newest version number of this mod
- *
- * @return newest version of the mod
- **/
- virtual MOBase::VersionInfo getNewestVersion() const = 0;
-
- /**
- * @return the repository from which the file was downloaded. Only relevant regular mods
- */
- virtual QString repository() const { return ""; }
-
- /**
- * @brief ignore the newest version for updates
- */
- virtual void ignoreUpdate(bool ignore) = 0;
-
- /**
- * @brief getter for the nexus mod id
- *
- * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist
- **/
- virtual int getNexusID() const = 0;
-
- /**
- * @brief getter for the source game repository
- *
- * @return the source game repository. should default to the active game.
- **/
- virtual QString getGameName() const = 0;
-
- /**
- * @return the fixed priority of mods of this type or INT_MIN if the priority of mods
- * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods
- * or INT_MAX to force priority above all user-modifiables
- */
- virtual int getFixedPriority() const = 0;
-
- /**
- * @return true if the mod is always enabled
- */
- virtual bool alwaysEnabled() const { return false; }
-
- /**
- * @return true if the mod can be updated
- */
- virtual bool canBeUpdated() const { return false; }
-
- /**
- * @return true if the mod can be enabled/disabled
- */
- virtual bool canBeEnabled() const { return false; }
-
- /**
- * @return a list of flags for this mod
- */
- virtual std::vector<EFlag> getFlags() const = 0;
-
- /**
- * @return a list of content types contained in a mod
- */
- virtual std::vector<EContent> getContents() const { return std::vector<EContent>(); }
-
- /**
- * @brief test if the specified flag is set for this mod
- * @param flag the flag to test
- * @return true if the flag is set, false otherwise
- */
- bool hasFlag(EFlag flag) const;
-
- /**
- * @brief test if the mods contains the specified content
- * @param content the content to test
- * @return true if the content is there, false otherwise
- */
- bool hasContent(ModInfo::EContent content) const;
-
- /**
- * @return an indicator if and how this mod should be highlighted by the UI
- */
- virtual int getHighlight() const { return HIGHLIGHT_NONE; }
-
- /**
- * @return list of names of ini tweaks
- **/
- virtual std::vector<QString> getIniTweaks() const = 0;
-
- /**
- * @return a description about the mod, to be displayed in the ui
- */
- virtual QString getDescription() const = 0;
-
- /**
- * @return comments for this mod
- */
- virtual QString comments() const = 0;
-
- /**
- * @return notes for this mod
- */
- virtual QString notes() const = 0;
-
- /**
- * @return creation time of this mod
- */
- virtual QDateTime creationTime() const = 0;
-
- /**
- * @return nexus description of the mod (html)
- */
- virtual QString getNexusDescription() const = 0;
-
- /**
- * @return last time nexus was queried for infos on this mod
- */
- virtual QDateTime getLastNexusQuery() const = 0;
-
- /**
- * @return a list of files that, if they exist in the data directory are treated as files in THIS mod
- */
- virtual QStringList stealFiles() const { return QStringList(); }
-
- /**
- * @return a list of archives belonging to this mod (as absolute file paths)
- */
- virtual QStringList archives() const = 0;
-
- /*
- *@return the color choosen by the user for the mod/separator
- */
- virtual QColor getColor() { return QColor(); }
-
- /*
- *@return true if the color has been set successfully.
- */
- virtual void setColor(QColor color) { }
-
- /**
- * @brief adds the information that a file has been installed into this mod
- * @param modId id of the mod installed
- * @param fileId id of the file installed
- */
- virtual void addInstalledFile(int modId, int fileId) = 0;
-
- /**
- * @brief test if the mod belongs to the specified category
- *
- * @param categoryID the category to test for.
- * @return true if the mod belongs to the specified category
- * @note this does not verify the id actually identifies a category
- **/
- bool categorySet(int categoryID) const;
-
- /**
- * @brief retrive the whole list of categories (as ids) this mod belongs to
- *
- * @return list of categories
- **/
- const std::set<int> &getCategories() const { return m_Categories; }
-
- /**
- * @return id of the primary category of this mod
- */
- int getPrimaryCategory() const { return m_PrimaryCategory; }
-
- /**
- * @brief sets the new primary category of the mod
- * @param categoryID the category to set
- */
- virtual void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; }
-
- /**
- * @return true if this mod is considered "valid", that is: it contains data used by the game
- **/
- virtual bool isValid() const { return m_Valid; }
-
- /**
- * @return true if the file has been endorsed on nexus
- */
- virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; }
-
- /**
- * @brief updates the valid-flag for this mod
- */
- void testValid();
-
- /**
- * @brief updates the mod to flag it as converted in order to ignore the alternate game warning
- */
- virtual void markConverted(bool converted) {}
-
- /**
- * @brief updates the mod to flag it as valid in order to ignore the invalid game data flag
- */
- virtual void markValidated(bool validated) {}
-
- /**
- * @brief reads meta information from disk
- */
- virtual void readMeta() {}
-
- /**
- * @brief stores meta information back to disk
- */
- virtual void saveMeta() {}
-
- /**
- * @return retrieve list of mods (as mod index) that are overwritten by this one. Updates may be delayed
- */
- virtual std::set<unsigned int> getModOverwrite() { return std::set<unsigned int>(); }
-
- /**
- * @return list of mods (as mod index) that overwrite this one. Updates may be delayed
- */
- virtual std::set<unsigned int> getModOverwritten() { return std::set<unsigned int>(); }
-
- /**
- * @brief update conflict information
- */
- virtual void doConflictCheck() const {}
-
- /**
- * @brief set the URL for a mod
- */
- virtual void setURL(QString const &) {}
-
- /**
- * @returns the URL for a mod
- */
- virtual QString getURL() const { return ""; }
-
-signals:
-
- /**
- * @brief emitted whenever the information of a mod changes
- *
- * @param success true if the mod details were updated successfully, false if not
- **/
- void modDetailsUpdated(bool success);
-
-protected:
-
- ModInfo(PluginContainer *pluginContainer);
-
- static void updateIndices();
- static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS);
-
-private:
-
- static void createFromOverwrite(PluginContainer *pluginContainer);
-
-protected:
-
- static std::vector<ModInfo::Ptr> s_Collection;
- static std::map<QString, unsigned int> s_ModsByName;
-
- int m_PrimaryCategory;
- std::set<int> m_Categories;
-
- MOBase::VersionInfo m_Version;
-
- bool m_PluginSelected = false;
-
-private:
-
- static QMutex s_Mutex;
- static std::map<std::pair<QString, int>, std::vector<unsigned int> > s_ModsByModID;
- static int s_NextID;
-
- bool m_Valid;
-
-};
-
-
-#endif // MODINFO_H
+/*
+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/>.
+*/
+
+#ifndef MODINFO_H
+#define MODINFO_H
+
+#include "imodinterface.h"
+#include "versioninfo.h"
+
+class PluginContainer;
+class QDir;
+class QDateTime;
+
+#include <QMutex>
+#include <QSharedPointer>
+#include <QString>
+#include <QStringList>
+#include <QColor>
+
+#include <boost/function.hpp>
+
+#include <map>
+#include <set>
+#include <vector>
+
+namespace MOBase { class IPluginGame; }
+namespace MOShared { class DirectoryEntry; }
+
+/**
+ * @brief Represents meta information about a single mod.
+ *
+ * Represents meta information about a single mod. The class interface is used
+ * to manage the mod collection
+ *
+ **/
+class ModInfo : public QObject, public MOBase::IModInterface
+{
+
+ Q_OBJECT
+
+public:
+
+ typedef QSharedPointer<ModInfo> Ptr;
+
+ static QString s_HiddenExt;
+
+ enum EFlag {
+ FLAG_INVALID,
+ FLAG_BACKUP,
+ FLAG_SEPARATOR,
+ FLAG_OVERWRITE,
+ FLAG_FOREIGN,
+ FLAG_NOTENDORSED,
+ FLAG_NOTES,
+ FLAG_CONFLICT_OVERWRITE,
+ FLAG_CONFLICT_OVERWRITTEN,
+ FLAG_CONFLICT_MIXED,
+ FLAG_CONFLICT_REDUNDANT,
+ FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE,
+ FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN,
+ FLAG_ARCHIVE_CONFLICT_OVERWRITE,
+ FLAG_ARCHIVE_CONFLICT_OVERWRITTEN,
+ FLAG_ARCHIVE_CONFLICT_MIXED,
+ FLAG_PLUGIN_SELECTED,
+ FLAG_ALTERNATE_GAME,
+ FLAG_TRACKED,
+ };
+
+ enum EContent {
+ CONTENT_PLUGIN,
+ CONTENT_TEXTURE,
+ CONTENT_MESH,
+ CONTENT_BSA,
+ CONTENT_INTERFACE,
+ CONTENT_SOUND,
+ CONTENT_SCRIPT,
+ CONTENT_SKSE,
+ CONTENT_SKSEFILES,
+ CONTENT_SKYPROC,
+ CONTENT_MCM,
+ CONTENT_INI,
+ CONTENT_MODGROUP
+ };
+
+ static const int NUM_CONTENT_TYPES = CONTENT_MODGROUP + 1;
+
+ enum EHighlight {
+ HIGHLIGHT_NONE = 0,
+ HIGHLIGHT_INVALID = 1,
+ HIGHLIGHT_CENTER = 2,
+ HIGHLIGHT_IMPORTANT = 4,
+ HIGHLIGHT_PLUGIN = 8
+ };
+
+ enum EEndorsedState {
+ ENDORSED_FALSE,
+ ENDORSED_TRUE,
+ ENDORSED_UNKNOWN,
+ ENDORSED_NEVER
+ };
+
+ enum ETrackedState {
+ TRACKED_FALSE,
+ TRACKED_TRUE,
+ TRACKED_UNKNOWN,
+ };
+
+ enum EModType {
+ MOD_DEFAULT,
+ MOD_DLC,
+ MOD_CC
+ };
+
+
+public:
+
+ /**
+ * @brief read the mod directory and Mod ModInfo objects for all subdirectories
+ **/
+ static void updateFromDisc(const QString &modDirectory,
+ MOShared::DirectoryEntry **directoryStructure,
+ PluginContainer *pluginContainer,
+ bool displayForeign,
+ MOBase::IPluginGame const *game);
+
+ static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); }
+
+ /**
+ * @brief retrieve the number of mods
+ *
+ * @return number of mods
+ **/
+ static unsigned int getNumMods();
+
+ /**
+ * @brief retrieve a ModInfo object based on its index
+ *
+ * @param index the index to look up. the maximum is getNumMods() - 1
+ * @return a reference counting pointer to the mod info.
+ * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread
+ **/
+ static ModInfo::Ptr getByIndex(unsigned int index);
+
+ /**
+ * @brief retrieve a ModInfo object based on its nexus mod id
+ *
+ * @param modID the nexus mod id to look up
+ * @return a reference counting pointer to the mod info
+ * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id,
+ * this function will return only one of them
+ **/
+ static std::vector<ModInfo::Ptr> getByModID(QString game, int modID);
+
+ /**
+ * @brief retrieve a ModInfo object based on its name
+ *
+ * @param name the name to look up
+ * @return a reference counting pointer to the mod info
+ * @note since the pointer is reference counter, the pointer remains valid even if the collection is refreshed in a different thread
+ **/
+ static ModInfo::Ptr getByName(const QString &name);
+
+ /**
+ * @brief remove a mod by index
+ *
+ * this physically deletes the specified mod from the disc and updates the ModInfo collection
+ * but not other structures that reference mods
+ * @param index index of the mod to delete
+ * @return true if removal was successful, fals otherwise
+ **/
+ static bool removeMod(unsigned int index);
+
+ /**
+ * @brief retrieve the mod index by the mod name
+ *
+ * @param name name of the mod to look up
+ * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned
+ **/
+ static unsigned int getIndex(const QString &name);
+
+ /**
+ * @brief find the first mod that fulfills the filter function (after no particular order)
+ * @param filter a function to filter by. should return true for a match
+ * @return index of the matching mod or UINT_MAX if there wasn't a match
+ */
+ static unsigned int findMod(const boost::function<bool (ModInfo::Ptr)> &filter);
+
+ /**
+ * @brief run a limited batch of mod update checks for "newest version" information
+ */
+ static void manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap<QString, int> IDs);
+
+ /**
+ * @brief query nexus information for every mod and update the "newest version" information
+ * @return true if any mods are checked for update
+ **/
+ static bool checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver);
+
+ static std::set<QSharedPointer<ModInfo>> filteredMods(QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false);
+
+ /**
+ * @brief create a new mod from the specified directory and add it to the collection
+ * @param dir directory to create from
+ * @return pointer to the info-structure of the newly created/added mod
+ */
+ static ModInfo::Ptr createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, MOShared::DirectoryEntry **directoryStructure);
+
+ /**
+ * @brief create a new "foreign-managed" mod from a tuple of plugin and archives
+ * @param espName name of the plugin
+ * @param bsaNames names of archives
+ * @return a new mod
+ */
+ static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer);
+
+ /**
+ * @brief retieve a name for one of the CONTENT_ enums
+ * @param contentType the content value
+ * @return a display string
+ */
+ static QString getContentTypeName(int contentType);
+
+ virtual bool isRegular() const { return false; }
+
+ virtual bool isEmpty() const { return false; }
+
+ /**
+ * @brief test if there is a newer version of the mod
+ *
+ * test if there is a newer version of the mod. This does NOT cause
+ * information to be retrieved from the nexus, it will only test version information already
+ * available locally. Use checkAllForUpdate() to update this version information
+ *
+ * @return true if there is a newer version
+ **/
+ virtual bool updateAvailable() const = 0;
+
+ /**
+ * @return true if the update currently available is ignored
+ */
+ virtual bool updateIgnored() const = 0;
+
+ /**
+ * @brief test if the "newest" version of the mod is older than the installed version
+ *
+ * test if there is a newer version of the mod. This does NOT cause
+ * information to be retrieved from the nexus, it will only test version information already
+ * available locally. Use checkAllForUpdate() to update this version information
+ *
+ * @return true if the newest version is older than the installed one
+ **/
+ virtual bool downgradeAvailable() const = 0;
+
+ /**
+ * @brief request an update of nexus description for this mod.
+ *
+ * This requests mod information from the nexus. This is an asynchronous request,
+ * so there is no immediate effect of this call.
+ * Right now, Mod Organizer interprets the "newest version" and "description" from the
+ * response, though the description is only stored in memory
+ *
+ **/
+ virtual bool updateNXMInfo() = 0;
+
+ /**
+ * @brief assign or unassign the specified category
+ *
+ * Every mod can have an arbitrary number of categories assigned to it
+ *
+ * @param categoryID id of the category to set
+ * @param active determines wheter the category is assigned or unassigned
+ * @note this function does not test whether categoryID actually identifies a valid category
+ **/
+ virtual void setCategory(int categoryID, bool active) = 0;
+
+ /**
+ * @brief changes the comments (manually set information displayed in the mod list) for this mod
+ * @param comments new comments
+ */
+ virtual void setComments(const QString &comments) = 0;
+
+ /**
+ * @brief change the notes (manually set information) for this mod
+ * @param notes new notes
+ */
+ virtual void setNotes(const QString &notes) = 0;
+
+ /**
+ * @brief set/change the source game of this mod
+ *
+ * @param gameName the source game shortName
+ */
+ virtual void setGameName(const QString &gameName) = 0;
+
+ /**
+ * @brief set/change the nexus mod id of this mod
+ *
+ * @param modID the nexus mod id
+ **/
+ virtual void setNexusID(int modID) = 0;
+
+ /**
+ * @brief set/change the version of this mod
+ * @param version new version of the mod
+ */
+ virtual void setVersion(const MOBase::VersionInfo &version);
+
+ /**
+ * @brief Controls if mod should be highlighted based on plugin selection
+ * @param isSelected whether or not the plugin has a selected mod
+ **/
+ virtual void setPluginSelected(const bool &isSelected);
+
+ /**
+ * @brief set the newest version of this mod on the nexus
+ *
+ * this can be used to overwrite the version of a mod without actually
+ * updating the mod
+ *
+ * @param version the new version to use
+ * @todo this function should be made obsolete. All queries for mod information should go through
+ * this class so no public function for this change is required
+ **/
+ virtual void setNewestVersion(const MOBase::VersionInfo &version) = 0;
+
+ /**
+ * @brief sets the repository that was used to download the mod
+ */
+ virtual void setRepository(const QString &) {}
+
+ /**
+ * @brief changes/updates the nexus description text
+ * @param description the current description text
+ */
+ virtual void setNexusDescription(const QString &description) = 0;
+
+ /**
+ * @brief sets the file this mod was installed from
+ * @param fileName name of the file
+ */
+ virtual void setInstallationFile(const QString &fileName) = 0;
+
+ /**
+ * @brief sets the category id from a nexus category id. Conversion to MO id happens internally
+ * @param categoryID the nexus category id
+ * @note if a mapping is not possible, the category is set to the default value
+ */
+ virtual void addNexusCategory(int categoryID) = 0;
+
+ virtual void addCategory(const QString &categoryName) override;
+ virtual bool removeCategory(const QString &categoryName) override;
+ virtual QStringList categories() override;
+
+ /**
+ * update the endorsement state for the mod. This only changes the
+ * buffered state, it does not sync with Nexus
+ * @param endorsed the new endorsement state
+ */
+ virtual void setIsEndorsed(bool endorsed) = 0;
+
+ /**
+ * set the mod to "i don't intend to endorse". The mod will not show as unendorsed but can still be endorsed
+ */
+ virtual void setNeverEndorse() = 0;
+
+ /**
+ * update the tracked state for the mod. This only changes the
+ * buffered state, it does not sync with Nexus
+ * @param tracked the new tracked state
+ */
+ virtual void setIsTracked(bool tracked) = 0;
+
+ /**
+ * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices
+ * @return true if the mod was successfully removed
+ **/
+ virtual bool remove() = 0;
+
+ /**
+ * @brief endorse or un-endorse the mod. This will sync with nexus!
+ * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed.
+ * @note if doEndorse doesn't differ from the current value, nothing happens.
+ */
+ virtual void endorse(bool doEndorse) = 0;
+
+ /**
+ * @brief track or untrack the mod. This will sync with nexus!
+ * @param doTrack if true, the mod is tracked, if false, it's untracked.
+ * @note if doTrack doesn't differ from the current value, nothing happens.
+ */
+ virtual void track(bool doTrack) = 0;
+
+ /**
+ * @brief clear all caches held for this mod
+ */
+ virtual void clearCaches() {}
+
+ /**
+ * @brief getter for the mod name
+ *
+ * @return the mod name
+ **/
+ virtual QString name() const = 0;
+
+ /**
+ * @brief getter for an internal name. This is usually the same as the regular name, but with special mod types it might be
+ * this is used to distinguish between mods that have the same visible name
+ * @return internal mod name
+ */
+ virtual QString internalName() const { return name(); }
+
+ /**
+ * @brief getter for the mod path
+ *
+ * @return the (absolute) path to the mod
+ **/
+ virtual QString absolutePath() const = 0;
+
+ /**
+ * @brief getter for the installation file
+ *
+ * @return file used to install this mod from
+ */
+ virtual QString getInstallationFile() const = 0;
+
+ /**
+ * @return version object for machine based comparisons
+ **/
+ virtual MOBase::VersionInfo getVersion() const { return m_Version; }
+
+ /**
+ * @brief getter for the newest version number of this mod
+ *
+ * @return newest version of the mod
+ **/
+ virtual MOBase::VersionInfo getNewestVersion() const = 0;
+
+ /**
+ * @return the repository from which the file was downloaded. Only relevant regular mods
+ */
+ virtual QString repository() const { return ""; }
+
+ /**
+ * @brief ignore the newest version for updates
+ */
+ virtual void ignoreUpdate(bool ignore) = 0;
+
+ /**
+ * @brief getter for the nexus mod id
+ *
+ * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist
+ **/
+ virtual int getNexusID() const = 0;
+
+ /**
+ * @brief getter for the source game repository
+ *
+ * @return the source game repository. should default to the active game.
+ **/
+ virtual QString getGameName() const = 0;
+
+ /**
+ * @return the fixed priority of mods of this type or INT_MIN if the priority of mods
+ * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods
+ * or INT_MAX to force priority above all user-modifiables
+ */
+ virtual int getFixedPriority() const = 0;
+
+ /**
+ * @return true if the mod is always enabled
+ */
+ virtual bool alwaysEnabled() const { return false; }
+
+ /**
+ * @return true if the mod can be updated
+ */
+ virtual bool canBeUpdated() const { return false; }
+
+ /**
+ * @return the mod update check expiration date
+ */
+ virtual QDateTime getExpires() const = 0;
+
+ /**
+ * @return true if the mod can be enabled/disabled
+ */
+ virtual bool canBeEnabled() const { return false; }
+
+ /**
+ * @return a list of flags for this mod
+ */
+ virtual std::vector<EFlag> getFlags() const = 0;
+
+ /**
+ * @return a list of content types contained in a mod
+ */
+ virtual std::vector<EContent> getContents() const { return std::vector<EContent>(); }
+
+ /**
+ * @brief test if the specified flag is set for this mod
+ * @param flag the flag to test
+ * @return true if the flag is set, false otherwise
+ */
+ bool hasFlag(EFlag flag) const;
+
+ /**
+ * @brief test if the mods contains the specified content
+ * @param content the content to test
+ * @return true if the content is there, false otherwise
+ */
+ bool hasContent(ModInfo::EContent content) const;
+
+ /**
+ * @return an indicator if and how this mod should be highlighted by the UI
+ */
+ virtual int getHighlight() const { return HIGHLIGHT_NONE; }
+
+ /**
+ * @return list of names of ini tweaks
+ **/
+ virtual std::vector<QString> getIniTweaks() const = 0;
+
+ /**
+ * @return a description about the mod, to be displayed in the ui
+ */
+ virtual QString getDescription() const = 0;
+
+ /**
+ * @return the nexus file status (aka category ID)
+ */
+ virtual int getNexusFileStatus() const = 0;
+
+
+ /**
+ * @brief sets the file status (category ID) from Nexus
+ * @param status the status id of the installed file
+ */
+ virtual void setNexusFileStatus(int status) = 0;
+
+ /**
+ * @return comments for this mod
+ */
+ virtual QString comments() const = 0;
+
+ /**
+ * @return notes for this mod
+ */
+ virtual QString notes() const = 0;
+
+ /**
+ * @return creation time of this mod
+ */
+ virtual QDateTime creationTime() const = 0;
+
+ /**
+ * @return nexus description of the mod (html)
+ */
+ virtual QString getNexusDescription() const = 0;
+
+ /**
+ * @brief get the last time nexus was checked for file updates on this mod
+ */
+ virtual QDateTime getLastNexusUpdate() const = 0;
+
+ /**
+ * @brief set the last time nexus was checked for file updates on this mod
+ */
+ virtual void setLastNexusUpdate(QDateTime time) = 0;
+
+ /**
+ * @return last time nexus was queried for infos on this mod
+ */
+ virtual QDateTime getLastNexusQuery() const = 0;
+
+ /**
+ * @brief set the last time nexus was queried for info on this mod
+ */
+ virtual void setLastNexusQuery(QDateTime time) = 0;
+
+ /**
+ * @return last time the mod was updated on Nexus
+ */
+ virtual QDateTime getNexusLastModified() const = 0;
+
+ /**
+ * @brief set the last time the mod was updated on Nexus
+ */
+ virtual void setNexusLastModified(QDateTime time) = 0;
+
+ /**
+ * @return a list of files that, if they exist in the data directory are treated as files in THIS mod
+ */
+ virtual QStringList stealFiles() const { return QStringList(); }
+
+ /**
+ * @return a list of archives belonging to this mod (as absolute file paths)
+ */
+ virtual QStringList archives(bool checkOnDisk = false) = 0;
+
+ /*
+ *@return the color choosen by the user for the mod/separator
+ */
+ virtual QColor getColor() { return QColor(); }
+
+ /*
+ *@return true if the color has been set successfully.
+ */
+ virtual void setColor(QColor color) { }
+
+ /**
+ * @brief adds the information that a file has been installed into this mod
+ * @param modId id of the mod installed
+ * @param fileId id of the file installed
+ */
+ virtual void addInstalledFile(int modId, int fileId) = 0;
+
+ /**
+ * @brief test if the mod belongs to the specified category
+ *
+ * @param categoryID the category to test for.
+ * @return true if the mod belongs to the specified category
+ * @note this does not verify the id actually identifies a category
+ **/
+ bool categorySet(int categoryID) const;
+
+ /**
+ * @brief retrive the whole list of categories (as ids) this mod belongs to
+ *
+ * @return list of categories
+ **/
+ const std::set<int> &getCategories() const { return m_Categories; }
+
+ /**
+ * @return id of the primary category of this mod
+ */
+ int getPrimaryCategory() const { return m_PrimaryCategory; }
+
+ /**
+ * @brief sets the new primary category of the mod
+ * @param categoryID the category to set
+ */
+ virtual void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; }
+
+ /**
+ * @return true if this mod is considered "valid", that is: it contains data used by the game
+ **/
+ virtual bool isValid() const { return m_Valid; }
+
+ /**
+ * @return true if the file has been endorsed on nexus
+ */
+ virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; }
+
+ /**
+ * @return true if the file is being tracked on nexus
+ */
+ virtual ETrackedState trackedState() const { return TRACKED_FALSE; }
+
+ /**
+ * @brief updates the valid-flag for this mod
+ */
+ void testValid();
+
+ /**
+ * @brief updates the mod to flag it as converted in order to ignore the alternate game warning
+ */
+ virtual void markConverted(bool converted) {}
+
+ /**
+ * @brief updates the mod to flag it as valid in order to ignore the invalid game data flag
+ */
+ virtual void markValidated(bool validated) {}
+
+ /**
+ * @brief reads meta information from disk
+ */
+ virtual void readMeta() {}
+
+ /**
+ * @brief stores meta information back to disk
+ */
+ virtual void saveMeta() {}
+
+ /**
+ * @return retrieve list of mods (as mod index) that are overwritten by this one. Updates may be delayed
+ */
+ virtual std::set<unsigned int> getModOverwrite() { return std::set<unsigned int>(); }
+
+ /**
+ * @return list of mods (as mod index) that overwrite this one. Updates may be delayed
+ */
+ virtual std::set<unsigned int> getModOverwritten() { return std::set<unsigned int>(); }
+
+ /**
+ * @return retrieve list of mods (as mod index) with archives that are overwritten by this one. Updates may be delayed
+ */
+ virtual std::set<unsigned int> getModArchiveOverwrite() { return std::set<unsigned int>(); }
+
+ /**
+ * @return list of mods (as mod index) with archives that overwrite this one. Updates may be delayed
+ */
+ virtual std::set<unsigned int> getModArchiveOverwritten() { return std::set<unsigned int>(); }
+
+ /**
+ * @return retrieve list of mods (as mod index) with archives that are overwritten by thos mod's loose files. Updates may be delayed
+ */
+ virtual std::set<unsigned int> getModArchiveLooseOverwrite() { return std::set<unsigned int>(); }
+
+ /**
+ * @return list of mods (as mod index) with loose files that overwrite this one's archive files. Updates may be delayed
+ */
+ virtual std::set<unsigned int> getModArchiveLooseOverwritten() { return std::set<unsigned int>(); }
+
+ /**
+ * @brief update conflict information
+ */
+ virtual void doConflictCheck() const {}
+
+ /**
+ * @brief set the URL for a mod
+ */
+ virtual void setURL(QString const &) {}
+
+ /**
+ * @returns the URL for a mod
+ */
+ virtual QString getURL() const { return ""; }
+
+signals:
+
+ /**
+ * @brief emitted whenever the information of a mod changes
+ *
+ * @param success true if the mod details were updated successfully, false if not
+ **/
+ void modDetailsUpdated(bool success);
+
+protected:
+
+ ModInfo(PluginContainer *pluginContainer);
+
+ static void updateIndices();
+ static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS);
+
+private:
+
+ static void createFromOverwrite(PluginContainer *pluginContainer,
+ MOShared::DirectoryEntry **directoryStructure);
+
+protected:
+
+ static std::vector<ModInfo::Ptr> s_Collection;
+ static std::map<QString, unsigned int> s_ModsByName;
+
+ int m_PrimaryCategory;
+ std::set<int> m_Categories;
+
+ MOBase::VersionInfo m_Version;
+
+ bool m_PluginSelected = false;
+
+private:
+
+ static QMutex s_Mutex;
+ static std::map<std::pair<QString, int>, std::vector<unsigned int> > s_ModsByModID;
+ static int s_NextID;
+
+ bool m_Valid;
+
+};
+
+
+#endif // MODINFO_H
diff --git a/src/modinfobackup.h b/src/modinfobackup.h
index da1fcd4a..cab613c9 100644
--- a/src/modinfobackup.h
+++ b/src/modinfobackup.h
@@ -14,17 +14,26 @@ public:
virtual bool updateIgnored() const { return false; }
virtual bool downgradeAvailable() const { return false; }
virtual bool updateNXMInfo() { return false; }
- virtual void setGameName(QString) {}
+ virtual void setGameName(const QString&) {}
virtual void setNexusID(int) {}
virtual void endorse(bool) {}
+ virtual void parseNexusInfo() {}
virtual int getFixedPriority() const { return -1; }
virtual void ignoreUpdate(bool) {}
virtual bool canBeUpdated() const { return false; }
+ virtual QDateTime getExpires() const { return QDateTime(); }
virtual bool canBeEnabled() const { return false; }
virtual std::vector<QString> getIniTweaks() const { return std::vector<QString>(); }
virtual std::vector<EFlag> getFlags() const;
virtual QString getDescription() const;
+ virtual int getNexusFileStatus() const { return 0; }
+ virtual void setNexusFileStatus(int) {}
virtual QDateTime getLastNexusQuery() const { return QDateTime(); }
+ virtual void setLastNexusQuery(QDateTime) {}
+ virtual QDateTime getLastNexusUpdate() const { return QDateTime(); }
+ virtual void setLastNexusUpdate(QDateTime) {}
+ virtual QDateTime getNexusLastModified() const { return QDateTime(); }
+ virtual void setNexusLastModified(QDateTime) {}
virtual void getNexusFiles(QList<MOBase::ModRepositoryFileInfo*>::const_iterator&,
QList<MOBase::ModRepositoryFileInfo*>::const_iterator&) {}
virtual QString getNexusDescription() const { return QString(); }
diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp
index 416eca58..0e41b10e 100644
--- a/src/modinfodialog.cpp
+++ b/src/modinfodialog.cpp
@@ -1,1552 +1,1511 @@
-/*
-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 "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 "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>
-
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-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;
-};
-
-
-static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS)
-{
- return LHS.m_SortValue < RHS.m_SortValue;
-}
-
-
-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)
-{
- 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);
-
- if (ui->tabWidget->currentIndex() == TAB_NEXUS) {
- activateNexusTab();
- }
-
- 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;
- }
- }
-}
-
-
-ModInfoDialog::~ModInfoDialog()
-{
- m_ModInfo->setComments(ui->commentsEdit->text());
- m_ModInfo->setNotes(ui->notesEdit->toPlainText());
- 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;
-}
-
-
-void ModInfoDialog::initINITweaks()
-{
- 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();
-}
-
-void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo)
-{
- ui->fileTree = findChild<QTreeView*>("fileTree");
-
- 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);
-
- 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()));
-}
-
-
-int ModInfoDialog::tabIndex(const QString &tabId)
-{
- for (int i = 0; i < ui->tabWidget->count(); ++i) {
- if (ui->tabWidget->widget(i)->objectName() == tabId) {
- return i;
- }
- }
- 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);
- }
- ui->tabWidget->blockSignals(false);
-}
-
-
-QByteArray ModInfoDialog::saveTabState() const
-{
- 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;
-}
-
-
-void ModInfoDialog::refreshLists()
-{
- 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::wstring>> alternatives = (*iter)->getAlternatives();
- if (!alternatives.empty()) {
- std::wostringstream altString;
- for (std::vector<std::pair<int, std::wstring>>::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.begin()->first).getName()));
- item->setData(1, Qt::UserRole + 1, alternatives.begin()->first);
- item->setData(1, Qt::UserRole + 2, archive);
- 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);
- 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);
-}
-
-
-void ModInfoDialog::addCategories(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)) {
- addCategories(factory, enabledCategories, newItem, categoryID);
- }
- root->addChild(newItem);
- }
-}
-
-
-void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode)
-{
- 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);
- }
-}
-
-
-void ModInfoDialog::on_closeButton_clicked()
-{
- if (allowNavigateFromTXT() && allowNavigateFromINI()) {
- this->close();
- }
-}
-
-
-
-QString ModInfoDialog::getModVersion() const
-{
- return m_Settings->value("version", "").toString();
-}
-
-
-const int ModInfoDialog::getModID() const
-{
- return m_Settings->value("modid", 0).toInt();
-}
-
-void ModInfoDialog::openTab(int tab)
-{
- QTabWidget *tabWidget = findChild<QTabWidget*>("tabWidget");
- if (tabWidget->isTabEnabled(tab)) {
- tabWidget->setCurrentIndex(tab);
- }
-}
-
-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));
-}
-
-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();
- }
- }
- return true;
-}
-
-
-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;
-}
-
-
-void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
-{
- QString fullPath = m_RootPath + "/" + current->text();
-
- 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;
- }
-
- if (allowNavigateFromTXT()) {
- openTextFile(fullPath);
- } else {
- ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current);
- }
-}
-
-
-void ModInfoDialog::openTextFile(const QString &fileName)
-{
- QString encoding;
- ui->textFileView->setText(MOBase::readFileText(fileName, &encoding));
- ui->textFileView->setProperty("currentFile", fileName);
- ui->textFileView->setProperty("encoding", encoding);
- ui->saveTXTButton->setEnabled(false);
-}
-
-
-void ModInfoDialog::openIniFile(const QString &fileName)
-{
- QFile iniFile(fileName);
- iniFile.open(QIODevice::ReadOnly);
- QByteArray buffer = iniFile.readAll();
-
- 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();
-
- ui->saveButton->setEnabled(false);
-}
-
-
-void ModInfoDialog::saveIniTweaks()
-{
- m_Settings->beginWriteArray("INI Tweaks");
-
- 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());
- }
- }
- m_Settings->endArray();
-}
-
-
-void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
-{
- 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
- return;
- }
-
- if (allowNavigateFromINI()) {
- openIniFile(fullPath);
- } else {
- ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current);
- }
-}
-
-
-void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
-{
- 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
- return;
- }
-
- if (allowNavigateFromINI()) {
- openIniFile(fullPath);
- } else {
- ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current);
- }
-
-}
-
-
-void ModInfoDialog::on_saveButton_clicked()
-{
- saveCurrentIniFile();
-}
-
-
-void ModInfoDialog::on_saveTXTButton_clicked()
-{
- saveCurrentTextFile();
-}
-
-
-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);
-}
-
-
-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");
- }
- ui->saveButton->setEnabled(false);
-}
-
-
-void ModInfoDialog::on_iniFileView_textChanged()
-{
- QPushButton* saveButton = findChild<QPushButton*>("saveButton");
- saveButton->setEnabled(true);
-}
-
-
-void ModInfoDialog::on_textFileView_textChanged()
-{
- ui->saveTXTButton->setEnabled(true);
-}
-
-
-void ModInfoDialog::on_activateESP_clicked()
-{
- QListWidget *activeESPList = findChild<QListWidget*>("activeESPList");
- QListWidget *inactiveESPList = findChild<QListWidget*>("inactiveESPList");
-
- int selectedRow = inactiveESPList->currentRow();
- if (selectedRow < 0) {
- return;
- }
-
- QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow);
-
- 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;
- }
- }
-
- if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) {
- activeESPList->addItem(selectedItem);
- if (renamed) {
- selectedItem->setData(Qt::UserRole, QVariant());
- }
- } 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");
-
- int selectedRow = activeESPList->currentRow();
- if (selectedRow < 0) {
- 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;
- }
- }
- }
-
- if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) {
- inactiveESPList->addItem(selectedItem);
- } else {
- activeESPList->insertItem(selectedRow, selectedItem);
- }
-}
-
-void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link)
-{
- emit linkActivated(link);
-}
-
-void ModInfoDialog::linkClicked(const QUrl &url)
-{
- //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)) {
-
- emit linkActivated(url.toString());
- } else {
- ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- }
-}
-
-void ModInfoDialog::linkClicked(QString url)
-{
- emit linkActivated(url);
-}
-
-
-void ModInfoDialog::refreshNexusData(int modID)
-{
- if ((!m_RequestStarted) && (modID > 0)) {
- m_RequestStarted = true;
-
- m_ModInfo->updateNXMInfo();
-
- MessageDialog::showMessage(tr("Info requested, please wait"), this);
- }
-}
-
-
-/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID)
-{
- std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
- if (idIter == m_RequestIDs.end()) {
- return;
- } else {
- m_RequestIDs.erase(idIter);
- }
-
- QVariantMap result = resultData.toMap();
-
- if (!result["description"].isNull()) {
- QString descriptionAsHTML =
- QString("<html>"
- "<head><style>body {background: #707070; } a { color: #5EA2E5; }</style></head>"
- "<body>%1</body>"
- "</html>").arg(BBCode::convertToHTML(result["description"].toString()));
-
-// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString());
- ui->descriptionView->setHtml(descriptionAsHTML);
- } else {
- ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)")));
- }
-
- QLineEdit *versionEdit = findChild<QLineEdit*>("versionEdit");
- QString version = result["version"].toString();
-
- if (!version.isEmpty()) {
- m_ModInfo->setNewestVersion(version);
-
- VersionInfo currentVersion(versionEdit->text());
- VersionInfo newestVersion(version);
-
- QPalette versionColor;
- if (currentVersion < newestVersion) {
- versionColor.setColor(QPalette::Text, Qt::red);
- versionEdit->setToolTip(tr("Current Version: %1").arg(version));
- } else {
- versionColor.setColor(QPalette::Text, Qt::green);
- versionEdit->setToolTip(tr("No update available"));
- }
- versionEdit->setPalette(versionColor);
- }
-}*/
-
-
-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("Misc");
- default: return tr("Unknown");
- }
-}
-
-
-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"));
- }
-// ui->versionEdit->setPalette(versionColor);
-}
-
-
-void ModInfoDialog::modDetailsUpdated(bool success)
-{
- if (success) {
- QString nexusDescription = m_ModInfo->getNexusDescription();
- if (!nexusDescription.isEmpty()) {
- /* QString input =
- "[size=20]sizetest[/size]\r\n"
- "[COLOR=yellow]colortest[/COLOR]\r\n"
- "[center]centertest[/center]\r\n"
- "[quote]quotetest 1[/quote]\r\n"
- "[quote=bla]quotetest 2[/quote]\r\n"
- "[url]www.skyrimnexus.com[/url]\r\n"
- "[url=www.skyrimnexus.com]urltest 2[/url]\r\n"
- "[ol]\r\n"
- "[li]item 2[/li]"
- "[*]item 1\r\n"
- "[/ol]\r\n"
- "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n"
- "[table][tr][th]headertest1[/th]"
- "[th]headertest2[/th][/tr]"
- "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]"
- "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]"
- "[email=\"sherb@gmx.net\"]mail me[/email]";
- ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/
-
- QString descriptionAsHTML =
- QString("<html>"
- "<head><style>body {background: #707070; } a { color: #5EA2E5; }</style></head>"
- "<body>%1</body>"
- "</html>").arg(BBCode::convertToHTML(nexusDescription));
-
- ui->descriptionView->page()->setHtml(descriptionAsHTML);
-
- // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString());
- // ui->descriptionView->setHtml(descriptionAsHTML);
- } else {
- // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)")));
- ui->descriptionView->page()->setHtml(tr("(description incomplete, please visit nexus)"));
- }
-
- updateVersionColor();
- }
-}
-
-
-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);
-
- if (m_ModInfo->getNexusDescription().isEmpty() ||
- QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) {
- refreshNexusData(modID);
- } else {
- this->modDetailsUpdated(true);
- }
- }
- QLineEdit *versionEdit = findChild<QLineEdit*>("versionEdit");
- QString currentVersion = m_Settings->value("version", "0.0").toString();
- versionEdit->setText(currentVersion);
-}
-
-
-void ModInfoDialog::on_tabWidget_currentChanged(int index)
-{
- if (m_RealTabPos[index] == TAB_NEXUS) {
- activateNexusTab();
- }
-}
-
-
-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);
-
- ui->descriptionView->page()->setHtml("");
- if (modID != 0) {
- m_RequestStarted = false;
- refreshNexusData(modID);
- }
- }
-}
-
-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;
- }
- }
-}
-
-void ModInfoDialog::on_versionEdit_editingFinished()
-{
- VersionInfo version(ui->versionEdit->text());
- m_ModInfo->setVersion(version);
- updateVersionColor();
-}
-
-
-bool ModInfoDialog::recursiveDelete(const QModelIndex &index)
-{
- 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;
- }
- return true;
-}
-
-
-void ModInfoDialog::on_openInExplorerButton_clicked()
-{
- ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
-}
-
-void ModInfoDialog::deleteFile(const QModelIndex &index)
-{
-
- 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));
- }
-}
-
-void ModInfoDialog::delete_activated()
-{
- if (ui->fileTree->hasFocus()) {
- QItemSelectionModel *selection = ui->fileTree->selectionModel();
-
- if (selection->hasSelection() && selection->selectedRows().count() >= 1) {
-
- 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;
- }
- }
-
- foreach(QModelIndex index, selection->selectedRows()) {
- deleteFile(index);
- }
- }
- }
-}
-
-void ModInfoDialog::deleteTriggered()
-{
- 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) {
- return;
- }
- }
-
- foreach(QModelIndex index, m_FileSelection) {
- deleteFile(index);
- }
-}
-
-
-void ModInfoDialog::renameTriggered()
-{
- QModelIndex selection = m_FileSelection.at(0);
- QModelIndex index = selection.sibling(selection.row(), 0);
- if (!index.isValid() || m_FileSystemModel->isReadOnly()) {
- return;
- }
-
- ui->fileTree->edit(index);
-}
-
-
-void ModInfoDialog::hideTriggered()
-{
- 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);
- }
- }
-}
-
-
-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);
- }
- }
-}
-
-
-void ModInfoDialog::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 ((unsigned long long)res <= 32) {
- qCritical("failed to invoke %s: %d", fileName.toUtf8().constData(), res);
- }
-}
-
-
-void ModInfoDialog::openTriggered()
-{
- foreach(QModelIndex idx, m_FileSelection) {
- openFile(idx);
- }
-}
-
-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 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);
- }
-
- QModelIndex newIndex = m_FileSystemModel->mkdir(index, name);
- if (!newIndex.isValid()) {
- reportError(tr("Failed to create \"%1\"").arg(name));
- return;
- }
-
- ui->fileTree->setCurrentIndex(newIndex);
- ui->fileTree->edit(newIndex);
-}
-
-
-void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos)
-{
- QItemSelectionModel *selectionModel = ui->fileTree->selectionModel();
- m_FileSelection = selectionModel->selectedRows(0);
-
-// m_FileSelection = ui->fileTree->indexAt(pos);
- QMenu menu(ui->fileTree);
-
- menu.addAction(m_NewFolderAction);
-
- bool hasFiles = false;
-
- foreach(QModelIndex idx, m_FileSelection) {
- if (m_FileSystemModel->fileInfo(idx).isFile()) {
- hasFiles = true;
- break;
- }
- }
-
- 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);
- }
- } 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();
-}
-
-
-void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree)
-{
- 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);
- }
- }
-}
-
-
-void ModInfoDialog::refreshPrimaryCategoriesBox()
-{
- 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;
- }
- }
-}
-
-
-void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index)
-{
- if (index != -1) {
- m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt());
- }
-}
-
-
-void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int)
-{
- this->close();
- emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS);
-}
-
-
-bool ModInfoDialog::hideFile(const QString &oldName)
-{
- QString newName = oldName + ModInfo::s_HiddenExt;
-
- 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 {
- 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;
- }
-}
-
-
-bool ModInfoDialog::unhideFile(const QString &oldName)
-{
- 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;
- }
-}
-
-
-void ModInfoDialog::hideConflictFile()
-{
- if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) {
- emit originModified(m_Origin->getID());
- refreshLists();
- }
-}
-
-
-void ModInfoDialog::unhideConflictFile()
-{
- if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) {
- emit originModified(m_Origin->getID());
- refreshLists();
- }
-}
-
-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
-
- // 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 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(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()));
- }
-
- menu.exec(ui->overwriteTree->mapToGlobal(pos));
- }
- }
-}
-
-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()
-{
- m_ModInfo->updateNXMInfo();
-
- MessageDialog::showMessage(tr("Info requested, please wait"), this);
-}
-
-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()
-{
- int currentTab = ui->tabWidget->currentIndex();
- int tab = m_RealTabPos[currentTab];
-
- emit modOpenPrev(tab);
- this->accept();
-}
-
-
-void ModInfoDialog::createTweak()
-{
- 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"));
- 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));
-}
+/*
+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 "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 "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>
+
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+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;
+};
+
+
+static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS)
+{
+ return LHS.m_SortValue < RHS.m_SortValue;
+}
+
+
+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)
+{
+ 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 (ui->tabWidget->currentIndex() == TAB_NEXUS) {
+ activateNexusTab();
+ }
+}
+
+
+ModInfoDialog::~ModInfoDialog()
+{
+ 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;
+}
+
+
+void ModInfoDialog::initINITweaks()
+{
+ 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();
+}
+
+void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo)
+{
+ ui->fileTree = findChild<QTreeView*>("fileTree");
+
+ 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);
+
+ 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()));
+}
+
+
+int ModInfoDialog::tabIndex(const QString &tabId)
+{
+ for (int i = 0; i < ui->tabWidget->count(); ++i) {
+ if (ui->tabWidget->widget(i)->objectName() == tabId) {
+ return i;
+ }
+ }
+ 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);
+ }
+ ui->tabWidget->blockSignals(false);
+}
+
+
+QByteArray ModInfoDialog::saveTabState() const
+{
+ 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;
+}
+
+
+void ModInfoDialog::refreshLists()
+{
+ 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);
+}
+
+
+void ModInfoDialog::addCategories(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)) {
+ addCategories(factory, enabledCategories, newItem, categoryID);
+ }
+ root->addChild(newItem);
+ }
+}
+
+
+void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode)
+{
+ 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);
+ }
+}
+
+
+void ModInfoDialog::on_closeButton_clicked()
+{
+ if (allowNavigateFromTXT() && allowNavigateFromINI()) {
+ this->close();
+ }
+}
+
+
+
+QString ModInfoDialog::getModVersion() const
+{
+ return m_Settings->value("version", "").toString();
+}
+
+
+const int ModInfoDialog::getModID() const
+{
+ return m_Settings->value("modid", 0).toInt();
+}
+
+void ModInfoDialog::openTab(int tab)
+{
+ QTabWidget *tabWidget = findChild<QTabWidget*>("tabWidget");
+ if (tabWidget->isTabEnabled(tab)) {
+ tabWidget->setCurrentIndex(tab);
+ }
+}
+
+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));
+}
+
+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();
+ }
+ }
+ return true;
+}
+
+
+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;
+}
+
+
+void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
+{
+ QString fullPath = m_RootPath + "/" + current->text();
+
+ 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;
+ }
+
+ if (allowNavigateFromTXT()) {
+ openTextFile(fullPath);
+ } else {
+ ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current);
+ }
+}
+
+
+void ModInfoDialog::openTextFile(const QString &fileName)
+{
+ QString encoding;
+ ui->textFileView->setText(MOBase::readFileText(fileName, &encoding));
+ ui->textFileView->setProperty("currentFile", fileName);
+ ui->textFileView->setProperty("encoding", encoding);
+ ui->saveTXTButton->setEnabled(false);
+}
+
+
+void ModInfoDialog::openIniFile(const QString &fileName)
+{
+ QFile iniFile(fileName);
+ iniFile.open(QIODevice::ReadOnly);
+ QByteArray buffer = iniFile.readAll();
+
+ 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();
+
+ ui->saveButton->setEnabled(false);
+}
+
+
+void ModInfoDialog::saveIniTweaks()
+{
+ m_Settings->remove("INI Tweaks");
+ m_Settings->beginWriteArray("INI Tweaks");
+
+ 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());
+ }
+ }
+ m_Settings->endArray();
+}
+
+
+void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
+{
+ 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
+ return;
+ }
+
+ if (allowNavigateFromINI()) {
+ openIniFile(fullPath);
+ } else {
+ ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current);
+ }
+}
+
+
+void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
+{
+ 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
+ return;
+ }
+
+ if (allowNavigateFromINI()) {
+ openIniFile(fullPath);
+ } else {
+ ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current);
+ }
+
+}
+
+
+void ModInfoDialog::on_saveButton_clicked()
+{
+ saveCurrentIniFile();
+}
+
+
+void ModInfoDialog::on_saveTXTButton_clicked()
+{
+ saveCurrentTextFile();
+}
+
+
+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);
+}
+
+
+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");
+ }
+ ui->saveButton->setEnabled(false);
+}
+
+
+void ModInfoDialog::on_iniFileView_textChanged()
+{
+ QPushButton* saveButton = findChild<QPushButton*>("saveButton");
+ saveButton->setEnabled(true);
+}
+
+
+void ModInfoDialog::on_textFileView_textChanged()
+{
+ ui->saveTXTButton->setEnabled(true);
+}
+
+
+void ModInfoDialog::on_activateESP_clicked()
+{
+ QListWidget *activeESPList = findChild<QListWidget*>("activeESPList");
+ QListWidget *inactiveESPList = findChild<QListWidget*>("inactiveESPList");
+
+ int selectedRow = inactiveESPList->currentRow();
+ if (selectedRow < 0) {
+ return;
+ }
+
+ QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow);
+
+ 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;
+ }
+ }
+
+ if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) {
+ activeESPList->addItem(selectedItem);
+ if (renamed) {
+ selectedItem->setData(Qt::UserRole, QVariant());
+ }
+ } 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");
+
+ int selectedRow = activeESPList->currentRow();
+ if (selectedRow < 0) {
+ 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;
+ }
+ }
+ }
+
+ if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) {
+ inactiveESPList->addItem(selectedItem);
+ } else {
+ activeESPList->insertItem(selectedRow, selectedItem);
+ }
+}
+
+void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link)
+{
+ emit linkActivated(link);
+}
+
+void ModInfoDialog::linkClicked(const QUrl &url)
+{
+ //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)) {
+
+ emit linkActivated(url.toString());
+ } else {
+ ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ }
+}
+
+void ModInfoDialog::linkClicked(QString url)
+{
+ emit linkActivated(url);
+}
+
+
+void ModInfoDialog::refreshNexusData(int modID)
+{
+ if ((!m_RequestStarted) && (modID > 0)) {
+ m_RequestStarted = true;
+
+ m_ModInfo->updateNXMInfo();
+
+ MessageDialog::showMessage(tr("Info requested, please wait"), this);
+ }
+}
+
+
+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");
+ }
+}
+
+
+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"));
+ }
+// 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>";
+
+ 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>"));
+ }
+
+ ui->descriptionView->page()->setHtml(descriptionAsHTML);
+
+ updateVersionColor();
+}
+
+
+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);
+
+ if (m_ModInfo->getNexusDescription().isEmpty() || QDateTime::currentDateTimeUtc() >= m_ModInfo->getLastNexusQuery().addDays(1)) {
+ refreshNexusData(modID);
+ } else {
+ modDetailsUpdated(true);
+ }
+ } 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)
+{
+ if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) {
+ activateNexusTab();
+ }
+}
+
+
+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);
+
+ ui->descriptionView->page()->setHtml("");
+ if (modID != 0) {
+ m_RequestStarted = false;
+ refreshNexusData(modID);
+ }
+ }
+}
+
+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;
+ }
+ }
+}
+
+void ModInfoDialog::on_versionEdit_editingFinished()
+{
+ VersionInfo version(ui->versionEdit->text());
+ m_ModInfo->setVersion(version);
+ updateVersionColor();
+}
+
+void ModInfoDialog::on_customUrlLineEdit_editingFinished()
+{
+ m_ModInfo->setURL(ui->customUrlLineEdit->text());
+}
+
+bool ModInfoDialog::recursiveDelete(const QModelIndex &index)
+{
+ 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;
+ }
+ return true;
+}
+
+
+void ModInfoDialog::on_openInExplorerButton_clicked()
+{
+ ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+}
+
+void ModInfoDialog::deleteFile(const QModelIndex &index)
+{
+
+ 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));
+ }
+}
+
+void ModInfoDialog::delete_activated()
+{
+ if (ui->fileTree->hasFocus()) {
+ QItemSelectionModel *selection = ui->fileTree->selectionModel();
+
+ if (selection->hasSelection() && selection->selectedRows().count() >= 1) {
+
+ 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;
+ }
+ }
+
+ foreach(QModelIndex index, selection->selectedRows()) {
+ deleteFile(index);
+ }
+ }
+ }
+}
+
+void ModInfoDialog::deleteTriggered()
+{
+ 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) {
+ return;
+ }
+ }
+
+ foreach(QModelIndex index, m_FileSelection) {
+ deleteFile(index);
+ }
+}
+
+
+void ModInfoDialog::renameTriggered()
+{
+ QModelIndex selection = m_FileSelection.at(0);
+ QModelIndex index = selection.sibling(selection.row(), 0);
+ if (!index.isValid() || m_FileSystemModel->isReadOnly()) {
+ return;
+ }
+
+ ui->fileTree->edit(index);
+}
+
+
+void ModInfoDialog::hideTriggered()
+{
+ 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);
+ }
+ }
+}
+
+
+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);
+ }
+ }
+}
+
+
+void ModInfoDialog::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 ((unsigned long long)res <= 32) {
+ qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res);
+ }
+}
+
+
+void ModInfoDialog::openTriggered()
+{
+ foreach(QModelIndex idx, m_FileSelection) {
+ openFile(idx);
+ }
+}
+
+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 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);
+ }
+
+ QModelIndex newIndex = m_FileSystemModel->mkdir(index, name);
+ if (!newIndex.isValid()) {
+ reportError(tr("Failed to create \"%1\"").arg(name));
+ return;
+ }
+
+ ui->fileTree->setCurrentIndex(newIndex);
+ ui->fileTree->edit(newIndex);
+}
+
+
+void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos)
+{
+ QItemSelectionModel *selectionModel = ui->fileTree->selectionModel();
+ m_FileSelection = selectionModel->selectedRows(0);
+
+// m_FileSelection = ui->fileTree->indexAt(pos);
+ QMenu menu(ui->fileTree);
+
+ menu.addAction(m_NewFolderAction);
+
+ bool hasFiles = false;
+
+ foreach(QModelIndex idx, m_FileSelection) {
+ if (m_FileSystemModel->fileInfo(idx).isFile()) {
+ hasFiles = true;
+ break;
+ }
+ }
+
+ 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);
+ }
+ } 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();
+}
+
+
+void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree)
+{
+ 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);
+ }
+ }
+}
+
+
+void ModInfoDialog::refreshPrimaryCategoriesBox()
+{
+ 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;
+ }
+ }
+}
+
+
+void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index)
+{
+ if (index != -1) {
+ m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt());
+ }
+}
+
+
+void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int)
+{
+ this->close();
+ emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS);
+}
+
+
+bool ModInfoDialog::hideFile(const QString &oldName)
+{
+ QString newName = oldName + ModInfo::s_HiddenExt;
+
+ 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 {
+ 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;
+ }
+}
+
+
+bool ModInfoDialog::unhideFile(const QString &oldName)
+{
+ 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;
+ }
+}
+
+
+void ModInfoDialog::hideConflictFile()
+{
+ if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) {
+ emit originModified(m_Origin->getID());
+ refreshLists();
+ }
+}
+
+
+void ModInfoDialog::unhideConflictFile()
+{
+ if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) {
+ emit originModified(m_Origin->getID());
+ refreshLists();
+ }
+}
+
+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
+
+ // 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 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()));
+ }
+
+ menu.exec(ui->overwriteTree->mapToGlobal(pos));
+ }
+ }
+}
+
+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()
+{
+ int currentTab = ui->tabWidget->currentIndex();
+ int tab = m_RealTabPos[currentTab];
+
+ emit modOpenPrev(tab);
+ this->accept();
+}
+
+
+void ModInfoDialog::createTweak()
+{
+ 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"));
+ 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));
+}
diff --git a/src/modinfodialog.h b/src/modinfodialog.h
index f3bc48b4..4c731c00 100644
--- a/src/modinfodialog.h
+++ b/src/modinfodialog.h
@@ -78,7 +78,8 @@ public:
* @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);
+ explicit ModInfoDialog(ModInfo::Ptr modInfo, const MOShared::DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent = 0);
+
~ModInfoDialog();
/**
@@ -185,6 +186,7 @@ private slots:
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);
diff --git a/src/modinfodialog.ui b/src/modinfodialog.ui
index 736214f4..0211e704 100644
--- a/src/modinfodialog.ui
+++ b/src/modinfodialog.ui
@@ -774,6 +774,20 @@ p, li { white-space: pre-wrap; }
</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>
+ </item>
</layout>
</widget>
<widget class="QWidget" name="tabNotes">
@@ -802,6 +816,9 @@ p, li { white-space: pre-wrap; }
<property name="whatsThis">
<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>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
<property name="placeholderText">
<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>
diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h
index 93061294..72fbb04f 100644
--- a/src/modinfoforeign.h
+++ b/src/modinfoforeign.h
@@ -20,7 +20,7 @@ public:
virtual bool setName(const QString&) { return false; }
virtual void setComments(const QString&) {}
virtual void setNotes(const QString&) {}
- virtual void setGameName(QString) {}
+ virtual void setGameName(const QString&) {}
virtual void setNexusID(int) {}
virtual void setNewestVersion(const MOBase::VersionInfo&) {}
virtual void ignoreUpdate(bool) {}
@@ -29,8 +29,11 @@ public:
virtual void addNexusCategory(int) {}
virtual void setIsEndorsed(bool) {}
virtual void setNeverEndorse() {}
+ virtual void setIsTracked(bool) {}
virtual bool remove() { return false; }
virtual void endorse(bool) {}
+ virtual void track(bool) {}
+ virtual void parseNexusInfo() {}
virtual bool isEmpty() const { return false; }
virtual QString name() const;
virtual QString internalName() const { return name(); }
@@ -42,14 +45,22 @@ public:
virtual QString getInstallationFile() const { return ""; }
virtual QString getGameName() const { return ""; }
virtual int getNexusID() const { return -1; }
+ virtual QDateTime getExpires() const { return QDateTime(); }
virtual std::vector<QString> getIniTweaks() const { return std::vector<QString>(); }
virtual std::vector<ModInfo::EFlag> getFlags() const;
virtual int getHighlight() const;
virtual QString getDescription() const;
+ virtual int getNexusFileStatus() const { return 0; }
+ virtual void setNexusFileStatus(int) {}
+ virtual QDateTime getLastNexusUpdate() const { return QDateTime(); }
+ virtual void setLastNexusUpdate(QDateTime) {}
virtual QDateTime getLastNexusQuery() const { return QDateTime(); }
+ virtual void setLastNexusQuery(QDateTime) {}
+ virtual QDateTime getNexusLastModified() const { return QDateTime(); }
+ virtual void setNexusLastModified(QDateTime) {}
virtual QString getNexusDescription() const { return QString(); }
virtual int getFixedPriority() const { return INT_MIN; }
- virtual QStringList archives() const { return m_Archives; }
+ virtual QStringList archives(bool checkOnDisk = false) { return m_Archives; }
virtual QStringList stealFiles() const { return m_Archives + QStringList(m_ReferenceFile); }
virtual bool alwaysEnabled() const { return true; }
virtual void addInstalledFile(int, int) {}
diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp
index 742ae384..37c8c650 100644
--- a/src/modinfooverwrite.cpp
+++ b/src/modinfooverwrite.cpp
@@ -6,7 +6,8 @@
#include <QApplication>
#include <QDirIterator>
-ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer) : ModInfo(pluginContainer)
+ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure)
+ : ModInfoWithConflictInfo(pluginContainer, directoryStructure)
{
testValid();
}
@@ -31,6 +32,8 @@ std::vector<ModInfo::EFlag> ModInfoOverwrite::getFlags() const
result.push_back(FLAG_OVERWRITE);
if (m_PluginSelected)
result.push_back(FLAG_PLUGIN_SELECTED);
+ for (auto flag : ModInfoWithConflictInfo::getFlags())
+ result.push_back(flag);
return result;
}
@@ -49,7 +52,7 @@ QString ModInfoOverwrite::getDescription() const
"modified (i.e. by the construction kit)");
}
-QStringList ModInfoOverwrite::archives() const
+QStringList ModInfoOverwrite::archives(bool checkOnDisk)
{
QStringList result;
QDir dir(this->absolutePath());
diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h
index 4b98d715..c5f58c2e 100644
--- a/src/modinfooverwrite.h
+++ b/src/modinfooverwrite.h
@@ -1,11 +1,11 @@
#ifndef MODINFOOVERWRITE_H
#define MODINFOOVERWRITE_H
-#include "modinfo.h"
+#include "modinfowithconflictinfo.h"
#include <QDateTime>
-class ModInfoOverwrite : public ModInfo
+class ModInfoOverwrite : public ModInfoWithConflictInfo
{
Q_OBJECT
@@ -22,7 +22,7 @@ public:
virtual bool setName(const QString&) { return false; }
virtual void setComments(const QString&) {}
virtual void setNotes(const QString&) {}
- virtual void setGameName(QString) {}
+ virtual void setGameName(const QString&) {}
virtual void setNexusID(int) {}
virtual void setNewestVersion(const MOBase::VersionInfo&) {}
virtual void ignoreUpdate(bool) {}
@@ -31,8 +31,11 @@ public:
virtual void addNexusCategory(int) {}
virtual void setIsEndorsed(bool) {}
virtual void setNeverEndorse() {}
+ virtual void setIsTracked(bool) {}
virtual bool remove() { return false; }
virtual void endorse(bool) {}
+ virtual void track(bool) {}
+ virtual void parseNexusInfo() {}
virtual bool alwaysEnabled() const { return true; }
virtual bool isEmpty() const;
virtual QString name() const { return "Overwrite"; }
@@ -45,18 +48,26 @@ public:
virtual int getFixedPriority() const { return INT_MAX; }
virtual QString getGameName() const { return ""; }
virtual int getNexusID() const { return -1; }
+ virtual QDateTime getExpires() const { return QDateTime(); }
virtual std::vector<QString> getIniTweaks() const { return std::vector<QString>(); }
virtual std::vector<ModInfo::EFlag> getFlags() const;
virtual int getHighlight() const;
virtual QString getDescription() const;
+ virtual int getNexusFileStatus() const { return 0; }
+ virtual void setNexusFileStatus(int) {}
+ virtual QDateTime getLastNexusUpdate() const { return QDateTime(); }
+ virtual void setLastNexusUpdate(QDateTime) {}
virtual QDateTime getLastNexusQuery() const { return QDateTime(); }
+ virtual void setLastNexusQuery(QDateTime) {}
+ virtual QDateTime getNexusLastModified() const { return QDateTime(); }
+ virtual void setNexusLastModified(QDateTime) {}
virtual QString getNexusDescription() const { return QString(); }
- virtual QStringList archives() const;
+ virtual QStringList archives(bool checkOnDisk = false);
virtual void addInstalledFile(int, int) {}
private:
- ModInfoOverwrite(PluginContainer *pluginContainer);
+ ModInfoOverwrite(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure );
};
diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp
index 6a52d3df..a271c4e8 100644
--- a/src/modinforegular.cpp
+++ b/src/modinforegular.cpp
@@ -4,6 +4,7 @@
#include "messagedialog.h"
#include "report.h"
#include "scriptextender.h"
+#include "settings.h"
#include <QApplication>
#include <QDirIterator>
@@ -34,6 +35,7 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa
, m_Validated(false)
, m_MetaInfoChanged(false)
, m_EndorsedState(ENDORSED_UNKNOWN)
+ , m_TrackedState(TRACKED_UNKNOWN)
, m_NexusBridge(pluginContainer)
{
testValid();
@@ -44,12 +46,20 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa
if (!game->primarySources().contains(m_GameName, Qt::CaseInsensitive))
m_IsAlternate = true;
+ //populate m_Archives
+ m_Archives = QStringList();
+ if (Settings::instance().archiveParsing()) {
+ archives(true);
+ }
+
connect(&m_NexusBridge, SIGNAL(descriptionAvailable(QString,int,QVariant,QVariant))
, this, SLOT(nxmDescriptionAvailable(QString,int,QVariant,QVariant)));
connect(&m_NexusBridge, SIGNAL(endorsementToggled(QString,int,QVariant,QVariant))
, this, SLOT(nxmEndorsementToggled(QString,int,QVariant,QVariant)));
- connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QString))
- , this, SLOT(nxmRequestFailed(QString,int,int,QVariant,QString)));
+ connect(&m_NexusBridge, SIGNAL(trackingToggled(QString,int,QVariant,bool))
+ , this, SLOT(nxmTrackingToggled(QString,int,QVariant,bool)));
+ connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QNetworkReply::NetworkError,QString))
+ , this, SLOT(nxmRequestFailed(QString,int,int,QVariant,QNetworkReply::NetworkError,QString)));
}
@@ -59,7 +69,7 @@ ModInfoRegular::~ModInfoRegular()
saveMeta();
} catch (const std::exception &e) {
qCritical("failed to save meta information for \"%s\": %s",
- m_Name.toUtf8().constData(), e.what());
+ qUtf8Printable(m_Name), e.what());
}
}
@@ -86,12 +96,16 @@ void ModInfoRegular::readMeta()
m_IgnoredVersion = metaFile.value("ignoredVersion", "").toString();
m_InstallationFile = metaFile.value("installationFile", "").toString();
m_NexusDescription = metaFile.value("nexusDescription", "").toString();
+ m_NexusFileStatus = metaFile.value("nexusFileStatus", "1").toInt();
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();
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);
m_Color = metaFile.value("color",QColor()).value<QColor>();
+ m_TrackedState = metaFile.value("tracked", false).toBool() ? TRACKED_TRUE : TRACKED_FALSE;
if (metaFile.contains("endorsed")) {
if (metaFile.value("endorsed").canConvert<int>()) {
switch (metaFile.value("endorsed").toInt()) {
@@ -104,7 +118,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);
@@ -153,14 +167,19 @@ void ModInfoRegular::saveMeta()
metaFile.setValue("notes", m_Notes);
metaFile.setValue("nexusDescription", m_NexusDescription);
metaFile.setValue("url", m_URL);
+ metaFile.setValue("nexusFileStatus", m_NexusFileStatus);
metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate));
+ metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate));
+ metaFile.setValue("nexusLastModified", m_NexusLastModified.toString(Qt::ISODate));
metaFile.setValue("converted", m_Converted);
metaFile.setValue("validated", m_Validated);
metaFile.setValue("color", m_Color);
if (m_EndorsedState != ENDORSED_UNKNOWN) {
metaFile.setValue("endorsed", m_EndorsedState);
}
+ metaFile.setValue("tracked", m_TrackedState);
+ metaFile.remove("installedFiles");
metaFile.beginWriteArray("installedFiles");
int idx = 0;
for (auto iter = m_InstalledFileIDs.begin(); iter != m_InstalledFileIDs.end(); ++iter) {
@@ -189,6 +208,9 @@ bool ModInfoRegular::updateAvailable() const
if (m_IgnoredVersion.isValid() && (m_IgnoredVersion == m_NewestVersion)) {
return false;
}
+ if (m_NexusFileStatus == 4 || m_NexusFileStatus == 6) {
+ return true;
+ }
return m_NewestVersion.isValid() && (m_Version < m_NewestVersion);
}
@@ -205,29 +227,56 @@ bool ModInfoRegular::downgradeAvailable() const
void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant resultData)
{
QVariantMap result = resultData.toMap();
- setNewestVersion(VersionInfo(result["version"].toString()));
setNexusDescription(result["description"].toString());
- if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("voted_by_user"))) {
- setEndorsedState(result["voted_by_user"].toBool() ? ENDORSED_TRUE : ENDORSED_FALSE);
+ if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("endorsement"))) {
+ QVariantMap endorsement = result["endorsement"].toMap();
+ QString endorsementStatus = endorsement["endorse_status"].toString();
+ if (endorsementStatus.compare("Endorsed", Qt::CaseInsensitive) == 00)
+ setEndorsedState(ENDORSED_TRUE);
+ else if (endorsementStatus.compare("Abstained", Qt::CaseInsensitive) == 00)
+ setEndorsedState(ENDORSED_NEVER);
+ else
+ setEndorsedState(ENDORSED_FALSE);
}
- m_LastNexusQuery = QDateTime::currentDateTime();
- //m_MetaInfoChanged = true;
+ m_LastNexusQuery = QDateTime::currentDateTimeUtc();
+ m_NexusLastModified = QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC);
+ m_MetaInfoChanged = true;
saveMeta();
+ disconnect(sender(), SIGNAL(descriptionAvailable(QString, int, QVariant, QVariant)));
emit modDetailsUpdated(true);
}
void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData)
{
- m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE;
+ QMap results = resultData.toMap();
+ if (results["status"].toString().compare("Endorsed") == 0) {
+ m_EndorsedState = ENDORSED_TRUE;
+ } else if (results["status"].toString().compare("Abstained") == 0) {
+ m_EndorsedState = ENDORSED_NEVER;
+ } else {
+ m_EndorsedState = ENDORSED_FALSE;
+ }
m_MetaInfoChanged = true;
saveMeta();
emit modDetailsUpdated(true);
}
-void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, const QString &errorMessage)
+void ModInfoRegular::nxmTrackingToggled(QString, int, QVariant, bool tracked)
+{
+ if (tracked)
+ m_TrackedState = TRACKED_TRUE;
+ else
+ m_TrackedState = TRACKED_FALSE;
+ m_MetaInfoChanged = true;
+ saveMeta();
+ emit modDetailsUpdated(true);
+}
+
+
+void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage)
{
QString fullMessage = errorMessage;
if (userData.canConvert<int>() && (userData.toInt() == 1)) {
@@ -242,7 +291,9 @@ void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, cons
bool ModInfoRegular::updateNXMInfo()
{
- if (m_NexusID > 0) {
+ QDateTime time = QDateTime::currentDateTimeUtc();
+ QDateTime target = m_LastNexusQuery.addDays(1);
+ if (m_NexusID > 0 && time >= target) {
m_NexusBridge.requestDescription(m_GameName, m_NexusID, QVariant());
return true;
}
@@ -301,7 +352,7 @@ bool ModInfoRegular::setName(const QString &name)
} else {
if (!shellRename(modDir.absoluteFilePath(m_Name), modDir.absoluteFilePath(name))) {
qCritical("failed to rename mod %s (errorcode %d)",
- qPrintable(name), ::GetLastError());
+ qUtf8Printable(name), ::GetLastError());
return false;
}
}
@@ -338,7 +389,7 @@ void ModInfoRegular::setNotes(const QString &notes)
m_MetaInfoChanged = true;
}
-void ModInfoRegular::setGameName(QString gameName)
+void ModInfoRegular::setGameName(const QString &gameName)
{
m_GameName = gameName;
m_MetaInfoChanged = true;
@@ -380,6 +431,14 @@ void ModInfoRegular::setEndorsedState(EEndorsedState endorsedState)
}
}
+void ModInfoRegular::setTrackedState(ETrackedState trackedState)
+{
+ if (trackedState != m_TrackedState) {
+ m_TrackedState = trackedState;
+ m_MetaInfoChanged = true;
+ }
+}
+
void ModInfoRegular::setInstallationFile(const QString &fileName)
{
m_InstallationFile = fileName;
@@ -399,13 +458,19 @@ void ModInfoRegular::setIsEndorsed(bool endorsed)
}
}
-
void ModInfoRegular::setNeverEndorse()
{
m_EndorsedState = ENDORSED_NEVER;
m_MetaInfoChanged = true;
}
+void ModInfoRegular::setIsTracked(bool tracked)
+{
+ if (tracked != (m_TrackedState == TRACKED_TRUE)) {
+ m_TrackedState = tracked ? TRACKED_TRUE : TRACKED_FALSE;
+ m_MetaInfoChanged = true;
+ }
+}
void ModInfoRegular::setColor(QColor color)
{
@@ -427,7 +492,14 @@ bool ModInfoRegular::remove()
void ModInfoRegular::endorse(bool doEndorse)
{
if (doEndorse != (m_EndorsedState == ENDORSED_TRUE)) {
- m_NexusBridge.requestToggleEndorsement(m_GameName, getNexusID(), doEndorse, QVariant(1));
+ m_NexusBridge.requestToggleEndorsement(m_GameName, getNexusID(), m_Version.canonicalString(), doEndorse, QVariant(1));
+ }
+}
+
+void ModInfoRegular::track(bool doTrack)
+{
+ if (doTrack != (m_TrackedState == TRACKED_TRUE)) {
+ m_NexusBridge.requestToggleTracking(m_GameName, getNexusID(), doTrack, QVariant(1));
}
}
@@ -462,13 +534,32 @@ void ModInfoRegular::ignoreUpdate(bool ignore)
m_MetaInfoChanged = true;
}
+bool ModInfoRegular::canBeUpdated() const
+{
+ QDateTime now = QDateTime::currentDateTimeUtc();
+ QDateTime target = getExpires();
+ if (now >= target)
+ return m_NexusID > 0;
+ return false;
+}
+
+QDateTime ModInfoRegular::getExpires() const
+{
+ return m_LastNexusUpdate.addSecs(300);
+}
std::vector<ModInfo::EFlag> ModInfoRegular::getFlags() const
{
std::vector<ModInfo::EFlag> result = ModInfoWithConflictInfo::getFlags();
- if ((m_NexusID > 0) && (endorsedState() == ENDORSED_FALSE)) {
+ if ((m_NexusID > 0) &&
+ (endorsedState() == ENDORSED_FALSE) &&
+ Settings::instance().endorsementIntegration()) {
result.push_back(ModInfo::FLAG_NOTENDORSED);
}
+ if ((m_NexusID > 0) &&
+ (trackedState() == TRACKED_TRUE)) {
+ result.push_back(ModInfo::FLAG_TRACKED);
+ }
if (!isValid() && !m_Validated) {
result.push_back(ModInfo::FLAG_INVALID);
}
@@ -575,6 +666,19 @@ QString ModInfoRegular::getDescription() const
}
}
+int ModInfoRegular::getNexusFileStatus() const
+{
+ return m_NexusFileStatus;
+}
+
+void ModInfoRegular::setNexusFileStatus(int status)
+{
+ m_NexusFileStatus = status;
+ m_MetaInfoChanged = true;
+ saveMeta();
+ emit modDetailsUpdated(true);
+}
+
QString ModInfoRegular::comments() const
{
return m_Comments;
@@ -605,11 +709,50 @@ ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const
return m_EndorsedState;
}
+ModInfoRegular::ETrackedState ModInfoRegular::trackedState() const
+{
+ return m_TrackedState;
+}
+
+QDateTime ModInfoRegular::getLastNexusUpdate() const
+{
+ return m_LastNexusUpdate;
+}
+
+void ModInfoRegular::setLastNexusUpdate(QDateTime time)
+{
+ m_LastNexusUpdate = time;
+ m_MetaInfoChanged = true;
+ saveMeta();
+ emit modDetailsUpdated(true);
+}
+
QDateTime ModInfoRegular::getLastNexusQuery() const
{
return m_LastNexusQuery;
}
+void ModInfoRegular::setLastNexusQuery(QDateTime time)
+{
+ m_LastNexusQuery = time;
+ m_MetaInfoChanged = true;
+ saveMeta();
+ emit modDetailsUpdated(true);
+}
+
+QDateTime ModInfoRegular::getNexusLastModified() const
+{
+ return m_NexusLastModified;
+}
+
+void ModInfoRegular::setNexusLastModified(QDateTime time)
+{
+ m_NexusLastModified = time;
+ m_MetaInfoChanged = true;
+ saveMeta();
+ emit modDetailsUpdated(true);
+}
+
void ModInfoRegular::setURL(QString const &url)
{
m_URL = url;
@@ -623,14 +766,18 @@ QString ModInfoRegular::getURL() const
-QStringList ModInfoRegular::archives() const
+QStringList ModInfoRegular::archives(bool checkOnDisk)
{
- QStringList result;
- QDir dir(this->absolutePath());
- for (const QString &archive : dir.entryList(QStringList({ "*.bsa", "*.ba2" }))) {
- result.append(this->absolutePath() + "/" + archive);
+ if (checkOnDisk) {
+ QStringList result;
+ QDir dir(this->absolutePath());
+ QStringList bsaList = dir.entryList(QStringList({ "*.bsa", "*.ba2" }));
+ for (const QString &archive : bsaList) {
+ result.append(this->absolutePath() + "/" + archive);
+ }
+ m_Archives = result;
}
- return result;
+ return m_Archives;
}
void ModInfoRegular::addInstalledFile(int modId, int fileId)
diff --git a/src/modinforegular.h b/src/modinforegular.h
index 33b26998..f70487a2 100644
--- a/src/modinforegular.h
+++ b/src/modinforegular.h
@@ -44,7 +44,7 @@ public:
/**
* @return true if the current update is being ignored
*/
- virtual bool updateIgnored() const { return m_IgnoredVersion == m_NewestVersion; }
+ virtual bool updateIgnored() const { return m_IgnoredVersion.isValid() && m_IgnoredVersion == m_NewestVersion; }
/**
* @brief test if there is a newer version of the mod
@@ -107,7 +107,7 @@ public:
*
* @param gameName the source game shortName
*/
- void setGameName(QString gameName);
+ void setGameName(const QString &gameName);
/**
* @brief set/change the nexus mod id of this mod
@@ -178,6 +178,13 @@ public:
virtual void setNeverEndorse();
/**
+ * update the tracked state for the mod. This only changes the
+ * buffered state. It does not sync with Nexus
+ * @param tracked the new tracked state
+ */
+ virtual void setIsTracked(bool tracked);
+
+ /**
* @brief delete the mod from the disc. This does not update the global ModInfo structure or indices
* @return true if the mod was successfully removed
**/
@@ -191,6 +198,13 @@ public:
virtual void endorse(bool doEndorse);
/**
+ * @brief track or untrack the mod. This will sync with nexus!
+ * @param doTrack if true, the mod is tracked, if false, it's untracked.
+ * @note if doTrack doesn't differ from the current value, nothing happens.
+ */
+ virtual void track(bool doTrack);
+
+ /**
* @brief updates the mod to flag it as converted in order to ignore the alternate game warning
*/
virtual void markConverted(bool converted) override;
@@ -256,7 +270,12 @@ public:
/**
* @return true if the mod can be updated
*/
- virtual bool canBeUpdated() const { return m_NexusID > 0; }
+ virtual bool canBeUpdated() const;
+
+ /**
+ * @return the update expiration date based on the last updated date from Nexus
+ */
+ virtual QDateTime getExpires() const;
/**
* @return true if the mod can be enabled/disabled
@@ -285,6 +304,19 @@ public:
*/
virtual QString getDescription() const;
+
+ /**
+ * @return the nexus file status (aka category ID)
+ */
+ virtual int getNexusFileStatus() const;
+
+
+ /**
+ * @brief sets the file status (category ID) from Nexus
+ * @param status the status id of the installed file
+ */
+ virtual void setNexusFileStatus(int status);
+
/**
* @return comments for this mod
*/
@@ -316,11 +348,41 @@ public:
virtual EEndorsedState endorsedState() const;
/**
+ * @return true if the file is being tracked on nexus
+ */
+ virtual ETrackedState trackedState() const;
+
+ /**
+ * @brief get the last time nexus was checked for file updates on this mod
+ */
+ virtual QDateTime getLastNexusUpdate() const;
+
+ /**
+ * @brief set the last time nexus was checked for file updates on this mod
+ */
+ virtual void setLastNexusUpdate(QDateTime time);
+
+ /**
* @return last time nexus was queried for infos on this mod
*/
virtual QDateTime getLastNexusQuery() const;
- virtual QStringList archives() const;
+ /**
+ * @brief set the last time nexus was queried for info on this mod
+ */
+ virtual void setLastNexusQuery(QDateTime time);
+
+ /**
+ * @return last time the mod was updated on Nexus
+ */
+ virtual QDateTime getNexusLastModified() const;
+
+ /**
+ * @brief set the last time the mod was updated on Nexus
+ */
+ virtual void setNexusLastModified(QDateTime time);
+
+ virtual QStringList archives(bool checkOnDisk = false);
virtual void setColor(QColor color);
@@ -348,12 +410,14 @@ public:
private:
void setEndorsedState(EEndorsedState endorsedState);
+ void setTrackedState(ETrackedState trackedState);
private slots:
void nxmDescriptionAvailable(QString, int modID, QVariant userData, QVariant resultData);
void nxmEndorsementToggled(QString, int, QVariant userData, QVariant resultData);
- void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, const QString &errorMessage);
+ void nxmTrackingToggled(QString, int, QVariant userData, bool tracked);
+ void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage);
protected:
@@ -371,9 +435,13 @@ private:
QString m_URL;
QString m_GameName;
+ mutable QStringList m_Archives;
+
QDateTime m_CreationTime;
QDateTime m_LastNexusQuery;
+ QDateTime m_LastNexusUpdate;
+ QDateTime m_NexusLastModified;
QColor m_Color;
@@ -384,10 +452,12 @@ private:
bool m_IsAlternate;
bool m_Converted;
bool m_Validated;
+ int m_NexusFileStatus;
MOBase::VersionInfo m_NewestVersion;
MOBase::VersionInfo m_IgnoredVersion;
EEndorsedState m_EndorsedState;
+ ETrackedState m_TrackedState;
NexusBridge m_NexusBridge;
diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h
index f215fb17..3560d786 100644
--- a/src/modinfoseparator.h
+++ b/src/modinfoseparator.h
@@ -20,23 +20,18 @@ public:
virtual int getNexusID() const { return -1; }
- virtual void setGameName(QString /*gameName*/)
- {
- }
+ virtual void setGameName(const QString& /*gameName*/) {}
- virtual void setNexusID(int /*modID*/)
- {
- }
+ virtual void setNexusID(int /*modID*/) {}
- virtual void endorse(bool /*doEndorse*/)
- {
- }
+ virtual void endorse(bool /*doEndorse*/) {}
- virtual void ignoreUpdate(bool /*ignore*/)
- {
- }
+ virtual void parseNexusInfo() {}
+
+ virtual void ignoreUpdate(bool /*ignore*/) {}
virtual bool canBeUpdated() const { return false; }
+ virtual QDateTime getExpires() const { return QDateTime(); }
virtual bool canBeEnabled() const { return false; }
virtual std::vector<QString> getIniTweaks() const { return std::vector<QString>(); }
@@ -49,8 +44,14 @@ public:
virtual QString getInstallationFile() const { return ""; }
virtual QString getURL() const { return ""; }
virtual QString repository() const { return ""; }
-
+ virtual int getNexusFileStatus() const { return 0; }
+ virtual void setNexusFileStatus(int) {}
+ virtual QDateTime getLastNexusUpdate() const { return QDateTime(); }
+ virtual void setLastNexusUpdate(QDateTime) {}
virtual QDateTime getLastNexusQuery() const { return QDateTime(); }
+ virtual void setLastNexusQuery(QDateTime) {}
+ virtual QDateTime getNexusLastModified() const { return QDateTime(); }
+ virtual void setNexusLastModified(QDateTime) {}
virtual QDateTime creationTime() const { return QDateTime(); }
virtual void getNexusFiles
diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp
index e88ba29d..7a2b0071 100644
--- a/src/modinfowithconflictinfo.cpp
+++ b/src/modinfowithconflictinfo.cpp
@@ -7,7 +7,7 @@ using namespace MOBase;
using namespace MOShared;
ModInfoWithConflictInfo::ModInfoWithConflictInfo(PluginContainer *pluginContainer, DirectoryEntry **directoryStructure)
- : ModInfo(pluginContainer), m_DirectoryStructure(directoryStructure) {}
+ : ModInfo(pluginContainer), m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false) {}
void ModInfoWithConflictInfo::clearCaches()
{
@@ -32,6 +32,31 @@ std::vector<ModInfo::EFlag> ModInfoWithConflictInfo::getFlags() const
} break;
default: { /* NOP */ }
}
+ switch (isLooseArchiveConflicted()) {
+ case CONFLICT_MIXED: {
+ result.push_back(ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE);
+ result.push_back(ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN);
+ } break;
+ case CONFLICT_OVERWRITE: {
+ result.push_back(ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE);
+ } break;
+ case CONFLICT_OVERWRITTEN: {
+ result.push_back(ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN);
+ } break;
+ default: { /* NOP */ }
+ }
+ switch (isArchiveConflicted()) {
+ case CONFLICT_MIXED: {
+ result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED);
+ } break;
+ case CONFLICT_OVERWRITE: {
+ result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE);
+ } break;
+ case CONFLICT_OVERWRITTEN: {
+ result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN);
+ } break;
+ default: { /* NOP */ }
+ }
return result;
}
@@ -40,6 +65,10 @@ void ModInfoWithConflictInfo::doConflictCheck() const
{
m_OverwriteList.clear();
m_OverwrittenList.clear();
+ m_ArchiveOverwriteList.clear();
+ m_ArchiveOverwrittenList.clear();
+ m_ArchiveLooseOverwriteList.clear();
+ m_ArchiveLooseOverwrittenList.clear();
bool providesAnything = false;
@@ -51,34 +80,74 @@ void ModInfoWithConflictInfo::doConflictCheck() const
std::wstring name = ToWString(this->name());
m_CurrentConflictState = CONFLICT_NONE;
+ m_ArchiveConflictState = CONFLICT_NONE;
+ m_ArchiveConflictLooseState = CONFLICT_NONE;
if ((*m_DirectoryStructure)->originExists(name)) {
FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name);
std::vector<FileEntry::Ptr> files = origin.getFiles();
// for all files in this origin
for (FileEntry::Ptr file : files) {
- const std::vector<std::pair<int, std::wstring>> &alternatives = file->getAlternatives();
- if ((alternatives.size() == 0) || (alternatives.begin()->first == dataID)) {
+ auto alternatives = file->getAlternatives();
+ if ((alternatives.size() == 0) || (alternatives.back().first == dataID)) {
// no alternatives -> no conflict
providesAnything = true;
} else {
+ // Get the archive data for the current mod
+ bool found = file->getOrigin() == origin.getID();
+ std::pair<std::wstring, int> archiveData;
+ if (found)
+ archiveData = file->getArchive();
+ else {
+ for (auto alts : alternatives) {
+ if (alts.first == origin.getID()) {
+ archiveData = alts.second;
+ break;
+ }
+ }
+ }
+
+ // If this is not the origin then determine the correct overwrite
if (file->getOrigin() != origin.getID()) {
FilesOrigin &altOrigin = (*m_DirectoryStructure)->getOriginByID(file->getOrigin());
unsigned int altIndex = ModInfo::getIndex(ToQString(altOrigin.getName()));
- m_OverwrittenList.insert(altIndex);
+ if (file->getArchive().first.size() == 0)
+ if (archiveData.first.size() == 0)
+ m_OverwrittenList.insert(altIndex);
+ else
+ m_ArchiveLooseOverwrittenList.insert(altIndex);
+ else
+ m_ArchiveOverwrittenList.insert(altIndex);
} else {
providesAnything = true;
}
- // for all non-providing alternative origins
+ // Sort out the alternatives
for (auto altInfo : alternatives) {
if ((altInfo.first != dataID) && (altInfo.first != origin.getID())) {
FilesOrigin &altOrigin = (*m_DirectoryStructure)->getOriginByID(altInfo.first);
- unsigned int altIndex = ModInfo::getIndex(ToQString(altOrigin.getName()));
- if (origin.getPriority() > altOrigin.getPriority()) {
- m_OverwriteList.insert(altIndex);
+ QString altOriginName = ToQString(altOrigin.getName());
+ unsigned int altIndex = ModInfo::getIndex(altOriginName);
+ if (altInfo.second.first.size() == 0) {
+ if (archiveData.first.size() == 0) {
+ if (origin.getPriority() > altOrigin.getPriority()) {
+ m_OverwriteList.insert(altIndex);
+ } else {
+ m_OverwrittenList.insert(altIndex);
+ }
+ } else {
+ m_ArchiveLooseOverwrittenList.insert(altIndex);
+ }
} else {
- m_OverwrittenList.insert(altIndex);
+ if (archiveData.first.size() == 0) {
+ m_ArchiveLooseOverwriteList.insert(altIndex);
+ } else {
+ if (archiveData.second > altInfo.second.second) {
+ m_ArchiveOverwriteList.insert(altIndex);
+ } else if (archiveData.second < altInfo.second.second) {
+ m_ArchiveOverwrittenList.insert(altIndex);
+ }
+ }
}
}
}
@@ -95,6 +164,20 @@ void ModInfoWithConflictInfo::doConflictCheck() const
m_CurrentConflictState = CONFLICT_OVERWRITE;
else if (!m_OverwrittenList.empty())
m_CurrentConflictState = CONFLICT_OVERWRITTEN;
+
+ if (!m_ArchiveOverwriteList.empty() && !m_ArchiveOverwrittenList.empty())
+ m_ArchiveConflictState = CONFLICT_MIXED;
+ else if (!m_ArchiveOverwriteList.empty())
+ m_ArchiveConflictState = CONFLICT_OVERWRITE;
+ else if (!m_ArchiveOverwrittenList.empty())
+ m_ArchiveConflictState = CONFLICT_OVERWRITTEN;
+
+ if (!m_ArchiveLooseOverwrittenList.empty() && !m_ArchiveLooseOverwriteList.empty())
+ m_ArchiveConflictLooseState = CONFLICT_MIXED;
+ else if (!m_ArchiveLooseOverwrittenList.empty())
+ m_ArchiveConflictLooseState = CONFLICT_OVERWRITTEN;
+ else if (!m_ArchiveLooseOverwriteList.empty())
+ m_ArchiveConflictLooseState = CONFLICT_OVERWRITE;
}
}
}
@@ -110,6 +193,26 @@ ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isConflicted() c
return m_CurrentConflictState;
}
+ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isArchiveConflicted() const
+{
+ QTime now = QTime::currentTime();
+ if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) {
+ doConflictCheck();
+ }
+
+ return m_ArchiveConflictState;
+}
+
+ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isLooseArchiveConflicted() const
+{
+ QTime now = QTime::currentTime();
+ if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) {
+ doConflictCheck();
+ }
+
+ return m_ArchiveConflictLooseState;
+}
+
bool ModInfoWithConflictInfo::isRedundant() const
{
diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h
index 6c1591c5..13d87d25 100644
--- a/src/modinfowithconflictinfo.h
+++ b/src/modinfowithconflictinfo.h
@@ -23,6 +23,14 @@ public:
virtual std::set<unsigned int> getModOverwritten() { return m_OverwrittenList; }
+ virtual std::set<unsigned int> getModArchiveOverwrite() { return m_ArchiveOverwriteList; }
+
+ virtual std::set<unsigned int> getModArchiveOverwritten() { return m_ArchiveOverwrittenList; }
+
+ virtual std::set<unsigned int> getModArchiveLooseOverwrite() { return m_ArchiveLooseOverwriteList; }
+
+ virtual std::set<unsigned int> getModArchiveLooseOverwritten() { return m_ArchiveLooseOverwrittenList; }
+
virtual void doConflictCheck() const;
private:
@@ -32,7 +40,8 @@ private:
CONFLICT_OVERWRITE,
CONFLICT_OVERWRITTEN,
CONFLICT_MIXED,
- CONFLICT_REDUNDANT
+ CONFLICT_REDUNDANT,
+ CONFLICT_CROSS
};
private:
@@ -43,6 +52,16 @@ private:
EConflictType isConflicted() const;
/**
+ * @return true if there are archive conflicts for files in this mod
+ */
+ EConflictType isArchiveConflicted() const;
+
+ /**
+ * @return true if there are archive conflicts with loose files in this mod
+ */
+ EConflictType isLooseArchiveConflicted() const;
+
+ /**
* @return true if this mod is completely replaced by others
*/
bool isRedundant() const;
@@ -52,10 +71,17 @@ private:
MOShared::DirectoryEntry **m_DirectoryStructure;
mutable EConflictType m_CurrentConflictState;
+ mutable EConflictType m_ArchiveConflictState;
+ mutable EConflictType m_ArchiveConflictLooseState;
+ mutable bool m_HasLooseOverwrite;
mutable QTime m_LastConflictCheck;
mutable std::set<unsigned int> m_OverwriteList; // indices of mods overritten by this mod
mutable std::set<unsigned int> m_OverwrittenList; // indices of mods overwriting this mod
+ mutable std::set<unsigned int> m_ArchiveOverwriteList; // indices of mods with archive files overritten by this mod
+ mutable std::set<unsigned int> m_ArchiveOverwrittenList; // indices of mods with archive files overwriting this mod
+ mutable std::set<unsigned int> m_ArchiveLooseOverwriteList; // indices of mods with archives being overwritten by this mod's loose files
+ mutable std::set<unsigned int> m_ArchiveLooseOverwrittenList; // indices of mods with loose files overwriting this mod's archive files
};
diff --git a/src/modlist.cpp b/src/modlist.cpp
index 16d2cd07..e2398cd6 100644
--- a/src/modlist.cpp
+++ b/src/modlist.cpp
@@ -1,1350 +1,1441 @@
-/*
-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 "modlist.h"
-
-#include "messagedialog.h"
-#include "installationtester.h"
-#include "qtgroupingproxy.h"
-#include "viewmarkingscrollbar.h"
-#include "modlistsortproxy.h"
-#include "pluginlist.h"
-#include "settings.h"
-#include "modinforegular.h"
-#include <appconfig.h>
-#include <utility.h>
-#include <report.h>
-
-#include <QFileInfo>
-#include <QDir>
-#include <QDirIterator>
-#include <QMimeData>
-#include <QStandardItemModel>
-#include <QMessageBox>
-#include <QStringList>
-#include <QEvent>
-#include <QContextMenuEvent>
-#include <QMenu>
-#include <QCheckBox>
-#include <QWidgetAction>
-#include <QAbstractItemView>
-#include <QSortFilterProxyModel>
-#include <QApplication>
-#include <QFontDatabase>
-
-#include <sstream>
-#include <stdexcept>
-#include <algorithm>
-
-
-using namespace MOBase;
-
-
-ModList::ModList(PluginContainer *pluginContainer, QObject *parent)
- : QAbstractItemModel(parent)
- , m_Profile(nullptr)
- , m_NexusInterface(nullptr)
- , m_Modified(false)
- , m_FontMetrics(QFont())
- , m_DropOnItems(false)
- , m_PluginContainer(pluginContainer)
-{
- m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)"));
- m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface"));
- m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes"));
- m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive"));
- m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)"));
- m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin"));
- m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher"));
- m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music"));
- m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures"));
- m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration"));
- m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files"));
- m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files"));
-
- m_LastCheck.start();
-}
-
-ModList::~ModList()
-{
- m_ModStateChanged.disconnect_all_slots();
- m_ModMoved.disconnect_all_slots();
-}
-
-void ModList::setProfile(Profile *profile)
-{
- m_Profile = profile;
-}
-
-int ModList::rowCount(const QModelIndex &parent) const
-{
- if (!parent.isValid()) {
- return ModInfo::getNumMods();
- } else {
- return 0;
- }
-}
-
-bool ModList::hasChildren(const QModelIndex &parent) const
-{
- if (!parent.isValid()) {
- return ModInfo::getNumMods() > 0;
- } else {
- return false;
- }
-}
-
-
-int ModList::columnCount(const QModelIndex &) const
-{
- return COL_LASTCOLUMN + 1;
-}
-
-
-QVariant ModList::getOverwriteData(int column, int role) const
-{
- switch (role) {
- case Qt::DisplayRole: {
- if (column == 0) {
- return "Overwrite";
- } else {
- return QVariant();
- }
- } break;
- case Qt::CheckStateRole: {
- if (column == 0) {
- return Qt::Checked;
- } else {
- return QVariant();
- }
- } break;
- case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
- case Qt::UserRole: return -1;
- case Qt::ForegroundRole: return QBrush(Qt::red);
- case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)");
- default: return QVariant();
- }
-}
-
-
-QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const
-{
- switch (flag) {
- case ModInfo::FLAG_BACKUP: return tr("Backup");
- case ModInfo::FLAG_SEPARATOR: return tr("Separator");
- case ModInfo::FLAG_INVALID: return tr("No valid game data");
- case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet");
- case ModInfo::FLAG_NOTES: {
- QStringList output;
- if (!modInfo->comments().isEmpty())
- output << QString("<i>%1</i>").arg(modInfo->comments());
- if (!modInfo->notes().isEmpty())
- output << QString("<i>%1</i>").arg(modInfo->notes()).replace("\n", "<br>");
- return output.join("<br>");
- }
- case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites files");
- case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten files");
- case ModInfo::FLAG_CONFLICT_MIXED: return tr("Overwrites & Overwritten");
- case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant");
- case ModInfo::FLAG_ALTERNATE_GAME: return tr("Alternate game source");
- default: return "";
- }
-}
-
-
-QVariantList ModList::contentsToIcons(const std::vector<ModInfo::EContent> &contents) const
-{
- QVariantList result;
- std::set<ModInfo::EContent> contentsSet(contents.begin(), contents.end());
- for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) {
- if (contentsSet.find(iter->first) != contentsSet.end()) {
- result.append(std::get<0>(iter->second));
- } else {
- result.append(QString());
- }
- }
- return result;
-}
-
-QString ModList::contentsToToolTip(const std::vector<ModInfo::EContent> &contents) const
-{
- QString result("<table cellspacing=7>");
-
- std::set<ModInfo::EContent> contentsSet(contents.begin(), contents.end());
- for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) {
- if (contentsSet.find(iter->first) != contentsSet.end()) {
- result.append(QString("<tr><td><img src=\"%1\" width=32/></td>"
- "<td valign=\"middle\">%2</td></tr>")
- .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second)));
- }
- }
- result.append("</table>");
- return result;
-}
-
-
-QVariant ModList::data(const QModelIndex &modelIndex, int role) const
-{
- if (m_Profile == nullptr) return QVariant();
- if (!modelIndex.isValid()) return QVariant();
- unsigned int modIndex = modelIndex.row();
- int column = modelIndex.column();
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- if ((role == Qt::DisplayRole) ||
- (role == Qt::EditRole)) {
- if ((column == COL_FLAGS)
- || (column == COL_CONTENT)) {
- return QVariant();
- } else if (column == COL_NAME) {
- auto flags = modInfo->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
- {
- return modInfo->name().replace("_separator", "");
- }
- else
- return modInfo->name();
- } else if (column == COL_VERSION) {
- VersionInfo verInfo = modInfo->getVersion();
- QString version = verInfo.displayString();
- if (role != Qt::EditRole) {
- if (version.isEmpty() && modInfo->canBeUpdated()) {
- version = "?";
- }
- }
- return version;
- } else if (column == COL_PRIORITY) {
- int priority = modInfo->getFixedPriority();
- if (priority != INT_MIN) {
- return QVariant(); // hide priority for mods where it's fixed
- } else {
- return m_Profile->getModPriority(modIndex);
- }
- } else if (column == COL_MODID) {
- int modID = modInfo->getNexusID();
- if (modID >= 0) {
- return modID;
- }
- else {
- return QVariant();
- }
- } else if (column == COL_GAME) {
- if (m_PluginContainer != nullptr) {
- for (auto game : m_PluginContainer->plugins<IPluginGame>()) {
- if (game->gameShortName() == modInfo->getGameName())
- return game->gameName();
- }
- }
- return modInfo->getGameName();
- } else if (column == COL_CATEGORY) {
- if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) {
- return tr("Non-MO");
- } else {
- int category = modInfo->getPrimaryCategory();
- if (category != -1) {
- CategoryFactory &categoryFactory = CategoryFactory::instance();
- if (categoryFactory.categoryExists(category)) {
- try {
- int categoryIdx = categoryFactory.getCategoryIndex(category);
- return categoryFactory.getCategoryName(categoryIdx);
- } catch (const std::exception &e) {
- qCritical("failed to retrieve category name: %s", e.what());
- return QString();
- }
- } else {
- qWarning("category %d doesn't exist (may have been removed)", category);
- modInfo->setCategory(category, false);
- return QString();
- }
- } else {
- return QVariant();
- }
- }
- } else if (column == COL_INSTALLTIME) {
- // display installation time for mods that can be updated
- if (modInfo->creationTime().isValid()) {
- return modInfo->creationTime();
- } else {
- return QVariant();
- }
- } else if (column == COL_NOTES) {
- return modInfo->comments();
- } else {
- return tr("invalid");
- }
- } else if ((role == Qt::CheckStateRole) && (column == 0)) {
- if (modInfo->canBeEnabled()) {
- return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked;
- } else {
- return QVariant();
- }
- } else if (role == Qt::TextAlignmentRole) {
- auto flags = modInfo->getFlags();
- if (column == COL_NAME) {
- if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) {
- return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
- } else {
- return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
- }
- } else if (column == COL_VERSION) {
- return QVariant(Qt::AlignRight | Qt::AlignVCenter);
- } else if (column == COL_NOTES) {
- return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
- } else {
- return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
- }
- } else if (role == Qt::UserRole) {
- if (column == COL_CATEGORY) {
- QVariantList categoryNames;
- std::set<int> categories = modInfo->getCategories();
- CategoryFactory &categoryFactory = CategoryFactory::instance();
- for (auto iter = categories.begin(); iter != categories.end(); ++iter) {
- categoryNames.append(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*iter)));
- }
- if (categoryNames.count() != 0) {
- return categoryNames;
- } else {
- return QVariant();
- }
- } else if (column == COL_PRIORITY) {
- int priority = modInfo->getFixedPriority();
- if (priority != INT_MIN) {
- return priority;
- } else {
- return m_Profile->getModPriority(modIndex);
- }
- } else {
- return modInfo->getNexusID();
- }
- } else if (role == Qt::UserRole + 1) {
- return modIndex;
- } else if (role == Qt::UserRole + 2) {
- switch (column) {
- case COL_MODID: return QtGroupingProxy::AGGR_FIRST;
- case COL_VERSION: return QtGroupingProxy::AGGR_MAX;
- case COL_CATEGORY: return QtGroupingProxy::AGGR_FIRST;
- case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN;
- default: return QtGroupingProxy::AGGR_NONE;
- }
- } else if (role == Qt::UserRole + 3) {
- return contentsToIcons(modInfo->getContents());
- } else if (role == Qt::UserRole + 4) {
- return modInfo->getGameName();
- } else if (role == Qt::FontRole) {
- QFont result;
- auto flags = modInfo->getFlags();
- if (column == COL_NAME) {
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
- {
- //result.setCapitalization(QFont::AllUppercase);
- result.setItalic(true);
- //result.setUnderline(true);
- result.setBold(true);
- } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) {
- result.setItalic(true);
- }
- } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) {
- result.setItalic(true);
- } else if (column == COL_VERSION) {
- if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) {
- result.setWeight(QFont::Bold);
- }
- }
- return result;
- } else if (role == Qt::DecorationRole) {
- if (column == COL_VERSION) {
- if (modInfo->updateAvailable()) {
- return QIcon(":/MO/gui/update_available");
- } else if (modInfo->downgradeAvailable()) {
- return QIcon(":/MO/gui/warning");
- } else if (modInfo->getVersion().scheme() == VersionInfo::SCHEME_DATE) {
- return QIcon(":/MO/gui/version_date");
- }
- }
- return QVariant();
- } else if (role == Qt::ForegroundRole) {
- if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) {
- return Settings::getIdealTextColor(modInfo->getColor());
- } else if (column == COL_NAME) {
- int highlight = modInfo->getHighlight();
- if (highlight & ModInfo::HIGHLIGHT_IMPORTANT)
- return QBrush(Qt::darkRed);
- else if (highlight & ModInfo::HIGHLIGHT_INVALID)
- return QBrush(Qt::darkGray);
- } else if (column == COL_VERSION) {
- if (!modInfo->getNewestVersion().isValid()) {
- return QVariant();
- } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) {
- return QBrush(Qt::red);
- } else {
- return QBrush(Qt::darkGreen);
- }
- }
- return QVariant();
- } else if ((role == Qt::BackgroundRole)
- || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
- if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) {
- return Settings::instance().modlistContainsPluginColor();
- } else if (m_Overwrite.find(modIndex) != m_Overwrite.end()) {
- return Settings::instance().modlistOverwrittenLooseColor();
- }
- else if (m_Overwritten.find(modIndex) != m_Overwritten.end()) {
- return Settings::instance().modlistOverwritingLooseColor();
- } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)
- && modInfo->getColor().isValid()
- && ((role != ViewMarkingScrollBar::DEFAULT_ROLE)
- || Settings::instance().colorSeparatorScrollbar())) {
- return modInfo->getColor();
- } else {
- return QVariant();
- }
- } else if (role == Qt::ToolTipRole) {
- if (column == COL_FLAGS) {
- QString result;
-
- for (ModInfo::EFlag flag : modInfo->getFlags()) {
- if (result.length() != 0) result += "<br>";
- result += getFlagText(flag, modInfo);
- }
-
- return result;
- } else if (column == COL_CONTENT) {
- return contentsToToolTip(modInfo->getContents());
- } else if (column == COL_NAME) {
- try {
- return modInfo->getDescription();
- } catch (const std::exception &e) {
- qCritical("invalid mod description: %s", e.what());
- return QString();
- }
- } else if (column == COL_VERSION) {
- QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString()).arg(modInfo->getNewestVersion().displayString());
- if (modInfo->downgradeAvailable()) {
- text += "<br>" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn "
- "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. "
- "Either way you may want to \"upgrade\".");
- }
- return text;
- } else if (column == COL_CATEGORY) {
- const std::set<int> &categories = modInfo->getCategories();
- std::wostringstream categoryString;
- categoryString << ToWString(tr("Categories: <br>"));
- CategoryFactory &categoryFactory = CategoryFactory::instance();
- for (std::set<int>::const_iterator catIter = categories.begin();
- catIter != categories.end(); ++catIter) {
- if (catIter != categories.begin()) {
- categoryString << " , ";
- }
- try {
- categoryString << "<span style=\"white-space: nowrap;\"><i>" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << "</font></span>";
- } catch (const std::exception &e) {
- qCritical("failed to generate tooltip: %s", e.what());
- return QString();
- }
- }
-
- return ToQString(categoryString.str());
- } else if (column == COL_NOTES) {
- return getFlagText(ModInfo::FLAG_NOTES, modInfo);
- } else {
- return QVariant();
- }
- } else {
- return QVariant();
- }
-}
-
-
-bool ModList::renameMod(int index, const QString &newName)
-{
- QString nameFixed = newName;
- if (!fixDirectoryName(nameFixed) || nameFixed.isEmpty()) {
- MessageDialog::showMessage(tr("Invalid name"), nullptr);
- return false;
- }
-
- if (ModList::allMods().contains(nameFixed, Qt::CaseInsensitive) && nameFixed.toLower()!=ModInfo::getByIndex(index)->name().toLower() ) {
- MessageDialog::showMessage(tr("Name is already in use by another mod"), nullptr);
- return false;
- }
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
- QString oldName = modInfo->name();
- if (nameFixed != oldName) {
- // before we rename, ensure there is no scheduled asynchronous to rewrite
- m_Profile->cancelModlistWrite();
-
-
- if (modInfo->setName(nameFixed))
- // Notice there is a good chance that setName() updated the modinfo indexes
- // the modRenamed() call will refresh the indexes in the current profile
- // and update the modlists in all profiles
- emit modRenamed(oldName, nameFixed);
- }
-
- // invalidate the currently displayed state of this list
- notifyChange(-1);
- return true;
-}
-
-bool ModList::setData(const QModelIndex &index, const QVariant &value, int role)
-{
- if (m_Profile == nullptr) return false;
-
- if (static_cast<unsigned int>(index.row()) >= ModInfo::getNumMods()) {
- return false;
- }
-
- int modID = index.row();
-
- ModInfo::Ptr info = ModInfo::getByIndex(modID);
- IModList::ModStates oldState = state(modID);
-
-
- bool result = false;
-
- emit aboutToChangeData();
-
- if (role == Qt::CheckStateRole) {
- bool enabled = value.toInt() == Qt::Checked;
- if (m_Profile->modEnabled(modID) != enabled) {
- m_Profile->setModEnabled(modID, enabled);
- m_Modified = true;
- m_LastCheck.restart();
- emit modlist_changed(index, role);
- }
- result = true;
- emit dataChanged(index, index);
- } else if (role == Qt::EditRole) {
- switch (index.column()) {
- case COL_NAME: {
- auto flags = info->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
- {
- result = renameMod(modID, value.toString() + "_separator");
- }
- else
- result = renameMod(modID, value.toString());
- } break;
- case COL_PRIORITY: {
- bool ok = false;
- int newPriority = value.toInt(&ok);
- if (ok && newPriority < 0) {
- newPriority = 0;
- }
- if (ok) {
- m_Profile->setModPriority(modID, newPriority);
-
- emit modorder_changed();
- result = true;
- } else {
- result = false;
- }
- } break;
- case COL_MODID: {
- bool ok = false;
- int newID = value.toInt(&ok);
- if (ok) {
- info->setNexusID(newID);
- emit modlist_changed(index, role);
- result = true;
- } else {
- result = false;
- }
- } break;
- case COL_VERSION: {
- VersionInfo::VersionScheme scheme = info->getVersion().scheme();
- VersionInfo version(value.toString(), scheme, true);
- if (version.isValid()) {
- info->setVersion(version);
- result = true;
- } else {
- result = false;
- }
- } break;
- case COL_NOTES: {
- info->setComments(value.toString());
- result = true;
- } break;
- default: {
- qWarning("edit on column \"%s\" not supported",
- getColumnName(index.column()).toUtf8().constData());
- result = false;
- } break;
- }
- if (result) {
- emit dataChanged(index, index);
- }
- }
-
- emit postDataChanged();
-
- IModList::ModStates newState = state(modID);
- if (oldState != newState) {
- try {
- m_ModStateChanged(info->name(), newState);
- } catch (const std::exception &e) {
- qCritical("failed to invoke state changed notification: %s", e.what());
- } catch (...) {
- qCritical("failed to invoke state changed notification: unknown exception");
- }
- }
-
- return result;
-}
-
-
-QVariant ModList::headerData(int section, Qt::Orientation orientation,
- int role) const
-{
- if (orientation == Qt::Horizontal) {
- if (role == Qt::DisplayRole) {
- return getColumnName(section);
- } else if (role == Qt::ToolTipRole) {
- return getColumnToolTip(section);
- } else if (role == Qt::TextAlignmentRole) {
- return QVariant(Qt::AlignCenter);
- } else if (role == Qt::SizeHintRole) {
- QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section));
- temp.rwidth() += 20;
- temp.rheight() += 12;
- return temp;
- }
- }
- return QAbstractItemModel::headerData(section, orientation, role);
-}
-
-
-Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const
-{
- Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
- if (modelIndex.internalId() < 0) {
- return Qt::ItemIsEnabled;
- }
- if (modelIndex.isValid()) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row());
- std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
- if (modInfo->getFixedPriority() == INT_MIN) {
- result |= Qt::ItemIsDragEnabled;
- result |= Qt::ItemIsUserCheckable;
- if ((modelIndex.column() == COL_PRIORITY) ||
- (modelIndex.column() == COL_VERSION) ||
- (modelIndex.column() == COL_MODID)) {
- result |= Qt::ItemIsEditable;
- }
- if (((modelIndex.column() == COL_NAME) ||
- (modelIndex.column() == COL_NOTES))
- && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) {
- result |= Qt::ItemIsEditable;
- }
- }
- if (m_DropOnItems
- && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) {
- result |= Qt::ItemIsDropEnabled;
- }
- } else {
- if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled;
- }
- return result;
-}
-
-
-QStringList ModList::mimeTypes() const
-{
- QStringList result = QAbstractItemModel::mimeTypes();
- result.append("text/uri-list");
- return result;
-}
-
-QMimeData *ModList::mimeData(const QModelIndexList &indexes) const
-{
- QMimeData *result = QAbstractItemModel::mimeData(indexes);
- result->setData("text/plain", "mod");
- return result;
-}
-
-void ModList::changeModPriority(std::vector<int> sourceIndices, int newPriority)
-{
- if (m_Profile == nullptr) return;
-
- emit layoutAboutToBeChanged();
- Profile *profile = m_Profile;
-
- // sort the moving mods by ascending priorities
- std::sort(sourceIndices.begin(), sourceIndices.end(),
- [profile](const int &LHS, const int &RHS) {
- return profile->getModPriority(LHS) > profile->getModPriority(RHS);
- });
-
- // move mods that are decreasing in priority
- for (std::vector<int>::const_iterator iter = sourceIndices.begin();
- iter != sourceIndices.end(); ++iter) {
- int oldPriority = profile->getModPriority(*iter);
- if (oldPriority > newPriority) {
- profile->setModPriority(*iter, newPriority);
- m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority);
- }
- }
-
- // sort the moving mods by descending priorities
- std::sort(sourceIndices.begin(), sourceIndices.end(),
- [profile](const int &LHS, const int &RHS) {
- return profile->getModPriority(LHS) < profile->getModPriority(RHS);
- });
-
- // if at least one mod is increasing in priority, the target index is
- // that of the row BELOW the dropped location, otherwise it's the one above
- for (std::vector<int>::const_iterator iter = sourceIndices.begin();
- iter != sourceIndices.end(); ++iter) {
- int oldPriority = profile->getModPriority(*iter);
- if (oldPriority < newPriority) {
- --newPriority;
- break;
- }
- }
-
- // move mods that are increasing in priority
- for (std::vector<int>::const_iterator iter = sourceIndices.begin();
- iter != sourceIndices.end(); ++iter) {
- int oldPriority = profile->getModPriority(*iter);
- if (oldPriority < newPriority) {
- profile->setModPriority(*iter, newPriority);
- m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority);
- }
- }
-
- emit layoutChanged();
-
- emit modorder_changed();
-}
-
-
-void ModList::changeModPriority(int sourceIndex, int newPriority)
-{
- if (m_Profile == nullptr) return;
- emit layoutAboutToBeChanged();
-
- m_Profile->setModPriority(sourceIndex, newPriority);
-
- emit layoutChanged();
-
- emit modorder_changed();
-}
-
-void ModList::setOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
-{
- m_Overwrite = overwrite;
- m_Overwritten = overwritten;
- notifyChange(0, rowCount() - 1);
-}
-
-void ModList::setPluginContainer(PluginContainer *pluginContianer)
-{
- m_PluginContainer = pluginContianer;
-}
-
-bool ModList::modInfoAboutToChange(ModInfo::Ptr info)
-{
- if (m_ChangeInfo.name.isEmpty()) {
- m_ChangeInfo.name = info->name();
- m_ChangeInfo.state = state(info->name());
- return true;
- } else {
- return false;
- }
-}
-
-void ModList::modInfoChanged(ModInfo::Ptr info)
-{
- if (info->name() == m_ChangeInfo.name) {
- IModList::ModStates newState = state(info->name());
- if (m_ChangeInfo.state != newState) {
- m_ModStateChanged(info->name(), newState);
- }
-
- int row = ModInfo::getIndex(info->name());
- info->testValid();
- emit aboutToChangeData();
- emit dataChanged(index(row, 0), index(row, columnCount()));
- emit postDataChanged();
- } else {
- qCritical("modInfoChanged not called after modInfoAboutToChange");
- }
- m_ChangeInfo.name = QString();
-}
-
-void ModList::disconnectSlots() {
- m_ModMoved.disconnect_all_slots();
- m_ModStateChanged.disconnect_all_slots();
-}
-
-int ModList::timeElapsedSinceLastChecked() const
-{
- return m_LastCheck.elapsed();
-}
-
-void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry)
-{
- for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
- ModInfo::getByIndex(i)->setPluginSelected(false);
- }
- for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) {
- QString modName = idx.data().toString();
-
- const MOShared::FileEntry::Ptr fileEntry = directoryEntry.findFile(modName.toStdWString());
- if (fileEntry.get() != nullptr) {
- bool archive = false;
- std::vector<std::pair<int, std::wstring>> origins;
- {
- std::vector<std::pair<int, std::wstring>> alternatives = fileEntry->getAlternatives();
- origins.insert(origins.end(), std::pair<int, std::wstring>(fileEntry->getOrigin(archive), fileEntry->getArchive()));
- }
- for (auto originInfo : origins) {
- MOShared::FilesOrigin &origin = directoryEntry.getOriginByID(originInfo.first);
- for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
- if (ModInfo::getByIndex(i)->internalName() == QString::fromStdWString(origin.getName())) {
- ModInfo::getByIndex(i)->setPluginSelected(true);
- break;
- }
- }
- }
- }
- }
- notifyChange(0, rowCount() - 1);
-}
-
-IModList::ModStates ModList::state(unsigned int modIndex) const
-{
- IModList::ModStates result;
- if (modIndex != UINT_MAX) {
- result |= IModList::STATE_EXISTS;
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- if (modInfo->isEmpty()) {
- result |= IModList::STATE_EMPTY;
- }
- if (modInfo->endorsedState() == ModInfo::ENDORSED_TRUE) {
- result |= IModList::STATE_ENDORSED;
- }
- if (modInfo->isValid()) {
- result |= IModList::STATE_VALID;
- }
- if (modInfo->isRegular()) {
- QSharedPointer<ModInfoRegular> modInfoRegular = modInfo.staticCast<ModInfoRegular>();
- if (modInfoRegular->isAlternate() && !modInfoRegular->isConverted())
- result |= IModList::STATE_ALTERNATE;
- if (!modInfo->isValid() && modInfoRegular->isValidated())
- result |= IModList::STATE_VALID;
- }
- if (modInfo->canBeEnabled()) {
- if (m_Profile->modEnabled(modIndex)) {
- result |= IModList::STATE_ACTIVE;
- }
- } else {
- result |= IModList::STATE_ESSENTIAL;
- }
- }
- return result;
-}
-
-QString ModList::displayName(const QString &internalName) const
-{
- unsigned int modIndex = ModInfo::getIndex(internalName);
- if (modIndex == UINT_MAX) {
- // might be better to throw an exception?
- return internalName;
- } else {
- return ModInfo::getByIndex(modIndex)->name();
- }
-}
-
-QStringList ModList::allMods() const
-{
- QStringList result;
- for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
- result.append(ModInfo::getByIndex(i)->internalName());
- }
- return result;
-}
-
-IModList::ModStates ModList::state(const QString &name) const
-{
- unsigned int modIndex = ModInfo::getIndex(name);
-
- return state(modIndex);
-}
-
-bool ModList::setActive(const QString &name, bool active)
-{
- unsigned int modIndex = ModInfo::getIndex(name);
- if (modIndex == UINT_MAX) {
- return false;
- } else {
- m_Profile->setModEnabled(modIndex, active);
-
- IModList::ModStates newState = state(modIndex);
- m_ModStateChanged(name, newState);
- return true;
- }
-}
-
-int ModList::priority(const QString &name) const
-{
- unsigned int modIndex = ModInfo::getIndex(name);
- if (modIndex == UINT_MAX) {
- return -1;
- } else {
- return m_Profile->getModPriority(modIndex);
- }
-}
-
-bool ModList::setPriority(const QString &name, int newPriority)
-{
- if ((newPriority < 0) || (newPriority >= static_cast<int>(m_Profile->numRegularMods()))) {
- return false;
- }
-
- unsigned int modIndex = ModInfo::getIndex(name);
- if (modIndex == UINT_MAX) {
- return false;
- } else {
- m_Profile->setModPriority(modIndex, newPriority);
- notifyChange(modIndex);
- return true;
- }
-}
-
-bool ModList::onModStateChanged(const std::function<void (const QString &, IModList::ModStates)> &func)
-{
- auto conn = m_ModStateChanged.connect(func);
- return conn.connected();
-}
-
-bool ModList::onModMoved(const std::function<void (const QString &, int, int)> &func)
-{
- auto conn = m_ModMoved.connect(func);
- return conn.connected();
-}
-
-bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent)
-{
- QStringList source;
- QStringList target;
-
- if (row == -1) {
- row = parent.row();
- }
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
- QDir modDirectory(modInfo->absolutePath());
- QDir gameDirectory(Settings::instance().getOverwriteDirectory());
-
- unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool {
- std::vector<ModInfo::EFlag> flags = mod->getFlags();
- return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); });
-
- QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name();
-
- for (const QUrl &url : mimeData->urls()) {
- if (!url.isLocalFile()) {
- continue;
- }
- QString relativePath = gameDirectory.relativeFilePath(url.toLocalFile());
- if (relativePath.startsWith("..")) {
- qDebug("%s drop ignored", qPrintable(url.toLocalFile()));
- continue;
- }
- source.append(url.toLocalFile());
- target.append(modDirectory.absoluteFilePath(relativePath));
- emit fileMoved(relativePath, overwriteName, modInfo->name());
- }
-
- if (source.count() != 0) {
- shellMove(source, target);
- }
-
- return true;
-}
-
-bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent)
-{
-
- try {
- QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist");
- QDataStream stream(&encoded, QIODevice::ReadOnly);
- std::vector<int> sourceRows;
-
- while (!stream.atEnd()) {
- int sourceRow, col;
- QMap<int, QVariant> roleDataMap;
- stream >> sourceRow >> col >> roleDataMap;
- if (col == 0) {
- sourceRows.push_back(sourceRow);
- }
- }
-
- if (row == -1) {
- row = parent.row();
- }
-
- if ((row < 0) || (static_cast<unsigned int>(row) >= ModInfo::getNumMods())) {
- return false;
- }
-
- int newPriority = 0;
- {
- if ((row < 0) || (row > static_cast<int>(m_Profile->numRegularMods()))) {
- newPriority = m_Profile->numRegularMods() + 1;
- } else {
- newPriority = m_Profile->getModPriority(row);
- }
- if (newPriority == -1) {
- newPriority = m_Profile->numRegularMods() + 1;
- }
- }
- changeModPriority(sourceRows, newPriority);
- } catch (const std::exception &e) {
- reportError(tr("drag&drop failed: %1").arg(e.what()));
- }
-
- return false;
-}
-
-
-bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent)
-{
- if (action == Qt::IgnoreAction) {
- return true;
- }
-
- if (m_Profile == nullptr) return false;
-
- if (mimeData->hasUrls()) {
- return dropURLs(mimeData, row, parent);
- } else if (mimeData->hasText()) {
- return dropMod(mimeData, row, parent);
- } else {
- return false;
- }
-}
-
-void ModList::removeRowForce(int row, const QModelIndex &parent)
-{
- if (static_cast<unsigned int>(row) >= ModInfo::getNumMods()) {
- return;
- }
- if (m_Profile == nullptr) return;
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
-
- bool wasEnabled = m_Profile->modEnabled(row);
-
- m_Profile->setModEnabled(row, false);
-
- m_Profile->cancelModlistWrite();
- beginRemoveRows(parent, row, row);
- ModInfo::removeMod(row);
- endRemoveRows();
- m_Profile->refreshModStatus(); // removes the mod from the status list
- m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed
-
- if (wasEnabled) {
- emit removeOrigin(modInfo->name());
- }
-
- emit modUninstalled(modInfo->getInstallationFile());
-}
-
-bool ModList::removeRows(int row, int count, const QModelIndex &parent)
-{
- if (static_cast<unsigned int>(row) >= ModInfo::getNumMods()) {
- return false;
- }
- if (m_Profile == nullptr) {
- return false;
- }
-
- bool success = false;
-
- if (count == 1) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
- std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
- if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) {
- emit clearOverwrite();
- success = true;
- }
- }
-
- for (int i = 0; i < count; ++i) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i);
- if (!modInfo->isRegular()) {
- continue;
- }
-
- success = true;
-
- QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"),
- tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()),
- QMessageBox::Yes | QMessageBox::No);
-
- if (confirmBox.exec() == QMessageBox::Yes) {
- m_Profile->setModEnabled(row + i, false);
- removeRowForce(row + i, parent);
- }
- }
-
- return success;
-}
-
-
-void ModList::notifyChange(int rowStart, int rowEnd)
-{
- if (rowStart < 0) {
- m_Overwrite.clear();
- m_Overwritten.clear();
- beginResetModel();
- endResetModel();
- } else {
- if (rowEnd == -1) {
- rowEnd = rowStart;
- }
- emit dataChanged(this->index(rowStart, 0), this->index(rowEnd, this->columnCount() - 1));
- }
-}
-
-
-QModelIndex ModList::index(int row, int column, const QModelIndex&) const
-{
- if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) {
- return QModelIndex();
- }
- QModelIndex res = createIndex(row, column, row);
- return res;
-}
-
-
-QModelIndex ModList::parent(const QModelIndex&) const
-{
- return QModelIndex();
-}
-
-QMap<int, QVariant> ModList::itemData(const QModelIndex &index) const
-{
- QMap<int, QVariant> result = QAbstractItemModel::itemData(index);
- result[Qt::UserRole] = data(index, Qt::UserRole);
- return result;
-}
-
-
-void ModList::dropModeUpdate(bool dropOnItems)
-{
- if (m_DropOnItems != dropOnItems) {
- m_DropOnItems = dropOnItems;
- }
-}
-
-
-QString ModList::getColumnName(int column)
-{
- switch (column) {
- case COL_FLAGS: return tr("Flags");
- case COL_CONTENT: return tr("Content");
- case COL_NAME: return tr("Mod Name");
- case COL_VERSION: return tr("Version");
- case COL_PRIORITY: return tr("Priority");
- case COL_CATEGORY: return tr("Category");
- case COL_GAME: return tr("Source Game");
- case COL_MODID: return tr("Nexus ID");
- case COL_INSTALLTIME: return tr("Installation");
- case COL_NOTES: return tr("Notes");
- default: return tr("unknown");
- }
-}
-
-
-QString ModList::getColumnToolTip(int column)
-{
- switch (column) {
- case COL_NAME: return tr("Name of your mods");
- case COL_VERSION: return tr("Version of the mod (if available)");
- case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus "
- "overwrites files from mods with lower priority.");
- case COL_CATEGORY: return tr("Category of the mod.");
- case COL_GAME: return tr("The source game which was the origin of this mod.");
- case COL_MODID: return tr("Id of the mod as used on Nexus.");
- case COL_FLAGS: return tr("Emblemes to highlight things that might require attention.");
- case COL_CONTENT: return tr("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>");
- case COL_INSTALLTIME: return tr("Time this mod was installed");
- case COL_NOTES: return tr("User notes about the mod");
- default: return tr("unknown");
- }
-}
-
-
-bool ModList::moveSelection(QAbstractItemView *itemView, int direction)
-{
- QItemSelectionModel *selectionModel = itemView->selectionModel();
-
- const QAbstractProxyModel *proxyModel = qobject_cast<const QAbstractProxyModel*>(selectionModel->model());
- const QSortFilterProxyModel *filterModel = nullptr;
-
- while ((filterModel == nullptr) && (proxyModel != nullptr)) {
- filterModel = qobject_cast<const QSortFilterProxyModel*>(proxyModel);
- if (filterModel == nullptr) {
- proxyModel = qobject_cast<const QAbstractProxyModel*>(proxyModel->sourceModel());
- }
- }
- if (filterModel == nullptr) {
- return true;
- }
-
- int offset = -1;
- if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) ||
- ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) {
- offset = 1;
- }
-
- QModelIndexList rows = selectionModel->selectedRows();
- if (direction > 0) {
- for (int i = 0; i < rows.size() / 2; ++i) {
- rows.swap(i, rows.size() - i - 1);
- }
- }
- for (QModelIndex idx : rows) {
- if (filterModel != nullptr) {
- idx = filterModel->mapToSource(idx);
- }
- int newPriority = m_Profile->getModPriority(idx.row()) + offset;
- if ((newPriority >= 0) && (newPriority < static_cast<int>(m_Profile->numRegularMods()))) {
- m_Profile->setModPriority(idx.row(), newPriority);
- notifyChange(idx.row());
- }
- }
- emit modorder_changed();
- return true;
-}
-
-bool ModList::deleteSelection(QAbstractItemView *itemView)
-{
- QItemSelectionModel *selectionModel = itemView->selectionModel();
-
- QModelIndexList rows = selectionModel->selectedRows();
- if (rows.count() > 1) {
- emit removeSelectedMods();
- } else if (rows.count() == 1) {
- removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex());
- }
- return true;
-}
-
-bool ModList::toggleSelection(QAbstractItemView *itemView)
-{
- emit aboutToChangeData();
-
- QItemSelectionModel *selectionModel = itemView->selectionModel();
-
- for (QModelIndex idx : selectionModel->selectedRows()) {
- int modId = idx.data(Qt::UserRole + 1).toInt();
- m_Profile->setModEnabled(modId, !m_Profile->modEnabled(modId));
- emit modlist_changed(idx, 0);
- }
-
- m_Modified = true;
- m_LastCheck.restart();
-
- emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
-
- emit postDataChanged();
-
- return true;
-}
-
-bool ModList::eventFilter(QObject *obj, QEvent *event)
-{
- if (event->type() == QEvent::ContextMenu) {
- QContextMenuEvent *contextEvent = static_cast<QContextMenuEvent*>(event);
- QWidget *object = qobject_cast<QWidget*>(obj);
- if ((object != nullptr) && (contextEvent->reason() == QContextMenuEvent::Mouse)) {
- emit requestColumnSelect(object->mapToGlobal(contextEvent->pos()));
-
- return true;
- }
- } else if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) {
- QAbstractItemView *itemView = qobject_cast<QAbstractItemView*>(obj);
- QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
-
- if ((itemView != nullptr)
- && (keyEvent->modifiers() == Qt::ControlModifier)
- && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) {
- return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1);
- } else if (keyEvent->key() == Qt::Key_Delete) {
- return deleteSelection(itemView);
- } else if (keyEvent->key() == Qt::Key_Space) {
- return toggleSelection(itemView);
- }
- return QAbstractItemModel::eventFilter(obj, event);
- }
- return QAbstractItemModel::eventFilter(obj, event);
-}
-
-
-void ModList::enableSelected(const QItemSelectionModel *selectionModel)
-{
- if (selectionModel->hasSelection()) {
- bool dirty = false;
- for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
- int modID = m_Profile->modIndexByPriority(row.data().toInt());
- if (!m_Profile->modEnabled(modID)) {
- m_Profile->setModEnabled(modID, true);
- emit modlist_changed(row, 0);
- }
- }
- }
-}
-
-
-void ModList::disableSelected(const QItemSelectionModel *selectionModel)
-{
- if (selectionModel->hasSelection()) {
- bool dirty = false;
- for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
- int modID = m_Profile->modIndexByPriority(row.data().toInt());
- if (m_Profile->modEnabled(modID)) {
- m_Profile->setModEnabled(modID, false);
- emit modlist_changed(row, 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 "modlist.h"
+
+#include "messagedialog.h"
+#include "installationtester.h"
+#include "qtgroupingproxy.h"
+#include "viewmarkingscrollbar.h"
+#include "modlistsortproxy.h"
+#include "pluginlist.h"
+#include "settings.h"
+#include "modinforegular.h"
+#include <appconfig.h>
+#include <utility.h>
+#include <report.h>
+
+#include <QFileInfo>
+#include <QDir>
+#include <QDirIterator>
+#include <QMimeData>
+#include <QStandardItemModel>
+#include <QMessageBox>
+#include <QStringList>
+#include <QEvent>
+#include <QContextMenuEvent>
+#include <QMenu>
+#include <QCheckBox>
+#include <QWidgetAction>
+#include <QAbstractItemView>
+#include <QSortFilterProxyModel>
+#include <QApplication>
+#include <QFontDatabase>
+
+#include <sstream>
+#include <stdexcept>
+#include <algorithm>
+
+
+using namespace MOBase;
+
+
+ModList::ModList(PluginContainer *pluginContainer, QObject *parent)
+ : QAbstractItemModel(parent)
+ , m_Profile(nullptr)
+ , m_NexusInterface(nullptr)
+ , m_Modified(false)
+ , m_FontMetrics(QFont())
+ , m_DropOnItems(false)
+ , m_PluginContainer(pluginContainer)
+{
+ m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)"));
+ m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface"));
+ m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes"));
+ m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive"));
+ m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)"));
+ m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin"));
+ m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher"));
+ m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music"));
+ m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures"));
+ m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration"));
+ m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files"));
+ m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files"));
+
+ m_LastCheck.start();
+}
+
+ModList::~ModList()
+{
+ m_ModStateChanged.disconnect_all_slots();
+ m_ModMoved.disconnect_all_slots();
+}
+
+void ModList::setProfile(Profile *profile)
+{
+ m_Profile = profile;
+}
+
+int ModList::rowCount(const QModelIndex &parent) const
+{
+ if (!parent.isValid()) {
+ return ModInfo::getNumMods();
+ } else {
+ return 0;
+ }
+}
+
+bool ModList::hasChildren(const QModelIndex &parent) const
+{
+ if (!parent.isValid()) {
+ return ModInfo::getNumMods() > 0;
+ } else {
+ return false;
+ }
+}
+
+
+int ModList::columnCount(const QModelIndex &) const
+{
+ return COL_LASTCOLUMN + 1;
+}
+
+
+QVariant ModList::getOverwriteData(int column, int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole: {
+ if (column == 0) {
+ return "Overwrite";
+ } else {
+ return QVariant();
+ }
+ } break;
+ case Qt::CheckStateRole: {
+ if (column == 0) {
+ return Qt::Checked;
+ } else {
+ return QVariant();
+ }
+ } break;
+ case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+ case Qt::UserRole: return -1;
+ case Qt::ForegroundRole: return QBrush(Qt::red);
+ case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)");
+ default: return QVariant();
+ }
+}
+
+
+QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const
+{
+ switch (flag) {
+ case ModInfo::FLAG_BACKUP: return tr("Backup");
+ case ModInfo::FLAG_SEPARATOR: return tr("Separator");
+ case ModInfo::FLAG_INVALID: return tr("No valid game data");
+ case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet");
+ case ModInfo::FLAG_NOTES: {
+ QStringList output;
+ if (!modInfo->comments().isEmpty())
+ output << QString("<i>%1</i>").arg(modInfo->comments());
+ if (!modInfo->notes().isEmpty())
+ output << QString("<i>%1</i>").arg(modInfo->notes());
+ return output.join("");
+ }
+ case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files");
+ case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files");
+ case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten");
+ case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten");
+ case ModInfo::FLAG_ALTERNATE_GAME: return tr("<br>This mod is for a different game, "
+ "make sure it's compatible or it could cause crashes.");
+ case ModInfo::FLAG_TRACKED: return tr("Mod is being tracked on the website");
+ default: return "";
+ }
+}
+
+
+QVariantList ModList::contentsToIcons(const std::vector<ModInfo::EContent> &contents) const
+{
+ QVariantList result;
+ std::set<ModInfo::EContent> contentsSet(contents.begin(), contents.end());
+ for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) {
+ if (contentsSet.find(iter->first) != contentsSet.end()) {
+ result.append(std::get<0>(iter->second));
+ } else {
+ result.append(QString());
+ }
+ }
+ return result;
+}
+
+QString ModList::contentsToToolTip(const std::vector<ModInfo::EContent> &contents) const
+{
+ QString result("<table cellspacing=7>");
+
+ std::set<ModInfo::EContent> contentsSet(contents.begin(), contents.end());
+ for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) {
+ if (contentsSet.find(iter->first) != contentsSet.end()) {
+ result.append(QString("<tr><td><img src=\"%1\" width=32/></td>"
+ "<td valign=\"middle\">%2</td></tr>")
+ .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second)));
+ }
+ }
+ result.append("</table>");
+ return result;
+}
+
+
+QVariant ModList::data(const QModelIndex &modelIndex, int role) const
+{
+ if (m_Profile == nullptr) return QVariant();
+ if (!modelIndex.isValid()) return QVariant();
+ unsigned int modIndex = modelIndex.row();
+ int column = modelIndex.column();
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ if ((role == Qt::DisplayRole) ||
+ (role == Qt::EditRole)) {
+ if ((column == COL_FLAGS)
+ || (column == COL_CONTENT)) {
+ return QVariant();
+ } else if (column == COL_NAME) {
+ auto flags = modInfo->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
+ {
+ return modInfo->name().replace("_separator", "");
+ }
+ else
+ return modInfo->name();
+ } else if (column == COL_VERSION) {
+ VersionInfo verInfo = modInfo->getVersion();
+ QString version = verInfo.displayString();
+ if (role != Qt::EditRole) {
+ if (version.isEmpty() && modInfo->canBeUpdated()) {
+ version = "?";
+ }
+ }
+ return version;
+ } else if (column == COL_PRIORITY) {
+ int priority = modInfo->getFixedPriority();
+ if (priority != INT_MIN) {
+ return QVariant(); // hide priority for mods where it's fixed
+ } else {
+ return m_Profile->getModPriority(modIndex);
+ }
+ } else if (column == COL_MODID) {
+ int modID = modInfo->getNexusID();
+ if (modID > 0) {
+ return modID;
+ }
+ else {
+ return QVariant();
+ }
+ } else if (column == COL_GAME) {
+ if (m_PluginContainer != nullptr) {
+ for (auto game : m_PluginContainer->plugins<IPluginGame>()) {
+ if (game->gameShortName().compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0)
+ return game->gameName();
+ }
+ }
+ return modInfo->getGameName();
+ } else if (column == COL_CATEGORY) {
+ if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) {
+ return tr("Non-MO");
+ } else {
+ int category = modInfo->getPrimaryCategory();
+ if (category != -1) {
+ CategoryFactory &categoryFactory = CategoryFactory::instance();
+ if (categoryFactory.categoryExists(category)) {
+ try {
+ int categoryIdx = categoryFactory.getCategoryIndex(category);
+ return categoryFactory.getCategoryName(categoryIdx);
+ } catch (const std::exception &e) {
+ qCritical("failed to retrieve category name: %s", e.what());
+ return QString();
+ }
+ } else {
+ qWarning("category %d doesn't exist (may have been removed)", category);
+ modInfo->setCategory(category, false);
+ return QString();
+ }
+ } else {
+ return QVariant();
+ }
+ }
+ } else if (column == COL_INSTALLTIME) {
+ // display installation time for mods that can be updated
+ if (modInfo->creationTime().isValid()) {
+ return modInfo->creationTime();
+ } else {
+ return QVariant();
+ }
+ } else if (column == COL_NOTES) {
+ return modInfo->comments();
+ } else {
+ return tr("invalid");
+ }
+ } else if ((role == Qt::CheckStateRole) && (column == 0)) {
+ if (modInfo->canBeEnabled()) {
+ return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked;
+ } else {
+ return QVariant();
+ }
+ } else if (role == Qt::TextAlignmentRole) {
+ auto flags = modInfo->getFlags();
+ if (column == COL_NAME) {
+ if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) {
+ return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+ } else {
+ return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+ }
+ } else if (column == COL_VERSION) {
+ return QVariant(Qt::AlignRight | Qt::AlignVCenter);
+ } else if (column == COL_NOTES) {
+ return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+ } else {
+ return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
+ }
+ } else if (role == Qt::UserRole) {
+ if (column == COL_CATEGORY) {
+ QVariantList categoryNames;
+ std::set<int> categories = modInfo->getCategories();
+ CategoryFactory &categoryFactory = CategoryFactory::instance();
+ for (auto iter = categories.begin(); iter != categories.end(); ++iter) {
+ categoryNames.append(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*iter)));
+ }
+ if (categoryNames.count() != 0) {
+ return categoryNames;
+ } else {
+ return QVariant();
+ }
+ } else if (column == COL_PRIORITY) {
+ int priority = modInfo->getFixedPriority();
+ if (priority != INT_MIN) {
+ return priority;
+ } else {
+ return m_Profile->getModPriority(modIndex);
+ }
+ } else {
+ return modInfo->getNexusID();
+ }
+ } else if (role == Qt::UserRole + 1) {
+ return modIndex;
+ } else if (role == Qt::UserRole + 2) {
+ switch (column) {
+ case COL_MODID: return QtGroupingProxy::AGGR_FIRST;
+ case COL_VERSION: return QtGroupingProxy::AGGR_MAX;
+ case COL_CATEGORY: return QtGroupingProxy::AGGR_FIRST;
+ case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN;
+ default: return QtGroupingProxy::AGGR_NONE;
+ }
+ } else if (role == Qt::UserRole + 3) {
+ return contentsToIcons(modInfo->getContents());
+ } else if (role == Qt::UserRole + 4) {
+ return modInfo->getGameName();
+ } else if (role == Qt::FontRole) {
+ QFont result;
+ auto flags = modInfo->getFlags();
+ if (column == COL_NAME) {
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
+ {
+ //result.setCapitalization(QFont::AllUppercase);
+ result.setItalic(true);
+ //result.setUnderline(true);
+ result.setBold(true);
+ } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) {
+ result.setItalic(true);
+ }
+ } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) {
+ result.setItalic(true);
+ } else if (column == COL_VERSION) {
+ if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) {
+ result.setWeight(QFont::Bold);
+ }
+ if (modInfo->canBeUpdated()) {
+ result.setItalic(true);
+ }
+ }
+ return result;
+ } else if (role == Qt::DecorationRole) {
+ if (column == COL_VERSION) {
+ if (modInfo->updateAvailable()) {
+ return QIcon(":/MO/gui/update_available");
+ } else if (modInfo->downgradeAvailable()) {
+ return QIcon(":/MO/gui/warning");
+ } else if (modInfo->getVersion().scheme() == VersionInfo::SCHEME_DATE) {
+ return QIcon(":/MO/gui/version_date");
+ }
+ }
+ return QVariant();
+ } else if (role == Qt::ForegroundRole) {
+ if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) {
+ return Settings::getIdealTextColor(modInfo->getColor());
+ } else if (column == COL_NAME) {
+ int highlight = modInfo->getHighlight();
+ if (highlight & ModInfo::HIGHLIGHT_IMPORTANT)
+ return QBrush(Qt::darkRed);
+ else if (highlight & ModInfo::HIGHLIGHT_INVALID)
+ return QBrush(Qt::darkGray);
+ } else if (column == COL_VERSION) {
+ if (!modInfo->getNewestVersion().isValid()) {
+ return QVariant();
+ } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) {
+ return QBrush(Qt::red);
+ } else {
+ return QBrush(Qt::darkGreen);
+ }
+ }
+ return QVariant();
+ } else if ((role == Qt::BackgroundRole)
+ || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
+ bool overwrite = m_Overwrite.find(modIndex) != m_Overwrite.end();
+ bool archiveOverwrite = m_ArchiveOverwrite.find(modIndex) != m_ArchiveOverwrite.end();
+ bool archiveLooseOverwrite = m_ArchiveLooseOverwrite.find(modIndex) != m_ArchiveLooseOverwrite.end();
+ bool overwritten = m_Overwritten.find(modIndex) != m_Overwritten.end();
+ bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end();
+ bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end();
+ if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) {
+ return Settings::instance().modlistContainsPluginColor();
+ } else if (overwritten || archiveLooseOverwritten) {
+ return Settings::instance().modlistOverwritingLooseColor();
+ } else if (overwrite || archiveLooseOverwrite) {
+ return Settings::instance().modlistOverwrittenLooseColor();
+ } else if (archiveOverwritten) {
+ return Settings::instance().modlistOverwritingArchiveColor();
+ } else if (archiveOverwrite) {
+ return Settings::instance().modlistOverwrittenArchiveColor();
+ } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)
+ && modInfo->getColor().isValid()
+ && ((role != ViewMarkingScrollBar::DEFAULT_ROLE)
+ || Settings::instance().colorSeparatorScrollbar())) {
+ return modInfo->getColor();
+ } else {
+ return QVariant();
+ }
+ } else if (role == Qt::ToolTipRole) {
+ if (column == COL_FLAGS) {
+ QString result;
+
+ for (ModInfo::EFlag flag : modInfo->getFlags()) {
+ if (result.length() != 0) result += "<br>";
+ result += getFlagText(flag, modInfo);
+ }
+
+ return result;
+ } else if (column == COL_CONTENT) {
+ return contentsToToolTip(modInfo->getContents());
+ } else if (column == COL_NAME) {
+ try {
+ return modInfo->getDescription();
+ } catch (const std::exception &e) {
+ qCritical("invalid mod description: %s", e.what());
+ return QString();
+ }
+ } else if (column == COL_VERSION) {
+ QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString(3)).arg(modInfo->getNewestVersion().displayString(3));
+ if (modInfo->downgradeAvailable()) {
+ text += "<br>" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn "
+ "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. "
+ "Either way you may want to \"upgrade\".");
+ }
+ if (modInfo->getNexusFileStatus() == 4) {
+ text += "<br>" + tr("This file has been marked as \"Old\". There is most likely an updated version of this file available.");
+ } else if (modInfo->getNexusFileStatus() == 6) {
+ text += "<br>" + tr("This file has been marked as \"Deleted\"! You may want to check for an update or remove the nexus ID from this mod!");
+ }
+ if (modInfo->getNexusID() > 0) {
+ if (!modInfo->canBeUpdated()) {
+ qint64 remains = QDateTime::currentDateTimeUtc().secsTo(modInfo->getExpires());
+ qint64 minutes = remains / 60;
+ qint64 seconds = remains % 60;
+ QString remainsStr(tr("%1 minute(s) and %2 second(s)").arg(minutes).arg(seconds));
+ text += "<br>" + tr("This mod will be available to check in %2.")
+ .arg(remainsStr);
+ }
+ }
+ return text;
+ } else if (column == COL_CATEGORY) {
+ const std::set<int> &categories = modInfo->getCategories();
+ std::wostringstream categoryString;
+ categoryString << ToWString(tr("Categories: <br>"));
+ CategoryFactory &categoryFactory = CategoryFactory::instance();
+ for (std::set<int>::const_iterator catIter = categories.begin();
+ catIter != categories.end(); ++catIter) {
+ if (catIter != categories.begin()) {
+ categoryString << " , ";
+ }
+ try {
+ categoryString << "<span style=\"white-space: nowrap;\"><i>" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << "</font></span>";
+ } catch (const std::exception &e) {
+ qCritical("failed to generate tooltip: %s", e.what());
+ return QString();
+ }
+ }
+
+ return ToQString(categoryString.str());
+ } else if (column == COL_NOTES) {
+ return getFlagText(ModInfo::FLAG_NOTES, modInfo);
+ } else {
+ return QVariant();
+ }
+ } else {
+ return QVariant();
+ }
+}
+
+
+bool ModList::renameMod(int index, const QString &newName)
+{
+ QString nameFixed = newName;
+ if (!fixDirectoryName(nameFixed) || nameFixed.isEmpty()) {
+ MessageDialog::showMessage(tr("Invalid name"), nullptr);
+ return false;
+ }
+
+ if (ModList::allMods().contains(nameFixed, Qt::CaseInsensitive) && nameFixed.toLower()!=ModInfo::getByIndex(index)->name().toLower() ) {
+ MessageDialog::showMessage(tr("Name is already in use by another mod"), nullptr);
+ return false;
+ }
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ QString oldName = modInfo->name();
+ if (nameFixed != oldName) {
+ // before we rename, ensure there is no scheduled asynchronous to rewrite
+ m_Profile->cancelModlistWrite();
+
+
+ if (modInfo->setName(nameFixed))
+ // Notice there is a good chance that setName() updated the modinfo indexes
+ // the modRenamed() call will refresh the indexes in the current profile
+ // and update the modlists in all profiles
+ emit modRenamed(oldName, nameFixed);
+ }
+
+ // invalidate the currently displayed state of this list
+ notifyChange(-1);
+ return true;
+}
+
+bool ModList::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (m_Profile == nullptr) return false;
+
+ if (static_cast<unsigned int>(index.row()) >= ModInfo::getNumMods()) {
+ return false;
+ }
+
+ int modID = index.row();
+
+ ModInfo::Ptr info = ModInfo::getByIndex(modID);
+ IModList::ModStates oldState = state(modID);
+
+ bool result = false;
+
+ emit aboutToChangeData();
+
+ if (role == Qt::CheckStateRole) {
+ bool enabled = value.toInt() == Qt::Checked;
+ if (m_Profile->modEnabled(modID) != enabled) {
+ m_Profile->setModEnabled(modID, enabled);
+ m_Modified = true;
+ m_LastCheck.restart();
+ emit modlistChanged(index, role);
+ }
+ result = true;
+ emit dataChanged(index, index);
+ } else if (role == Qt::EditRole) {
+ switch (index.column()) {
+ case COL_NAME: {
+ auto flags = info->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end())
+ {
+ result = renameMod(modID, value.toString() + "_separator");
+ }
+ else
+ result = renameMod(modID, value.toString());
+ } break;
+ case COL_PRIORITY: {
+ bool ok = false;
+ int newPriority = value.toInt(&ok);
+ if (ok && newPriority < 0) {
+ newPriority = 0;
+ }
+ if (ok) {
+ m_Profile->setModPriority(modID, newPriority);
+
+ emit modorder_changed();
+ result = true;
+ } else {
+ result = false;
+ }
+ } break;
+ case COL_MODID: {
+ bool ok = false;
+ int newID = value.toInt(&ok);
+ if (ok) {
+ info->setNexusID(newID);
+ emit modlistChanged(index, role);
+ result = true;
+ } else {
+ result = false;
+ }
+ } break;
+ case COL_VERSION: {
+ VersionInfo::VersionScheme scheme = info->getVersion().scheme();
+ VersionInfo version(value.toString(), scheme, true);
+ if (version.isValid()) {
+ info->setVersion(version);
+ result = true;
+ } else {
+ result = false;
+ }
+ } break;
+ case COL_NOTES: {
+ info->setComments(value.toString());
+ result = true;
+ } break;
+ default: {
+ qWarning("edit on column \"%s\" not supported",
+ getColumnName(index.column()).toUtf8().constData());
+ result = false;
+ } break;
+ }
+ if (result) {
+ emit dataChanged(index, index);
+ }
+ }
+
+ emit postDataChanged();
+
+ IModList::ModStates newState = state(modID);
+ if (oldState != newState) {
+ try {
+ m_ModStateChanged(info->name(), newState);
+ } catch (const std::exception &e) {
+ qCritical("failed to invoke state changed notification: %s", e.what());
+ } catch (...) {
+ qCritical("failed to invoke state changed notification: unknown exception");
+ }
+ }
+
+ return result;
+}
+
+
+QVariant ModList::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation == Qt::Horizontal) {
+ if (role == Qt::DisplayRole) {
+ return getColumnName(section);
+ } else if (role == Qt::ToolTipRole) {
+ return getColumnToolTip(section);
+ } else if (role == Qt::TextAlignmentRole) {
+ return QVariant(Qt::AlignCenter);
+ } else if (role == Qt::SizeHintRole) {
+ QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section));
+ temp.rwidth() += 20;
+ temp.rheight() += 12;
+ return temp;
+ }
+ }
+ return QAbstractItemModel::headerData(section, orientation, role);
+}
+
+
+Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const
+{
+ Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
+ if (modelIndex.internalId() < 0) {
+ return Qt::ItemIsEnabled;
+ }
+ if (modelIndex.isValid()) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row());
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+ if (modInfo->getFixedPriority() == INT_MIN) {
+ result |= Qt::ItemIsDragEnabled;
+ result |= Qt::ItemIsUserCheckable;
+ if ((modelIndex.column() == COL_PRIORITY) ||
+ (modelIndex.column() == COL_VERSION) ||
+ (modelIndex.column() == COL_MODID)) {
+ result |= Qt::ItemIsEditable;
+ }
+ if (((modelIndex.column() == COL_NAME) ||
+ (modelIndex.column() == COL_NOTES))
+ && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) {
+ result |= Qt::ItemIsEditable;
+ }
+ }
+ if (m_DropOnItems
+ && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) {
+ result |= Qt::ItemIsDropEnabled;
+ }
+ } else {
+ if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled;
+ }
+ return result;
+}
+
+
+QStringList ModList::mimeTypes() const
+{
+ QStringList result = QAbstractItemModel::mimeTypes();
+ result.append("text/uri-list");
+ return result;
+}
+
+QMimeData *ModList::mimeData(const QModelIndexList &indexes) const
+{
+ QMimeData *result = QAbstractItemModel::mimeData(indexes);
+ result->setData("text/plain", "mod");
+ return result;
+}
+
+void ModList::changeModPriority(std::vector<int> sourceIndices, int newPriority)
+{
+ if (m_Profile == nullptr) return;
+
+ emit layoutAboutToBeChanged();
+ Profile *profile = m_Profile;
+
+ // sort the moving mods by ascending priorities
+ std::sort(sourceIndices.begin(), sourceIndices.end(),
+ [profile](const int &LHS, const int &RHS) {
+ return profile->getModPriority(LHS) > profile->getModPriority(RHS);
+ });
+
+ // move mods that are decreasing in priority
+ for (std::vector<int>::const_iterator iter = sourceIndices.begin();
+ iter != sourceIndices.end(); ++iter) {
+ int oldPriority = profile->getModPriority(*iter);
+ if (oldPriority > newPriority) {
+ profile->setModPriority(*iter, newPriority);
+ m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority);
+ }
+ }
+
+ // sort the moving mods by descending priorities
+ std::sort(sourceIndices.begin(), sourceIndices.end(),
+ [profile](const int &LHS, const int &RHS) {
+ return profile->getModPriority(LHS) < profile->getModPriority(RHS);
+ });
+
+ // if at least one mod is increasing in priority, the target index is
+ // that of the row BELOW the dropped location, otherwise it's the one above
+ for (std::vector<int>::const_iterator iter = sourceIndices.begin();
+ iter != sourceIndices.end(); ++iter) {
+ int oldPriority = profile->getModPriority(*iter);
+ if (oldPriority < newPriority) {
+ --newPriority;
+ break;
+ }
+ }
+
+ // move mods that are increasing in priority
+ for (std::vector<int>::const_iterator iter = sourceIndices.begin();
+ iter != sourceIndices.end(); ++iter) {
+ int oldPriority = profile->getModPriority(*iter);
+ if (oldPriority < newPriority) {
+ profile->setModPriority(*iter, newPriority);
+ m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority);
+ }
+ }
+
+ emit layoutChanged();
+
+ emit modorder_changed();
+}
+
+
+void ModList::changeModPriority(int sourceIndex, int newPriority)
+{
+ if (m_Profile == nullptr) return;
+ emit layoutAboutToBeChanged();
+
+ m_Profile->setModPriority(sourceIndex, newPriority);
+
+ emit layoutChanged();
+
+ emit modorder_changed();
+}
+
+void ModList::setOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
+{
+ m_Overwrite = overwrite;
+ m_Overwritten = overwritten;
+ notifyChange(0, rowCount() - 1);
+}
+
+void ModList::setArchiveOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
+{
+ m_ArchiveOverwrite = overwrite;
+ m_ArchiveOverwritten = overwritten;
+ notifyChange(0, rowCount() - 1);
+}
+
+void ModList::setArchiveLooseOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten)
+{
+ m_ArchiveLooseOverwrite = overwrite;
+ m_ArchiveLooseOverwritten = overwritten;
+ notifyChange(0, rowCount() - 1);
+}
+
+void ModList::setPluginContainer(PluginContainer *pluginContianer)
+{
+ m_PluginContainer = pluginContianer;
+}
+
+bool ModList::modInfoAboutToChange(ModInfo::Ptr info)
+{
+ if (m_ChangeInfo.name.isEmpty()) {
+ m_ChangeInfo.name = info->name();
+ m_ChangeInfo.state = state(info->name());
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void ModList::modInfoChanged(ModInfo::Ptr info)
+{
+ if (info->name() == m_ChangeInfo.name) {
+ IModList::ModStates newState = state(info->name());
+ if (m_ChangeInfo.state != newState) {
+ m_ModStateChanged(info->name(), newState);
+ }
+
+ int row = ModInfo::getIndex(info->name());
+ info->testValid();
+ emit aboutToChangeData();
+ emit dataChanged(index(row, 0), index(row, columnCount()));
+ emit postDataChanged();
+ } else {
+ qCritical("modInfoChanged not called after modInfoAboutToChange");
+ }
+ m_ChangeInfo.name = QString();
+}
+
+void ModList::disconnectSlots() {
+ m_ModMoved.disconnect_all_slots();
+ m_ModStateChanged.disconnect_all_slots();
+}
+
+int ModList::timeElapsedSinceLastChecked() const
+{
+ return m_LastCheck.elapsed();
+}
+
+void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry)
+{
+ for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
+ ModInfo::getByIndex(i)->setPluginSelected(false);
+ }
+ for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) {
+ QString modName = idx.data().toString();
+
+ const MOShared::FileEntry::Ptr fileEntry = directoryEntry.findFile(modName.toStdWString());
+ if (fileEntry.get() != nullptr) {
+ bool archive = false;
+ std::vector<std::pair<int, std::pair<std::wstring, int>>> origins;
+ {
+ std::vector<std::pair<int, std::pair<std::wstring, int>>> alternatives = fileEntry->getAlternatives();
+ origins.insert(origins.end(), std::pair<int, std::pair<std::wstring, int>>(fileEntry->getOrigin(archive), fileEntry->getArchive()));
+ }
+ for (auto originInfo : origins) {
+ MOShared::FilesOrigin &origin = directoryEntry.getOriginByID(originInfo.first);
+ for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
+ if (ModInfo::getByIndex(i)->internalName() == QString::fromStdWString(origin.getName())) {
+ ModInfo::getByIndex(i)->setPluginSelected(true);
+ break;
+ }
+ }
+ }
+ }
+ }
+ notifyChange(0, rowCount() - 1);
+}
+
+IModList::ModStates ModList::state(unsigned int modIndex) const
+{
+ IModList::ModStates result;
+ if (modIndex != UINT_MAX) {
+ result |= IModList::STATE_EXISTS;
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ if (modInfo->isEmpty()) {
+ result |= IModList::STATE_EMPTY;
+ }
+ if (modInfo->endorsedState() == ModInfo::ENDORSED_TRUE) {
+ result |= IModList::STATE_ENDORSED;
+ }
+ if (modInfo->isValid()) {
+ result |= IModList::STATE_VALID;
+ }
+ if (modInfo->isRegular()) {
+ QSharedPointer<ModInfoRegular> modInfoRegular = modInfo.staticCast<ModInfoRegular>();
+ if (modInfoRegular->isAlternate() && !modInfoRegular->isConverted())
+ result |= IModList::STATE_ALTERNATE;
+ if (!modInfo->isValid() && modInfoRegular->isValidated())
+ result |= IModList::STATE_VALID;
+ }
+ if (modInfo->canBeEnabled()) {
+ if (m_Profile->modEnabled(modIndex)) {
+ result |= IModList::STATE_ACTIVE;
+ }
+ } else {
+ result |= IModList::STATE_ESSENTIAL;
+ }
+ }
+ return result;
+}
+
+QString ModList::displayName(const QString &internalName) const
+{
+ unsigned int modIndex = ModInfo::getIndex(internalName);
+ if (modIndex == UINT_MAX) {
+ // might be better to throw an exception?
+ return internalName;
+ } else {
+ return ModInfo::getByIndex(modIndex)->name();
+ }
+}
+
+QStringList ModList::allMods() const
+{
+ QStringList result;
+ for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) {
+ result.append(ModInfo::getByIndex(i)->internalName());
+ }
+ return result;
+}
+
+IModList::ModStates ModList::state(const QString &name) const
+{
+ unsigned int modIndex = ModInfo::getIndex(name);
+
+ return state(modIndex);
+}
+
+bool ModList::setActive(const QString &name, bool active)
+{
+ unsigned int modIndex = ModInfo::getIndex(name);
+ if (modIndex == UINT_MAX) {
+ return false;
+ } else {
+ m_Profile->setModEnabled(modIndex, active);
+
+ IModList::ModStates newState = state(modIndex);
+ m_ModStateChanged(name, newState);
+ return true;
+ }
+}
+
+int ModList::priority(const QString &name) const
+{
+ unsigned int modIndex = ModInfo::getIndex(name);
+ if (modIndex == UINT_MAX) {
+ return -1;
+ } else {
+ return m_Profile->getModPriority(modIndex);
+ }
+}
+
+bool ModList::setPriority(const QString &name, int newPriority)
+{
+ if ((newPriority < 0) || (newPriority >= static_cast<int>(m_Profile->numRegularMods()))) {
+ return false;
+ }
+
+ unsigned int modIndex = ModInfo::getIndex(name);
+ if (modIndex == UINT_MAX) {
+ return false;
+ } else {
+ m_Profile->setModPriority(modIndex, newPriority);
+ notifyChange(modIndex);
+ return true;
+ }
+}
+
+bool ModList::onModStateChanged(const std::function<void (const QString &, IModList::ModStates)> &func)
+{
+ auto conn = m_ModStateChanged.connect(func);
+ return conn.connected();
+}
+
+bool ModList::onModMoved(const std::function<void (const QString &, int, int)> &func)
+{
+ auto conn = m_ModMoved.connect(func);
+ return conn.connected();
+}
+
+bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent)
+{
+ if (row == -1) {
+ row = parent.row();
+ }
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
+ QDir modDir = QDir(modInfo->absolutePath());
+
+ QDir allModsDir(Settings::instance().getModDirectory());
+ QDir overwriteDir(Settings::instance().getOverwriteDirectory());
+
+ QStringList sourceList;
+ QStringList targetList;
+ QList<QPair<QString,QString>> relativePathList;
+
+ unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool {
+ std::vector<ModInfo::EFlag> flags = mod->getFlags();
+ return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); });
+
+ QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name();
+
+ for (auto url : mimeData->urls()) {
+ //qDebug("URL drop requested: %s -> %s", qUtf8Printable(url.url()), qUtf8Printable(modDir.canonicalPath()));
+ if (!url.isLocalFile()) {
+ qDebug("URL drop ignored: \"%s\" is not a local file", qUtf8Printable(url.url()));
+ continue;
+ }
+
+ QFileInfo sourceInfo(url.toLocalFile());
+ QString sourceFile = sourceInfo.canonicalFilePath();
+
+ QString relativePath;
+ QString originName;
+
+ if (sourceFile.startsWith(allModsDir.canonicalPath())) {
+ QDir relativeDir(allModsDir.relativeFilePath(sourceFile));
+ QStringList splitPath = relativeDir.path().split("/");
+ originName = splitPath[0];
+ splitPath.pop_front();
+ relativePath = splitPath.join("/");
+ } else if (sourceFile.startsWith(overwriteDir.canonicalPath())) {
+ originName = overwriteName;
+ relativePath = overwriteDir.relativeFilePath(sourceFile);
+ } else {
+ qDebug("URL drop ignored: \"%s\" is not a known file to MO", qUtf8Printable(sourceFile));
+ continue;
+ }
+
+ QFileInfo targetInfo(modDir.absoluteFilePath(sourceInfo.fileName()));
+ sourceList << sourceFile;
+ targetList << targetInfo.absoluteFilePath();
+ relativePathList << QPair<QString,QString>(relativePath, originName);
+ }
+
+ if (sourceList.count()) {
+ if (!shellMove(sourceList, targetList)) {
+ qDebug("Failed to move file (error %d)", ::GetLastError());
+ return false;
+ }
+ }
+
+ for (auto iter : relativePathList) {
+ emit fileMoved(iter.first, iter.second, modInfo->name());
+ }
+
+ if (!modInfo->isValid()) {
+ modInfo->testValid();
+ }
+
+ return true;
+}
+
+bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent)
+{
+
+ try {
+ QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist");
+ QDataStream stream(&encoded, QIODevice::ReadOnly);
+ std::vector<int> sourceRows;
+
+ while (!stream.atEnd()) {
+ int sourceRow, col;
+ QMap<int, QVariant> roleDataMap;
+ stream >> sourceRow >> col >> roleDataMap;
+ if (col == 0) {
+ sourceRows.push_back(sourceRow);
+ }
+ }
+
+ if (row == -1) {
+ row = parent.row();
+ }
+
+ if ((row < 0) || (static_cast<unsigned int>(row) >= ModInfo::getNumMods())) {
+ return false;
+ }
+
+ int newPriority = 0;
+ {
+ if ((row < 0) || (row > static_cast<int>(m_Profile->numRegularMods()))) {
+ newPriority = m_Profile->numRegularMods() + 1;
+ } else {
+ newPriority = m_Profile->getModPriority(row);
+ }
+ if (newPriority == -1) {
+ newPriority = m_Profile->numRegularMods() + 1;
+ }
+ }
+ changeModPriority(sourceRows, newPriority);
+ } catch (const std::exception &e) {
+ reportError(tr("drag&drop failed: %1").arg(e.what()));
+ }
+
+ return false;
+}
+
+
+bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent)
+{
+ if (action == Qt::IgnoreAction) {
+ return true;
+ }
+
+ if (m_Profile == nullptr) return false;
+
+ if (mimeData->hasUrls()) {
+ return dropURLs(mimeData, row, parent);
+ } else if (mimeData->hasText()) {
+ return dropMod(mimeData, row, parent);
+ } else {
+ return false;
+ }
+}
+
+void ModList::removeRowForce(int row, const QModelIndex &parent)
+{
+ if (static_cast<unsigned int>(row) >= ModInfo::getNumMods()) {
+ return;
+ }
+ if (m_Profile == nullptr) return;
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
+
+ bool wasEnabled = m_Profile->modEnabled(row);
+
+ m_Profile->setModEnabled(row, false);
+
+ m_Profile->cancelModlistWrite();
+ beginRemoveRows(parent, row, row);
+ ModInfo::removeMod(row);
+ endRemoveRows();
+ m_Profile->refreshModStatus(); // removes the mod from the status list
+ m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed
+
+ if (wasEnabled) {
+ emit removeOrigin(modInfo->name());
+ }
+
+ emit modUninstalled(modInfo->getInstallationFile());
+}
+
+bool ModList::removeRows(int row, int count, const QModelIndex &parent)
+{
+ if (static_cast<unsigned int>(row) >= ModInfo::getNumMods()) {
+ return false;
+ }
+ if (m_Profile == nullptr) {
+ return false;
+ }
+
+ bool success = false;
+
+ if (count == 1) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
+ std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
+ if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) {
+ emit clearOverwrite();
+ success = true;
+ }
+ }
+
+ for (int i = 0; i < count; ++i) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i);
+ if (!modInfo->isRegular()) {
+ continue;
+ }
+
+ success = true;
+
+ QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"),
+ tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()),
+ QMessageBox::Yes | QMessageBox::No);
+
+ if (confirmBox.exec() == QMessageBox::Yes) {
+ m_Profile->setModEnabled(row + i, false);
+ removeRowForce(row + i, parent);
+ }
+ }
+
+ return success;
+}
+
+
+void ModList::notifyChange(int rowStart, int rowEnd)
+{
+ if (rowStart < 0) {
+ m_Overwrite.clear();
+ m_Overwritten.clear();
+ m_ArchiveOverwrite.clear();
+ m_ArchiveOverwritten.clear();
+ m_ArchiveLooseOverwrite.clear();
+ m_ArchiveLooseOverwritten.clear();
+ beginResetModel();
+ endResetModel();
+ } else {
+ if (rowEnd == -1) {
+ rowEnd = rowStart;
+ }
+ emit dataChanged(this->index(rowStart, 0), this->index(rowEnd, this->columnCount() - 1));
+ }
+}
+
+
+QModelIndex ModList::index(int row, int column, const QModelIndex&) const
+{
+ if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) {
+ return QModelIndex();
+ }
+ QModelIndex res = createIndex(row, column, row);
+ return res;
+}
+
+
+QModelIndex ModList::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+QMap<int, QVariant> ModList::itemData(const QModelIndex &index) const
+{
+ QMap<int, QVariant> result = QAbstractItemModel::itemData(index);
+ result[Qt::UserRole] = data(index, Qt::UserRole);
+ return result;
+}
+
+
+void ModList::dropModeUpdate(bool dropOnItems)
+{
+ if (m_DropOnItems != dropOnItems) {
+ m_DropOnItems = dropOnItems;
+ }
+}
+
+
+QString ModList::getColumnName(int column)
+{
+ switch (column) {
+ case COL_FLAGS: return tr("Flags");
+ case COL_CONTENT: return tr("Content");
+ case COL_NAME: return tr("Mod Name");
+ case COL_VERSION: return tr("Version");
+ case COL_PRIORITY: return tr("Priority");
+ case COL_CATEGORY: return tr("Category");
+ case COL_GAME: return tr("Source Game");
+ case COL_MODID: return tr("Nexus ID");
+ case COL_INSTALLTIME: return tr("Installation");
+ case COL_NOTES: return tr("Notes");
+ default: return tr("unknown");
+ }
+}
+
+
+QString ModList::getColumnToolTip(int column)
+{
+ switch (column) {
+ case COL_NAME: return tr("Name of your mods");
+ case COL_VERSION: return tr("Version of the mod (if available)");
+ case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus "
+ "overwrites files from mods with lower priority.");
+ case COL_CATEGORY: return tr("Category of the mod.");
+ case COL_GAME: return tr("The source game which was the origin of this mod.");
+ case COL_MODID: return tr("Id of the mod as used on Nexus.");
+ case COL_FLAGS: return tr("Emblemes to highlight things that might require attention.");
+ case COL_CONTENT: return tr("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>");
+ case COL_INSTALLTIME: return tr("Time this mod was installed");
+ case COL_NOTES: return tr("User notes about the mod");
+ default: return tr("unknown");
+ }
+}
+
+
+bool ModList::moveSelection(QAbstractItemView *itemView, int direction)
+{
+ QItemSelectionModel *selectionModel = itemView->selectionModel();
+
+ const QAbstractProxyModel *proxyModel = qobject_cast<const QAbstractProxyModel*>(selectionModel->model());
+ const QSortFilterProxyModel *filterModel = nullptr;
+
+ while ((filterModel == nullptr) && (proxyModel != nullptr)) {
+ filterModel = qobject_cast<const QSortFilterProxyModel*>(proxyModel);
+ if (filterModel == nullptr) {
+ proxyModel = qobject_cast<const QAbstractProxyModel*>(proxyModel->sourceModel());
+ }
+ }
+ if (filterModel == nullptr) {
+ return true;
+ }
+
+ int offset = -1;
+ if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) ||
+ ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) {
+ offset = 1;
+ }
+
+ QModelIndexList rows = selectionModel->selectedRows();
+ if (direction > 0) {
+ for (int i = 0; i < rows.size() / 2; ++i) {
+ rows.swap(i, rows.size() - i - 1);
+ }
+ }
+ for (QModelIndex idx : rows) {
+ if (filterModel != nullptr) {
+ idx = filterModel->mapToSource(idx);
+ }
+ int newPriority = m_Profile->getModPriority(idx.row()) + offset;
+ if ((newPriority >= 0) && (newPriority < static_cast<int>(m_Profile->numRegularMods()))) {
+ m_Profile->setModPriority(idx.row(), newPriority);
+ notifyChange(idx.row());
+ }
+ }
+ emit modorder_changed();
+ return true;
+}
+
+bool ModList::deleteSelection(QAbstractItemView *itemView)
+{
+ QItemSelectionModel *selectionModel = itemView->selectionModel();
+
+ QModelIndexList rows = selectionModel->selectedRows();
+ if (rows.count() > 1) {
+ emit removeSelectedMods();
+ } else if (rows.count() == 1) {
+ removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex());
+ }
+ return true;
+}
+
+bool ModList::toggleSelection(QAbstractItemView *itemView)
+{
+ emit aboutToChangeData();
+
+ QItemSelectionModel *selectionModel = itemView->selectionModel();
+
+ QList<unsigned int> modsToEnable;
+ QList<unsigned int> modsToDisable;
+ QModelIndexList dirtyMods;
+ for (QModelIndex idx : selectionModel->selectedRows()) {
+ int modId = idx.data(Qt::UserRole + 1).toInt();
+ if (m_Profile->modEnabled(modId)) {
+ modsToDisable.append(modId);
+ dirtyMods.append(idx);
+ } else {
+ modsToEnable.append(modId);
+ dirtyMods.append(idx);
+ }
+ }
+
+ m_Profile->setModsEnabled(modsToEnable, modsToDisable);
+
+ emit modlistChanged(dirtyMods, 0);
+
+ m_Modified = true;
+ m_LastCheck.restart();
+
+ emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
+
+ emit postDataChanged();
+
+ return true;
+}
+
+bool ModList::eventFilter(QObject *obj, QEvent *event)
+{
+ if (event->type() == QEvent::ContextMenu) {
+ QContextMenuEvent *contextEvent = static_cast<QContextMenuEvent*>(event);
+ QWidget *object = qobject_cast<QWidget*>(obj);
+ if ((object != nullptr) && (contextEvent->reason() == QContextMenuEvent::Mouse)) {
+ emit requestColumnSelect(object->mapToGlobal(contextEvent->pos()));
+
+ return true;
+ }
+ } else if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) {
+ QAbstractItemView *itemView = qobject_cast<QAbstractItemView*>(obj);
+ QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
+
+ if ((itemView != nullptr)
+ && (keyEvent->modifiers() == Qt::ControlModifier)
+ && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) {
+ return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1);
+ } else if (keyEvent->key() == Qt::Key_Delete) {
+ return deleteSelection(itemView);
+ } else if (keyEvent->key() == Qt::Key_Space) {
+ return toggleSelection(itemView);
+ }
+ return QAbstractItemModel::eventFilter(obj, event);
+ }
+ return QAbstractItemModel::eventFilter(obj, event);
+}
+
+//note: caller needs to make sure sort proxy is updated
+void ModList::enableSelected(const QItemSelectionModel *selectionModel)
+{
+ if (selectionModel->hasSelection()) {
+ QList<unsigned int> modsToEnable;
+ for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
+ int modID = m_Profile->modIndexByPriority(row.data().toInt());
+ modsToEnable.append(modID);
+ }
+ m_Profile->setModsEnabled(modsToEnable, QList<unsigned int>());
+ }
+}
+
+//note: caller needs to make sure sort proxy is updated
+void ModList::disableSelected(const QItemSelectionModel *selectionModel)
+{
+ if (selectionModel->hasSelection()) {
+ QList<unsigned int> modsToDisable;
+ for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
+ int modID = m_Profile->modIndexByPriority(row.data().toInt());
+ modsToDisable.append(modID);
+ }
+ m_Profile->setModsEnabled(QList<unsigned int>(), modsToDisable);
+ }
+}
diff --git a/src/modlist.h b/src/modlist.h
index 42269386..402d662f 100644
--- a/src/modlist.h
+++ b/src/modlist.h
@@ -112,6 +112,9 @@ public:
void setOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten);
void setPluginContainer(PluginContainer *pluginContainer);
+ void setArchiveOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten);
+ void setArchiveLooseOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten);
+
bool modInfoAboutToChange(ModInfo::Ptr info);
void modInfoChanged(ModInfo::Ptr info);
@@ -233,9 +236,18 @@ signals:
* @param role role of the field that changed
* @note this signal must only be emitted if the row really did change.
* Slots handling this signal therefore do not have to verify that a change has happened
- * @note this signal is currently only used in tutorials
**/
- void modlist_changed(const QModelIndex &index, int role);
+ void modlistChanged(const QModelIndex &index, int role);
+
+ /**
+ * @brief emitted whenever multiple row sin the list has changed
+ *
+ * @param indicies the list of indicies of the changed field
+ * @param role role of the field that changed
+ * @note this signal must only be emitted if the row really did change.
+ * Slots handling this signal therefore do not have to verify that a change has happened
+ **/
+ void modlistChanged(const QModelIndexList &indicies, int role);
/**
* @brief emitted to have all selected mods deleted
@@ -331,6 +343,10 @@ private:
std::set<unsigned int> m_Overwrite;
std::set<unsigned int> m_Overwritten;
+ std::set<unsigned int> m_ArchiveOverwrite;
+ std::set<unsigned int> m_ArchiveOverwritten;
+ std::set<unsigned int> m_ArchiveLooseOverwrite;
+ std::set<unsigned int> m_ArchiveLooseOverwritten;
TModInfoChange m_ChangeInfo;
diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp
index 9adaa511..2d9ea4a5 100644
--- a/src/modlistsortproxy.cpp
+++ b/src/modlistsortproxy.cpp
@@ -39,10 +39,6 @@ ModListSortProxy::ModListSortProxy(Profile* profile, QObject *parent)
, m_FilterActive(false)
, m_FilterMode(FILTER_AND)
{
- m_EnabledColumns.set(ModList::COL_FLAGS);
- m_EnabledColumns.set(ModList::COL_NAME);
- m_EnabledColumns.set(ModList::COL_VERSION);
- m_EnabledColumns.set(ModList::COL_PRIORITY);
setDynamicSortFilter(true); // this seems to work without dynamicsortfilter
// but I don't know why. This should be necessary
}
@@ -62,16 +58,21 @@ void ModListSortProxy::updateFilterActive()
void ModListSortProxy::setCategoryFilter(const std::vector<int> &categories)
{
- m_CategoryFilter = categories;
- updateFilterActive();
- invalidate();
+ //avoid refreshing the filter unless we are checking all mods for update.
+ if (categories != m_CategoryFilter || (!categories.empty() && categories.at(0) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)) {
+ m_CategoryFilter = categories;
+ updateFilterActive();
+ invalidate();
+ }
}
void ModListSortProxy::setContentFilter(const std::vector<int> &content)
{
- m_ContentFilter = content;
- updateFilterActive();
- invalidate();
+ if (content != m_ContentFilter) {
+ m_ContentFilter = content;
+ updateFilterActive();
+ invalidate();
+ }
}
Qt::ItemFlags ModListSortProxy::flags(const QModelIndex &modelIndex) const
@@ -85,10 +86,12 @@ void ModListSortProxy::enableAllVisible()
{
if (m_Profile == nullptr) return;
+ QList<unsigned int> modsToEnable;
for (int i = 0; i < this->rowCount(); ++i) {
int modID = mapToSource(index(i, 0)).data(Qt::UserRole + 1).toInt();
- m_Profile->setModEnabled(modID, true);
+ modsToEnable.append(modID);
}
+ m_Profile->setModsEnabled(modsToEnable, QList<unsigned int>());
invalidate();
}
@@ -96,10 +99,12 @@ void ModListSortProxy::disableAllVisible()
{
if (m_Profile == nullptr) return;
+ QList<unsigned int> modsToDisable;
for (int i = 0; i < this->rowCount(); ++i) {
int modID = mapToSource(index(i, 0)).data(Qt::UserRole + 1).toInt();
- m_Profile->setModEnabled(modID, false);
+ modsToDisable.append(modID);
}
+ m_Profile->setModsEnabled(QList<unsigned int>(), modsToDisable);
invalidate();
}
@@ -218,9 +223,25 @@ bool ModListSortProxy::lessThan(const QModelIndex &left,
lt = comp < 0;
}
} break;
+ case ModList::COL_NOTES: {
+ QString leftComments = leftMod->comments();
+ QString rightComments = rightMod->comments();
+ if (leftComments != rightComments) {
+ if (leftComments.isEmpty()) {
+ lt = sortOrder() == Qt::DescendingOrder;
+ } else if (rightComments.isEmpty()) {
+ lt = sortOrder() == Qt::AscendingOrder;
+ } else {
+ lt = leftComments < rightComments;
+ }
+ }
+ } break;
case ModList::COL_PRIORITY: {
// nop, already compared by priority
} break;
+ default: {
+ qWarning() << "Sorting is not defined for column " << left.column();
+ } break;
}
return lt;
}
@@ -238,9 +259,14 @@ bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EFlag> &flags)
{
for (ModInfo::EFlag flag : flags) {
if ((flag == ModInfo::FLAG_CONFLICT_MIXED) ||
- (flag == ModInfo::FLAG_CONFLICT_OVERWRITE) ||
- (flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) ||
- (flag == ModInfo::FLAG_CONFLICT_REDUNDANT)) {
+ (flag == ModInfo::FLAG_CONFLICT_OVERWRITE) ||
+ (flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) ||
+ (flag == ModInfo::FLAG_CONFLICT_REDUNDANT) ||
+ (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE) ||
+ (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN) ||
+ (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED) ||
+ (flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE) ||
+ (flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN)) {
return true;
}
}
@@ -340,18 +366,99 @@ bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const
bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const
{
- if (!m_CurrentFilter.isEmpty() &&
- !info->name().contains(m_CurrentFilter, Qt::CaseInsensitive)) {
- return false;
- }
+ if (!m_CurrentFilter.isEmpty()) {
+ bool display = false;
+ QString filterCopy = QString(m_CurrentFilter);
+ filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
+ QStringList ORList = filterCopy.split(";", QString::SkipEmptyParts);
+
+ bool segmentGood = true;
+
+ //split in ORSegments that internally use AND logic
+ for (auto& ORSegment : ORList) {
+ QStringList ANDKeywords = ORSegment.split(" ", QString::SkipEmptyParts);
+ segmentGood = true;
+ bool foundKeyword = false;
+
+ //check each word in the segment for match, each word needs to be matched but it doesn't matter where.
+ for (auto& currentKeyword : ANDKeywords) {
+ foundKeyword = false;
+
+ //search keyword in name
+ if (m_EnabledColumns[ModList::COL_NAME] &&
+ info->name().contains(currentKeyword, Qt::CaseInsensitive)) {
+ foundKeyword = true;
+ }
+
+ // Search by notes
+ if (!foundKeyword &&
+ m_EnabledColumns[ModList::COL_NOTES] &&
+ info->comments().contains(currentKeyword, Qt::CaseInsensitive)) {
+ foundKeyword = true;
+ }
+
+ // Search by categories
+ if (!foundKeyword &&
+ m_EnabledColumns[ModList::COL_CATEGORY]) {
+ for (auto category : info->categories()) {
+ if (category.contains(currentKeyword, Qt::CaseInsensitive)) {
+ foundKeyword = true;
+ break;
+ }
+ }
+ }
+
+ // Search by Nexus ID
+ if (!foundKeyword &&
+ m_EnabledColumns[ModList::COL_MODID]) {
+ bool ok;
+ int filterID = currentKeyword.toInt(&ok);
+ if (ok) {
+ int modID = info->getNexusID();
+ while (modID > 0) {
+ if (modID == filterID) {
+ foundKeyword = true;
+ break;
+ }
+ modID = (int)(modID / 10);
+ }
+ }
+ }
+
+ if (!foundKeyword) {
+ //currentKeword is missing from everything, AND fails and we need to check next ORsegment
+ segmentGood = false;
+ break;
+ }
+
+ }//for ANDKeywords loop
+
+ if (segmentGood) {
+ //the last AND loop didn't break so the ORSegments is true so mod matches filter
+ display = true;
+ break;
+ }
+
+ }//for ORList loop
+
+ if (!display) {
+ return false;
+ }
+ }//if (!m_CurrentFilter.isEmpty())
if (m_FilterMode == FILTER_AND) {
return filterMatchesModAnd(info, enabled);
- } else {
+ }
+ else {
return filterMatchesModOr(info, enabled);
}
}
+void ModListSortProxy::setColumnVisible(int column, bool visible)
+{
+ m_EnabledColumns[column] = visible;
+}
+
void ModListSortProxy::setFilterMode(ModListSortProxy::FilterMode mode)
{
if (m_FilterMode != mode) {
@@ -367,13 +474,13 @@ bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex &parent) cons
}
if (row >= static_cast<int>(m_Profile->numMods())) {
- qWarning("invalid row idx %d", row);
+ qWarning("invalid row index: %d", row);
return false;
}
QModelIndex idx = sourceModel()->index(row, 0, parent);
if (!idx.isValid()) {
- qDebug("invalid index");
+ qDebug("invalid mod index");
return false;
}
if (sourceModel()->hasChildren(idx)) {
diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h
index b3c01fea..5fe8d9d6 100644
--- a/src/modlistsortproxy.h
+++ b/src/modlistsortproxy.h
@@ -60,6 +60,7 @@ public:
virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
+
/**
* @brief enable all mods visible under the current filter
**/
@@ -94,6 +95,13 @@ public:
return rowCount(parent) > 0;
}
+ /**
+ * @brief sets whether a column is visible
+ * @param column the index of the column
+ * @param visible the visibility of the column
+ */
+ void setColumnVisible(int column, bool visible);
+
public slots:
void updateFilter(const QString &filter);
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp
index e97e9800..9db0d54e 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -1,697 +1,1086 @@
-/*
-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 "nexusinterface.h"
-
-#include "iplugingame.h"
-#include "nxmaccessmanager.h"
-#include "json.h"
-#include "selectiondialog.h"
-#include <utility.h>
-#include <util.h>
-
-#include <QApplication>
-#include <QNetworkCookieJar>
-
-#include <regex>
-
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule)
- : m_Interface(NexusInterface::instance(pluginContainer))
- , m_SubModule(subModule)
-{
-}
-
-void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData)
-{
- m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule));
-}
-
-void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData)
-{
- m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule));
-}
-
-void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData)
-{
- m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule));
-}
-
-void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData)
-{
- m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule));
-}
-
-void NexusBridge::requestToggleEndorsement(QString gameName, int modID, bool endorse, QVariant userData)
-{
- m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, endorse, this, userData, m_SubModule));
-}
-
-void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator iter = m_RequestIDs.find(requestID);
- if (iter != m_RequestIDs.end()) {
- m_RequestIDs.erase(iter);
-
- emit descriptionAvailable(gameName, modID, userData, resultData);
- }
-}
-
-void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator iter = m_RequestIDs.find(requestID);
- if (iter != m_RequestIDs.end()) {
- m_RequestIDs.erase(iter);
-
- QList<ModRepositoryFileInfo> fileInfoList;
-
- QVariantList resultList = resultData.toList();
-
- for (const QVariant &file : resultList) {
- ModRepositoryFileInfo temp;
- QVariantMap fileInfo = file.toMap();
- temp.uri = fileInfo["uri"].toString();
- temp.name = fileInfo["name"].toString();
- temp.description = fileInfo["description"].toString();
- temp.version = VersionInfo(fileInfo["version"].toString());
- temp.categoryID = fileInfo["category_id"].toInt();
- temp.fileID = fileInfo["id"].toInt();
- temp.fileSize = fileInfo["size"].toInt();
- fileInfoList.append(temp);
- }
-
- emit filesAvailable(gameName, modID, userData, fileInfoList);
- }
-}
-
-void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator iter = m_RequestIDs.find(requestID);
- if (iter != m_RequestIDs.end()) {
- m_RequestIDs.erase(iter);
- emit fileInfoAvailable(gameName, modID, fileID, userData, resultData);
- }
-}
-
-void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator iter = m_RequestIDs.find(requestID);
- if (iter != m_RequestIDs.end()) {
- m_RequestIDs.erase(iter);
- emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData);
- }
-}
-
-void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
-{
- std::set<int>::iterator iter = m_RequestIDs.find(requestID);
- if (iter != m_RequestIDs.end()) {
- m_RequestIDs.erase(iter);
- emit endorsementToggled(gameName, modID, userData, resultData);
- }
-}
-
-void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorMessage)
-{
- std::set<int>::iterator iter = m_RequestIDs.find(requestID);
- if (iter != m_RequestIDs.end()) {
- m_RequestIDs.erase(iter);
- emit requestFailed(gameName, modID, fileID, userData, errorMessage);
- }
-}
-
-
-QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0);
-
-
-NexusInterface::NexusInterface(PluginContainer *pluginContainer)
- : m_NMMVersion(), m_PluginContainer(pluginContainer)
-{
- m_MOVersion = createVersionInfo();
-
- m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString());
- m_DiskCache = new QNetworkDiskCache(this);
- connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString)));
-}
-
-NXMAccessManager *NexusInterface::getAccessManager()
-{
- return m_AccessManager;
-}
-
-NexusInterface::~NexusInterface()
-{
- cleanup();
-}
-
-NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer)
-{
- static NexusInterface s_Instance(pluginContainer);
- return &s_Instance;
-}
-
-void NexusInterface::setCacheDirectory(const QString &directory)
-{
- m_DiskCache->setCacheDirectory(directory);
- m_AccessManager->setCache(m_DiskCache);
-}
-
-void NexusInterface::setNMMVersion(const QString &nmmVersion)
-{
- m_NMMVersion = nmmVersion;
- m_AccessManager->setNMMVersion(nmmVersion);
-}
-
-void NexusInterface::loginCompleted()
-{
- nextRequest();
-}
-
-
-void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query)
-{
- //Look for something along the lines of modulename-Vn-m + any old rubbish.
- static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp");
- static std::regex simpleexp("^([a-zA-Z0-9_]+)");
-
- QByteArray fileNameUTF8 = fileName.toUtf8();
- std::cmatch result;
- if (std::regex_search(fileNameUTF8.constData(), result, exp)) {
- modName = QString::fromUtf8(result[1].str().c_str());
- modName = modName.replace('_', ' ').trimmed();
-
- std::string candidate = result[3].str();
- std::string candidate2 = result[2].str();
- if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) {
- // well, that second match might be an id too...
- size_t offset = strspn(candidate2.c_str(), "-_ ");
- if (offset < candidate2.length() && query) {
- SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName));
- QString r2Highlight(fileName);
- r2Highlight.insert(result.position(2) + result.length(2), "* ")
- .insert(result.position(2) + static_cast<int>(offset), " *");
- QString r3Highlight(fileName);
- r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *");
-
- selection.addChoice(candidate.c_str(), r3Highlight, static_cast<int>(strtol(candidate.c_str(), nullptr, 10)));
- selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast<int>(abs(strtol(candidate2.c_str() + offset, nullptr, 10))));
- if (selection.exec() == QDialog::Accepted) {
- modID = selection.getChoiceData().toInt();
- } else {
- modID = -1;
- }
- } else {
- modID = -1;
- }
- } else {
- modID = strtol(candidate.c_str(), nullptr, 10);
- }
- qDebug("mod id guessed: %s -> %d", qPrintable(fileName), modID);
- } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) {
- qDebug("simple expression matched, using name only");
- modName = QString::fromUtf8(result[1].str().c_str());
- modName = modName.replace('_', ' ').trimmed();
-
- modID = -1;
- } else {
- qDebug("no expression matched!");
- modName.clear();
- modID = -1;
- }
-}
-
-bool NexusInterface::isURLGameRelated(const QUrl &url) const
-{
- QString const name(url.toString());
- return name.startsWith(getGameURL("") + "/") ||
- name.startsWith(getOldModsURL("") + "/");
-}
-
-QString NexusInterface::getGameURL(QString gameName) const
-{
- IPluginGame *game = getGame(gameName);
- return "https://www.nexusmods.com/" + game->gameNexusName().toLower();
-}
-
-QString NexusInterface::getOldModsURL(QString gameName) const
-{
- IPluginGame *game = getGame(gameName);
- return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods";
-}
-
-
-QString NexusInterface::getModURL(int modID, QString gameName = "") const
-{
- return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID);
-}
-
-std::vector<std::pair<QString, QString>> NexusInterface::getGameChoices(const MOBase::IPluginGame *game)
-{
- std::vector<std::pair<QString, QString>> choices;
- choices.push_back(std::pair<QString, QString>(game->gameShortName(), game->gameName()));
- for (QString gameName : game->validShortNames()) {
- for (auto gamePlugin : m_PluginContainer->plugins<IPluginGame>()) {
- if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
- choices.push_back(std::pair<QString, QString>(gamePlugin->gameShortName(), gamePlugin->gameName()));
- break;
- }
- }
- }
- return choices;
-}
-
-bool NexusInterface::isModURL(int modID, const QString &url) const
-{
- if (QUrl(url) == QUrl(getModURL(modID))) {
- return true;
- }
- //Try the alternate (old style) mod name
- QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID);
- return QUrl(alt) == QUrl(url);
-}
-
-void NexusInterface::setPluginContainer(PluginContainer *pluginContainer)
-{
- m_PluginContainer = pluginContainer;
-}
-
-int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData,
- const QString &subModule, MOBase::IPluginGame const *game)
-{
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
-
- connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
-
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection);
-
- nextRequest();
- return requestInfo.m_ID;
-}
-
-
-int NexusInterface::requestUpdates(const std::vector<int> &modIDs, QObject *receiver, QVariant userData,
- QString gameName, const QString &subModule)
-{
- IPluginGame *game = getGame(gameName);
- NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
-
- connect(this, SIGNAL(nxmUpdatesAvailable(std::vector<int>, QVariant, QVariant, int)),
- receiver, SLOT(nxmUpdatesAvailable(std::vector<int>, QVariant, QVariant, int)), Qt::UniqueConnection);
-
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection);
-
- nextRequest();
- return requestInfo.m_ID;
-}
-
-
-void NexusInterface::fakeFiles()
-{
- static int id = 42;
-
- QVariantList result;
- QVariantMap fileMap;
- fileMap["uri"] = "fakeURI";
- fileMap["name"] = "fakeName";
- fileMap["description"] = "fakeDescription";
- fileMap["version"] = "1.0.0";
- fileMap["category_id"] = "1";
- fileMap["id"] = "1";
- fileMap["size"] = "512";
- result.append(fileMap);
-
- emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++);
-}
-
-
-int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData,
- const QString &subModule, MOBase::IPluginGame const *game)
-{
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
- connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
-
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection);
-
- nextRequest();
- return requestInfo.m_ID;
-}
-
-
-int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule)
-{
- IPluginGame *gamePlugin = getGame(gameName);
- NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin);
- m_RequestQueue.enqueue(requestInfo);
-
- connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection);
-
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection);
-
- nextRequest();
- return requestInfo.m_ID;
-}
-
-
-int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData,
- const QString &subModule, MOBase::IPluginGame const *game)
-{
- NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
-
- connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)),
- receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection);
-
- connect(this, SIGNAL(nxmRequestFailed(QString,int,int,QVariant,int,QString)),
- receiver, SLOT(nxmRequestFailed(QString,int,int,QVariant,int,QString)), Qt::UniqueConnection);
-
- nextRequest();
- return requestInfo.m_ID;
-}
-
-
-int NexusInterface::requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData,
- const QString &subModule, MOBase::IPluginGame const *game)
-{
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game);
- requestInfo.m_Endorse = endorse;
- m_RequestQueue.enqueue(requestInfo);
-
- connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
-
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection);
-
- nextRequest();
- return requestInfo.m_ID;
-}
-
-bool NexusInterface::requiresLogin(const NXMRequestInfo &info)
-{
- return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT)
- || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL);
-}
-
-IPluginGame* NexusInterface::getGame(QString gameName) const
-{
- auto gamePlugins = m_PluginContainer->plugins<IPluginGame>();
- IPluginGame *gamePlugin = qApp->property("managed_game").value<IPluginGame*>();
- for (auto plugin : gamePlugins) {
- if (plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
- gamePlugin = plugin;
- break;
- }
- }
- return gamePlugin;
-}
-
-void NexusInterface::cleanup()
-{
-// delete m_AccessManager;
-// delete m_DiskCache;
- m_AccessManager = nullptr;
- m_DiskCache = nullptr;
-}
-
-void NexusInterface::clearCache()
-{
- m_DiskCache->clear();
- m_AccessManager->clearCookies();
-}
-
-void NexusInterface::nextRequest()
-{
- if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS)
- || m_RequestQueue.isEmpty()) {
- return;
- }
-
- if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->loggedIn()) {
- if (!getAccessManager()->loginAttempted()) {
- emit needLogin();
- return;
- } else if (getAccessManager()->loginWaiting()) {
- return;
- }
- }
-
- NXMRequestInfo info = m_RequestQueue.dequeue();
- info.m_Timeout = new QTimer(this);
- info.m_Timeout->setInterval(60000);
-
- QString url;
- if (!info.m_Reroute) {
- bool hasParams = false;
- switch (info.m_Type) {
- case NXMRequestInfo::TYPE_DESCRIPTION: {
- url = QString("%1/Mods/%2/").arg(info.m_URL).arg(info.m_ModID);
- } break;
- case NXMRequestInfo::TYPE_FILES: {
- url = QString("%1/Files/indexfrommod/%2/").arg(info.m_URL).arg(info.m_ModID);
- } break;
- case NXMRequestInfo::TYPE_FILEINFO: {
- url = QString("%1/Files/%2/").arg(info.m_URL).arg(info.m_FileID);
- } break;
- case NXMRequestInfo::TYPE_DOWNLOADURL: {
- url = QString("%1/Files/download/%2").arg(info.m_URL).arg(info.m_FileID);
- } break;
- case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: {
- url = QString("%1/Mods/toggleendorsement/%2?lvote=%3").arg(info.m_URL).arg(info.m_ModID).arg(!info.m_Endorse);
- hasParams = true;
- } break;
- case NXMRequestInfo::TYPE_GETUPDATES: {
- QString modIDList = VectorJoin<int>(info.m_ModIDList, ",");
- modIDList = "[" + modIDList + "]";
- url = QString("%1/Mods/GetUpdates?ModList=%2").arg(info.m_URL).arg(modIDList);
- hasParams = true;
- } break;
- }
- url.append(QString("%1game_id=%2").arg(hasParams ? '&' : '?').arg(info.m_NexusGameID));
- } else {
- url = info.m_URL;
- }
- QNetworkRequest request(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml");
- request.setRawHeader("User-Agent", m_AccessManager->userAgent(info.m_SubModule).toUtf8());
-
- info.m_Reply = m_AccessManager->get(request);
-
- connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished()));
- connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
- connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout()));
- info.m_Timeout->start();
- m_ActiveRequest.push_back(info);
-}
-
-
-void NexusInterface::downloadRequestedNXM(const QString &url)
-{
- emit requestNXMDownload(url);
-}
-
-void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
-{
- QNetworkReply *reply = iter->m_Reply;
-
- if (reply->error() != QNetworkReply::NoError) {
- qWarning("request failed: %s", reply->errorString().toUtf8().constData());
- emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->errorString());
- } else {
- int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- if (statusCode == 301) {
- // redirect request, return request to queue
- iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
- iter->m_Reroute = true;
- m_RequestQueue.enqueue(*iter);
- //nextRequest();
- return;
- }
- QByteArray data = reply->readAll();
- if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) {
- QString nexusError(reply->rawHeader("NexusErrorInfo"));
- if (nexusError.length() == 0) {
- nexusError = tr("empty response");
- }
- qDebug("nexus error: %s", qPrintable(nexusError));
- emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, nexusError);
- } else {
- bool ok;
- QVariant result = QtJson::parse(data, ok);
- if (result.isValid() && ok) {
- switch (iter->m_Type) {
- case NXMRequestInfo::TYPE_DESCRIPTION: {
- emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
- } break;
- case NXMRequestInfo::TYPE_FILES: {
- emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
- } break;
- case NXMRequestInfo::TYPE_FILEINFO: {
- emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID);
- } break;
- case NXMRequestInfo::TYPE_DOWNLOADURL: {
- emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID);
- } break;
- case NXMRequestInfo::TYPE_GETUPDATES: {
- emit nxmUpdatesAvailable(iter->m_ModIDList, iter->m_UserData, result, iter->m_ID);
- } break;
- case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: {
- emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
- } break;
- }
- } else {
- emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, tr("invalid response"));
- }
- }
- }
-}
-
-
-void NexusInterface::requestFinished()
-{
- QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
- for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
- if (iter->m_Reply == reply) {
- iter->m_Timeout->stop();
- iter->m_Timeout->deleteLater();
- requestFinished(iter);
- iter->m_Reply->deleteLater();
- m_ActiveRequest.erase(iter);
- nextRequest();
- return;
- }
- }
-}
-
-
-void NexusInterface::requestError(QNetworkReply::NetworkError)
-{
- QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
- if (reply == nullptr) {
- qWarning("invalid sender type");
- return;
- }
-
- qCritical("request (%s) error: %s (%d)",
- qPrintable(reply->url().toString()),
- qPrintable(reply->errorString()),
- reply->error());
-}
-
-
-void NexusInterface::requestTimeout()
-{
- QTimer *timer = qobject_cast<QTimer*>(sender());
- if (timer == nullptr) {
- qWarning("invalid sender type");
- return;
- }
- for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
- if (iter->m_Timeout == timer) {
- // this abort causes a "request failed" which cleans up the rest
- iter->m_Reply->abort();
- return;
- }
- }
-}
-
-namespace {
- QString get_management_url(MOBase::IPluginGame const *game)
- {
- return "https://legacy-api.nexusmods.com/" + game->gameNexusName().toLower();
- }
-}
-
-NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
- , NexusInterface::NXMRequestInfo::Type type
- , QVariant userData
- , const QString &subModule
- , MOBase::IPluginGame const *game
- )
- : m_ModID(modID)
- , m_FileID(0)
- , m_Reply(nullptr)
- , m_Type(type)
- , m_UserData(userData)
- , m_Timeout(nullptr)
- , m_Reroute(false)
- , m_ID(s_NextID.fetchAndAddAcquire(1))
- , m_URL(get_management_url(game))
- , m_SubModule(subModule)
- , m_NexusGameID(game->nexusGameID())
- , m_GameName(game->gameShortName())
- , m_Endorse(false)
-{}
-
-NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector<int> modIDList
- , NexusInterface::NXMRequestInfo::Type type
- , QVariant userData
- , const QString &subModule
- , MOBase::IPluginGame const *game
- )
- : m_ModID(-1)
- , m_ModIDList(modIDList)
- , m_FileID(0)
- , m_Reply(nullptr)
- , m_Type(type)
- , m_UserData(userData)
- , m_Timeout(nullptr)
- , m_Reroute(false)
- , m_ID(s_NextID.fetchAndAddAcquire(1))
- , m_URL(get_management_url(game))
- , m_SubModule(subModule)
- , m_NexusGameID(game->nexusGameID())
- , m_GameName(game->gameShortName())
- , m_Endorse(false)
-{}
-
-NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
- , int fileID
- , NexusInterface::NXMRequestInfo::Type type
- , QVariant userData
- , const QString &subModule
- , MOBase::IPluginGame const *game
- )
- : m_ModID(modID)
- , m_FileID(fileID)
- , m_Reply(nullptr)
- , m_Type(type)
- , m_UserData(userData)
- , m_Timeout(nullptr)
- , m_Reroute(false)
- , m_ID(s_NextID.fetchAndAddAcquire(1))
- , m_URL(get_management_url(game))
- , m_SubModule(subModule)
- , m_NexusGameID(game->nexusGameID())
- , m_GameName(game->gameShortName())
- , m_Endorse(false)
-{}
+/*
+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 "nexusinterface.h"
+
+#include "iplugingame.h"
+#include "nxmaccessmanager.h"
+#include "selectiondialog.h"
+#include "bbcode.h"
+#include <utility.h>
+#include <util.h>
+
+#include <QApplication>
+#include <QNetworkCookieJar>
+#include <QJsonDocument>
+#include <QRegularExpression>
+
+#include <regex>
+
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule)
+ : m_Interface(NexusInterface::instance(pluginContainer))
+ , m_SubModule(subModule)
+{
+}
+
+void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData)
+{
+ m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule));
+}
+
+void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData)
+{
+ m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule));
+}
+
+void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData)
+{
+ m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule));
+}
+
+void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData)
+{
+ m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule));
+}
+
+void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData)
+{
+ m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule));
+}
+
+void NexusBridge::requestToggleTracking(QString gameName, int modID, bool track, QVariant userData)
+{
+ m_RequestIDs.insert(m_Interface->requestToggleTracking(gameName, modID, track, this, userData, m_SubModule));
+}
+
+void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+
+ emit descriptionAvailable(gameName, modID, userData, resultData);
+ }
+}
+
+void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+
+ QList<ModRepositoryFileInfo> fileInfoList;
+
+ QVariantMap resultInfo = resultData.toMap();
+ QList resultList = resultInfo["files"].toList();
+
+ for (const QVariant &file : resultList) {
+ ModRepositoryFileInfo temp;
+ QVariantMap fileInfo = file.toMap();
+ temp.uri = fileInfo["file_name"].toString();
+ temp.name = fileInfo["name"].toString();
+ temp.description = BBCode::convertToHTML(fileInfo["description"].toString());
+ temp.version = VersionInfo(fileInfo["version"].toString());
+ temp.categoryID = fileInfo["category_id"].toInt();
+ temp.fileID = fileInfo["file_id"].toInt();
+ temp.fileSize = fileInfo["size"].toInt();
+ fileInfoList.append(temp);
+ }
+
+ emit filesAvailable(gameName, modID, userData, fileInfoList);
+ }
+}
+
+void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit fileInfoAvailable(gameName, modID, fileID, userData, resultData);
+ }
+}
+
+void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData);
+ }
+}
+
+void NexusBridge::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit endorsementsAvailable(userData, resultData);
+ }
+}
+
+void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit endorsementToggled(gameName, modID, userData, resultData);
+ }
+}
+
+void NexusBridge::nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit trackedModsAvailable(userData, resultData);
+ }
+}
+
+void NexusBridge::nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit trackingToggled(gameName, modID, userData, tracked);
+ }
+}
+
+void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage)
+{
+ std::set<int>::iterator iter = m_RequestIDs.find(requestID);
+ if (iter != m_RequestIDs.end()) {
+ m_RequestIDs.erase(iter);
+ emit requestFailed(gameName, modID, fileID, userData, error, errorMessage);
+ }
+}
+
+
+QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0);
+
+
+NexusInterface::NexusInterface(PluginContainer *pluginContainer)
+ : m_PluginContainer(pluginContainer), m_RemainingDailyRequests(2500), m_RemainingHourlyRequests(100), m_MaxDailyRequests(2500), m_MaxHourlyRequests(100)
+{
+ m_MOVersion = createVersionInfo();
+
+ m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3));
+ m_DiskCache = new QNetworkDiskCache(this);
+ connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString)));
+}
+
+NXMAccessManager *NexusInterface::getAccessManager()
+{
+ return m_AccessManager;
+}
+
+NexusInterface::~NexusInterface()
+{
+ cleanup();
+}
+
+NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer)
+{
+ static NexusInterface s_Instance(pluginContainer);
+ return &s_Instance;
+}
+
+void NexusInterface::setCacheDirectory(const QString &directory)
+{
+ m_DiskCache->setCacheDirectory(directory);
+ m_AccessManager->setCache(m_DiskCache);
+}
+
+void NexusInterface::loginCompleted()
+{
+ nextRequest();
+}
+
+void NexusInterface::setRateMax(const QString&, bool, std::tuple<int, int, int, int> limits)
+{
+ 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);
+}
+
+void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query)
+{
+ //Look for something along the lines of modulename-Vn-m + any old rubbish.
+ static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp");
+ static std::regex simpleexp("^([a-zA-Z0-9_]+)");
+
+ QByteArray fileNameUTF8 = fileName.toUtf8();
+ std::cmatch result;
+ if (std::regex_search(fileNameUTF8.constData(), result, exp)) {
+ modName = QString::fromUtf8(result[1].str().c_str());
+ modName = modName.replace('_', ' ').trimmed();
+
+ std::string candidate = result[3].str();
+ std::string candidate2 = result[2].str();
+ if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) {
+ // well, that second match might be an id too...
+ size_t offset = strspn(candidate2.c_str(), "-_ ");
+ if (offset < candidate2.length() && query) {
+ SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName));
+ QString r2Highlight(fileName);
+ r2Highlight.insert(result.position(2) + result.length(2), "* ")
+ .insert(result.position(2) + static_cast<int>(offset), " *");
+ QString r3Highlight(fileName);
+ r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *");
+
+ selection.addChoice(candidate.c_str(), r3Highlight, static_cast<int>(strtol(candidate.c_str(), nullptr, 10)));
+ selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast<int>(abs(strtol(candidate2.c_str() + offset, nullptr, 10))));
+ if (selection.exec() == QDialog::Accepted) {
+ modID = selection.getChoiceData().toInt();
+ } else {
+ modID = -1;
+ }
+ } else {
+ modID = -1;
+ }
+ } else {
+ modID = strtol(candidate.c_str(), nullptr, 10);
+ }
+ qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID);
+ } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) {
+ qDebug("simple expression matched, using name only");
+ modName = QString::fromUtf8(result[1].str().c_str());
+ modName = modName.replace('_', ' ').trimmed();
+
+ modID = -1;
+ } else {
+ qDebug("no expression matched!");
+ modName.clear();
+ modID = -1;
+ }
+}
+
+bool NexusInterface::isURLGameRelated(const QUrl &url) const
+{
+ QString const name(url.toString());
+ return name.startsWith(getGameURL("") + "/") ||
+ name.startsWith(getOldModsURL("") + "/");
+}
+
+QString NexusInterface::getGameURL(QString gameName) const
+{
+ IPluginGame *game = getGame(gameName);
+ if (game != nullptr) {
+ return "https://www.nexusmods.com/" + game->gameNexusName().toLower();
+ } else {
+ qCritical("getGameURL can't find plugin for %s", qUtf8Printable(gameName));
+ return "";
+ }
+}
+
+QString NexusInterface::getOldModsURL(QString gameName) const
+{
+ IPluginGame *game = getGame(gameName);
+ if (game != nullptr) {
+ return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods";
+ } else {
+ qCritical("getOldModsURL can't find plugin for %s", qUtf8Printable(gameName));
+ return "";
+ }
+}
+
+
+QString NexusInterface::getModURL(int modID, QString gameName = "") const
+{
+ return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID);
+}
+
+std::vector<std::pair<QString, QString>> NexusInterface::getGameChoices(const MOBase::IPluginGame *game)
+{
+ std::vector<std::pair<QString, QString>> choices;
+ choices.push_back(std::pair<QString, QString>(game->gameShortName(), game->gameName()));
+ for (QString gameName : game->validShortNames()) {
+ for (auto gamePlugin : m_PluginContainer->plugins<IPluginGame>()) {
+ if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
+ choices.push_back(std::pair<QString, QString>(gamePlugin->gameShortName(), gamePlugin->gameName()));
+ break;
+ }
+ }
+ }
+ return choices;
+}
+
+bool NexusInterface::isModURL(int modID, const QString &url) const
+{
+ if (QUrl(url) == QUrl(getModURL(modID))) {
+ return true;
+ }
+ //Try the alternate (old style) mod name
+ QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID);
+ return QUrl(alt) == QUrl(url);
+}
+
+void NexusInterface::setPluginContainer(PluginContainer *pluginContainer)
+{
+ m_PluginContainer = pluginContainer;
+}
+
+int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData,
+ const QString &subModule, MOBase::IPluginGame const *game)
+{
+ NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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::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);
+
+ connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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;
+ }
+ 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;
+}
+
+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);
+
+ connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)),
+ receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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;
+ }
+ 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;
+}
+
+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;
+ }
+
+ NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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;
+ }
+ 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;
+}
+
+
+void NexusInterface::fakeFiles()
+{
+ static int id = 42;
+
+ QVariantList result;
+ QVariantMap fileMap;
+ fileMap["uri"] = "fakeURI";
+ fileMap["name"] = "fakeName";
+ fileMap["description"] = "fakeDescription";
+ fileMap["version"] = "1.0.0";
+ fileMap["category_id"] = "1";
+ fileMap["id"] = "1";
+ fileMap["size"] = "512";
+ result.append(fileMap);
+
+ emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++);
+}
+
+
+int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData,
+ const QString &subModule, MOBase::IPluginGame const *game)
+{
+ NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
+ connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule)
+{
+ IPluginGame *gamePlugin = getGame(gameName);
+ if (gamePlugin == nullptr) {
+ qCritical("requestFileInfo can't find plugin for %s", qUtf8Printable(gameName));
+ return -1;
+ }
+
+ NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin);
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData,
+ const QString &subModule, MOBase::IPluginGame const *game)
+{
+ NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)),
+ receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection);
+
+ 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::requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule)
+{
+ NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_ENDORSEMENTS, userData, subModule);
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmEndorsementsAvailable(QVariant, QVariant, int)),
+ receiver, SLOT(nxmEndorsementsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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::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);
+
+ connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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;
+ }
+ 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;
+}
+
+int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule)
+{
+ NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_TRACKEDMODS, userData, subModule);
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmTrackedModsAvailable(QVariant, QVariant, int)),
+ receiver, SLOT(nxmTrackedModsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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::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);
+
+ connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)),
+ receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection);
+
+ 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;
+ }
+ 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;
+}
+
+int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData,
+ const QString &subModule, MOBase::IPluginGame const *game)
+{
+ NXMRequestInfo requestInfo(hash, NXMRequestInfo::TYPE_FILEINFO_MD5, userData, subModule, game);
+ requestInfo.m_Hash = hash;
+ requestInfo.m_AllowedErrors[QNetworkReply::NetworkError::ContentNotFoundError].append(404);
+ requestInfo.m_IgnoreGenericErrorHandler = true;
+ m_RequestQueue.enqueue(requestInfo);
+
+ connect(this, SIGNAL(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)),
+ receiver, SLOT(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), Qt::UniqueConnection);
+
+ 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;
+}
+
+IPluginGame* NexusInterface::getGame(QString gameName) const
+{
+ auto gamePlugins = m_PluginContainer->plugins<IPluginGame>();
+ IPluginGame *gamePlugin = qApp->property("managed_game").value<IPluginGame*>();
+ for (auto plugin : gamePlugins) {
+ if (plugin != nullptr && plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
+ gamePlugin = plugin;
+ break;
+ }
+ }
+ return gamePlugin;
+}
+
+void NexusInterface::cleanup()
+{
+ m_AccessManager = nullptr;
+ m_DiskCache = nullptr;
+}
+
+void NexusInterface::clearCache()
+{
+ m_DiskCache->clear();
+ m_AccessManager->clearCookies();
+}
+
+void NexusInterface::nextRequest()
+{
+ if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS)
+ || m_RequestQueue.isEmpty()) {
+ return;
+ }
+
+ if (!getAccessManager()->validated()) {
+ if (!getAccessManager()->validateAttempted()) {
+ emit needLogin();
+ return;
+ } else if (getAccessManager()->validateWaiting()) {
+ return;
+ } else {
+ qCritical("You must authorize MO2 in Settings -> Nexus to use the Nexus API.");
+ }
+ }
+
+ if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) <= 0) {
+ 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. "
+ "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;
+ }
+
+ NXMRequestInfo info = m_RequestQueue.dequeue();
+ info.m_Timeout = new QTimer(this);
+ info.m_Timeout->setInterval(60000);
+
+ QJsonObject postObject;
+ QJsonDocument postData(postObject);
+ bool requestIsDelete = false;
+
+ QString url;
+ if (!info.m_Reroute) {
+ bool hasParams = false;
+ switch (info.m_Type) {
+ case NXMRequestInfo::TYPE_DESCRIPTION:
+ case NXMRequestInfo::TYPE_MODINFO: {
+ url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID);
+ } break;
+ case NXMRequestInfo::TYPE_CHECKUPDATES: {
+ QString period;
+ switch (info.m_UpdatePeriod) {
+ case UpdatePeriod::DAY:
+ period = "1d";
+ break;
+ case UpdatePeriod::WEEK:
+ period = "1w";
+ break;
+ case UpdatePeriod::MONTH:
+ period = "1m";
+ break;
+ }
+ url = QString("%1/games/%2/mods/updated?period=%3").arg(info.m_URL).arg(info.m_GameName).arg(period);
+ } break;
+ case NXMRequestInfo::TYPE_FILES:
+ case NXMRequestInfo::TYPE_GETUPDATES: {
+ url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID);
+ } break;
+ case NXMRequestInfo::TYPE_FILEINFO: {
+ url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID);
+ } break;
+ case NXMRequestInfo::TYPE_DOWNLOADURL: {
+ ModRepositoryFileInfo *fileInfo = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(info.m_UserData));
+ if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires)
+ 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);
+ } break;
+ case NXMRequestInfo::TYPE_ENDORSEMENTS: {
+ url = QString("%1/user/endorsements").arg(info.m_URL);
+ } break;
+ case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: {
+ QString endorse = info.m_Endorse ? "endorse" : "abstain";
+ url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse);
+ postObject.insert("Version", info.m_ModVersion);
+ postData.setObject(postObject);
+ } break;
+ case NXMRequestInfo::TYPE_TOGGLETRACKING: {
+ url = QStringLiteral("%1/user/tracked_mods?domain_name=%2").arg(info.m_URL).arg(info.m_GameName);
+ postObject.insert("mod_id", info.m_ModID);
+ postData.setObject(postObject);
+ requestIsDelete = !info.m_Track;
+ } break;
+ case NXMRequestInfo::TYPE_TRACKEDMODS: {
+ url = QStringLiteral("%1/user/tracked_mods").arg(info.m_URL);
+ } break;
+ case NXMRequestInfo::TYPE_FILEINFO_MD5: {
+ url = QStringLiteral("%1/games/%2/mods/md5_search/%3").arg(info.m_URL).arg(info.m_GameName).arg(QString(info.m_Hash.toHex()));
+ }
+ }
+ } else {
+ url = info.m_URL;
+ }
+ QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
+ request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
+ request.setRawHeader("APIKEY", m_AccessManager->apiKey().toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule));
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Protocol-Version", "1.0.0");
+ request.setRawHeader("Application-Name", "MO2");
+ request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8());
+
+ if (postData.object().isEmpty()) {
+ if (!requestIsDelete) {
+ info.m_Reply = m_AccessManager->get(request);
+ } else {
+ info.m_Reply = m_AccessManager->deleteResource(request);
+ }
+ } else if (!requestIsDelete) {
+ info.m_Reply = m_AccessManager->post(request, postData.toJson());
+ } else {
+ // Qt doesn't support DELETE with a payload as that's technically against the HTTP standard...
+ info.m_Reply = m_AccessManager->sendCustomRequest(request, "DELETE", postData.toJson());
+ }
+
+ connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished()));
+ if (!info.m_IgnoreGenericErrorHandler)
+ connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
+ connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout()));
+ info.m_Timeout->start();
+ m_ActiveRequest.push_back(info);
+}
+
+
+void NexusInterface::downloadRequestedNXM(const QString &url)
+{
+ emit requestNXMDownload(url);
+}
+
+void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
+{
+ QNetworkReply *reply = iter->m_Reply;
+
+ auto error = reply->error();
+ if (error != QNetworkReply::NoError) {
+ int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ 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();
+
+ if (m_RemainingDailyRequests || m_RemainingHourlyRequests)
+ qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team.");
+ 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
+ )));
+
+ qWarning("Error: %s", reply->errorString().toUtf8().constData());
+ } else {
+ qWarning("request failed: %s", reply->errorString().toUtf8().constData());
+ }
+ emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString());
+ } else {
+ int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (statusCode == 301) {
+ // redirect request, return request to queue
+ iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
+ iter->m_Reroute = true;
+ m_RequestQueue.enqueue(*iter);
+ //nextRequest();
+ return;
+ }
+ QByteArray data = reply->readAll();
+ if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) {
+ QString nexusError(reply->rawHeader("NexusErrorInfo"));
+ if (nexusError.length() == 0) {
+ nexusError = tr("empty response");
+ }
+ qDebug("nexus error: %s", qUtf8Printable(nexusError));
+ emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), nexusError);
+ } else {
+ QJsonDocument responseDoc = QJsonDocument::fromJson(data);
+ if (!responseDoc.isNull()) {
+ QVariant result = responseDoc.toVariant();
+ switch (iter->m_Type) {
+ case NXMRequestInfo::TYPE_DESCRIPTION: {
+ emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_MODINFO: {
+ emit nxmModInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_CHECKUPDATES: {
+ emit nxmUpdateInfoAvailable(iter->m_GameName, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_FILES: {
+ emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_GETUPDATES: {
+ emit nxmUpdatesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_FILEINFO: {
+ emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_DOWNLOADURL: {
+ emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_ENDORSEMENTS: {
+ emit nxmEndorsementsAvailable(iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: {
+ emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_TOGGLETRACKING: {
+ auto results = result.toMap();
+ auto message = results["message"].toString();
+ if (message.contains(QRegularExpression("User [0-9]+ is already Tracking Mod: [0-9]+")) ||
+ message.contains(QRegularExpression("User [0-9]+ is now Tracking Mod: [0-9]+"))) {
+ emit nxmTrackingToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, true, iter->m_ID);
+ } else if (message.contains(QRegularExpression("User [0-9]+ is no longer tracking [0-9]+")) ||
+ message.contains(QRegularExpression("Users is not tracking mod. Unable to untrack."))) {
+ emit nxmTrackingToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, false, iter->m_ID);
+ }
+ } break;
+ case NXMRequestInfo::TYPE_TRACKEDMODS: {
+ emit nxmTrackedModsAvailable(iter->m_UserData, result, iter->m_ID);
+ } break;
+ case NXMRequestInfo::TYPE_FILEINFO_MD5: {
+ emit nxmFileInfoFromMd5Available(iter->m_GameName, iter->m_UserData, result, iter->m_ID);
+ } 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
+ )));
+ } else {
+ emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response"));
+ }
+ }
+ }
+}
+
+
+void NexusInterface::requestFinished()
+{
+ QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
+ for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
+ if (iter->m_Reply == reply) {
+ iter->m_Timeout->stop();
+ iter->m_Timeout->deleteLater();
+ requestFinished(iter);
+ iter->m_Reply->deleteLater();
+ m_ActiveRequest.erase(iter);
+ nextRequest();
+ return;
+ }
+ }
+}
+
+
+void NexusInterface::requestError(QNetworkReply::NetworkError)
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ if (reply == nullptr) {
+ qWarning("invalid sender type");
+ return;
+ }
+
+ qCritical("request (%s) error: %s (%d)",
+ qUtf8Printable(reply->url().toString()),
+ qUtf8Printable(reply->errorString()),
+ reply->error());
+}
+
+
+void NexusInterface::requestTimeout()
+{
+ QTimer *timer = qobject_cast<QTimer*>(sender());
+ if (timer == nullptr) {
+ qWarning("invalid sender type");
+ return;
+ }
+ for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
+ if (iter->m_Timeout == timer) {
+ // this abort causes a "request failed" which cleans up the rest
+ iter->m_Reply->abort();
+ return;
+ }
+ }
+}
+
+namespace {
+ QString get_management_url()
+ {
+ return "https://api.nexusmods.com/v1";
+ }
+}
+
+NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
+ , NexusInterface::NXMRequestInfo::Type type
+ , QVariant userData
+ , const QString &subModule
+ , MOBase::IPluginGame const *game
+ )
+ : m_ModID(modID)
+ , m_ModVersion("0")
+ , m_FileID(0)
+ , m_Reply(nullptr)
+ , m_Type(type)
+ , m_UpdatePeriod(UpdatePeriod::NONE)
+ , m_UserData(userData)
+ , m_Timeout(nullptr)
+ , m_Reroute(false)
+ , m_ID(s_NextID.fetchAndAddAcquire(1))
+ , m_URL(get_management_url())
+ , m_SubModule(subModule)
+ , m_NexusGameID(game->nexusGameID())
+ , m_GameName(game->gameNexusName())
+ , m_Endorse(false)
+ , m_Track(false)
+ , m_Hash(QByteArray())
+{
+}
+
+NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
+ , QString modVersion
+ , NexusInterface::NXMRequestInfo::Type type
+ , QVariant userData
+ , const QString &subModule
+ , MOBase::IPluginGame const *game
+)
+ : m_ModID(modID)
+ , m_ModVersion(modVersion)
+ , m_FileID(0)
+ , m_Reply(nullptr)
+ , m_Type(type)
+ , m_UpdatePeriod(UpdatePeriod::NONE)
+ , m_UserData(userData)
+ , m_Timeout(nullptr)
+ , m_Reroute(false)
+ , m_ID(s_NextID.fetchAndAddAcquire(1))
+ , m_URL(get_management_url())
+ , m_SubModule(subModule)
+ , m_NexusGameID(game->nexusGameID())
+ , m_GameName(game->gameNexusName())
+ , m_Endorse(false)
+ , m_Track(false)
+ , m_Hash(QByteArray())
+{}
+
+NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
+ , int fileID
+ , NexusInterface::NXMRequestInfo::Type type
+ , QVariant userData
+ , const QString &subModule
+ , MOBase::IPluginGame const *game
+)
+ : m_ModID(modID)
+ , m_ModVersion("0")
+ , m_FileID(fileID)
+ , m_Reply(nullptr)
+ , m_Type(type)
+ , m_UpdatePeriod(UpdatePeriod::NONE)
+ , m_UserData(userData)
+ , m_Timeout(nullptr)
+ , m_Reroute(false)
+ , m_ID(s_NextID.fetchAndAddAcquire(1))
+ , m_URL(get_management_url())
+ , m_SubModule(subModule)
+ , m_NexusGameID(game->nexusGameID())
+ , m_GameName(game->gameNexusName())
+ , m_Endorse(false)
+ , m_Track(false)
+ , m_Hash(QByteArray())
+{}
+
+NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type
+ , QVariant userData
+ , const QString &subModule
+)
+ : m_ModID(0)
+ , m_ModVersion("0")
+ , m_FileID(0)
+ , m_Reply(nullptr)
+ , m_Type(type)
+ , m_UpdatePeriod(UpdatePeriod::NONE)
+ , m_UserData(userData)
+ , m_Timeout(nullptr)
+ , m_Reroute(false)
+ , m_ID(s_NextID.fetchAndAddAcquire(1))
+ , m_URL(get_management_url())
+ , m_SubModule(subModule)
+ , m_NexusGameID(0)
+ , m_GameName("")
+ , m_Endorse(false)
+ , m_Track(false)
+ , m_Hash(QByteArray())
+{}
+
+
+NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period
+ , NexusInterface::NXMRequestInfo::Type type
+ , QVariant userData
+ , const QString &subModule
+ , MOBase::IPluginGame const *game
+)
+ : m_ModID(0)
+ , m_ModVersion("0")
+ , m_FileID(0)
+ , m_Reply(nullptr)
+ , m_Type(type)
+ , m_UpdatePeriod(period)
+ , m_UserData(userData)
+ , m_Timeout(nullptr)
+ , m_Reroute(false)
+ , m_ID(s_NextID.fetchAndAddAcquire(1))
+ , m_URL(get_management_url())
+ , m_SubModule(subModule)
+ , m_NexusGameID(game->nexusGameID())
+ , m_GameName(game->gameNexusName())
+ , m_Endorse(false)
+ , m_Track(false)
+ , m_Hash(QByteArray())
+{}
+
+
+NexusInterface::NXMRequestInfo::NXMRequestInfo(QByteArray &hash
+ , NexusInterface::NXMRequestInfo::Type type
+ , QVariant userData
+ , const QString &subModule
+ , MOBase::IPluginGame const *game
+)
+ : m_ModID(0)
+ , m_ModVersion("0")
+ , m_FileID(0)
+ , m_Reply(nullptr)
+ , m_Type(type)
+ , m_UpdatePeriod(UpdatePeriod::NONE)
+ , m_UserData(userData)
+ , m_Timeout(nullptr)
+ , m_Reroute(false)
+ , m_ID(s_NextID.fetchAndAddAcquire(1))
+ , m_URL(get_management_url())
+ , m_SubModule(subModule)
+ , m_NexusGameID(game->nexusGameID())
+ , m_GameName(game->gameNexusName())
+ , m_Endorse(false)
+ , m_Track(false)
+ , m_Hash(hash)
+{}
diff --git a/src/nexusinterface.h b/src/nexusinterface.h
index defb370f..ac7f61c5 100644
--- a/src/nexusinterface.h
+++ b/src/nexusinterface.h
@@ -1,422 +1,556 @@
-/*
-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/>.
-*/
-
-#ifndef NEXUSINTERFACE_H
-#define NEXUSINTERFACE_H
-
-#include <utility.h>
-#include <versioninfo.h>
-#include <imodrepositorybridge.h>
-#include <plugincontainer.h>
-
-#include <QNetworkReply>
-#include <QNetworkDiskCache>
-#include <QQueue>
-#include <QVariant>
-#include <QTimer>
-
-#include <list>
-#include <set>
-
-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.
- * Therefore, the objects need to store the id of the requests they started and then filter
- * the result.
- * NexusBridge does this automatically. Users connect to the signals of NexusBridge they intend
- * to handle and only receive the signals the caused
- **/
-class NexusBridge : public MOBase::IModRepositoryBridge
-{
-
- Q_OBJECT
-
-public:
-
- NexusBridge(PluginContainer *pluginContainer, const QString &subModule = "");
-
- /**
- * @brief request description for a mod
- *
- * @param modID id of the mod caller is interested in
- * @param userData user data to be returned with the result
- * @param url the url to request from
- **/
- virtual void requestDescription(QString gameName, int modID, QVariant userData);
-
- /**
- * @brief request a list of the files belonging to a mod
- *
- * @param modID id of the mod caller is interested in
- * @param userData user data to be returned with the result
- **/
- virtual void requestFiles(QString gameName, int modID, QVariant userData);
-
- /**
- * @brief request info about a single file of a mod
- *
- * @param modID id of the mod caller is interested in
- * @param fileID id of the file the caller is interested in
- * @param userData user data to be returned with the result
- **/
- virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData);
-
- /**
- * @brief request the download url of a file
- *
- * @param modID id of the mod caller is interested in
- * @param fileID id of the file the caller is interested in
- * @param userData user data to be returned with the result
- **/
- virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData);
-
- /**
- * @brief requestToggleEndorsement
- * @param modID id of the mod caller is interested in
- * @param userData user data to be returned with the result
- */
- virtual void requestToggleEndorsement(QString gameName, int modID, bool endorse, QVariant userData);
-
-public slots:
-
- void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
- void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
- void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
- void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
- void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
- void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorMessage);
-
-private:
-
- NexusInterface *m_Interface;
- QString m_SubModule;
- std::set<int> m_RequestIDs;
-
-};
-
-
-/**
- * @brief Makes asynchronous requests to the nexus API
- *
- * This class can be used to make asynchronous requests to the Nexus API.
- * Currently, responses are sent to all receivers that have sent a request of the relevant type, so the
- * recipient has to filter the response by the id returned when making the request
- **/
-class NexusInterface : public QObject
-{
- Q_OBJECT
-
-public:
-
- ~NexusInterface();
-
- static NexusInterface *instance(PluginContainer *pluginContainer);
-
- /**
- * @return the access manager object used to connect to nexus
- **/
- NXMAccessManager *getAccessManager();
-
- /**
- * @brief cleanup this interface. this is destructive, afterwards it can't be used again
- */
- void cleanup();
-
- /**
- * @brief clear webcache and cookies associated with this access manager
- */
- void clearCache();
-
- /**
- * @brief request description for a mod
- *
- * @param modID id of the mod caller is interested in (assumed to be for the current game)
- * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable)
- * @param userData user data to be returned with the result
- * @return int an id to identify the request
- **/
- int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule)
- {
- return requestDescription(gameName, modID, receiver, userData, subModule, getGame(gameName));
- }
-
- /**
- * @brief request description for a mod
- *
- * @param modID id of the mod caller is interested in
- * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable)
- * @param userData user data to be returned with the result
- * @param game Game with which the mod is associated
- * @return int an id to identify the request
- **/
- int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule,
- MOBase::IPluginGame const *game);
-
- /**
- * @brief request nexus descriptions for multiple mods at once
- * @param modIDs a list of ids of mods the caller is interested in
- * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable)
- * @param userData user data to be returned with the result
- * @param game the game with which the mods are associated
- * @return int an id to identify the request
- */
- int requestUpdates(const std::vector<int> &modIDs, QObject *receiver, QVariant userData, QString gameName, const QString &subModule);
-
- /**
- * @brief request a list of the files belonging to a mod
- *
- * @param modID id of the mod caller is interested in (assumed to be for the current game)
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @return int an id to identify the request
- **/
- int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule)
- {
- return requestFiles(gameName, modID, receiver, userData, subModule, getGame(gameName));
- }
-
-
- /**
- * @brief request a list of the files belonging to a mod
- *
- * @param modID id of the mod caller is interested in
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @param game the game with which the mods are associated
- * @return int an id to identify the request
- **/
- int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule,
- MOBase::IPluginGame const *game);
-
- /**
- * @brief request info about a single file of a mod
- *
- * @param game name of the game short name to request the download from
- * @param modID id of the mod caller is interested in (assumed to be for the current game)
- * @param fileID id of the file the caller is interested in
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @return int an id to identify the request
- **/
- int requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule);
-
- /**
- * @brief request the download url of a file
- *
- * @param modID id of the mod caller is interested in (assumed to be for the current game)
- * @param fileID id of the file the caller is interested in
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @return int an id to identify the request
- **/
- int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule)
- {
- return requestDownloadURL(gameName, modID, fileID, receiver, userData, subModule, getGame(gameName));
- }
-
- /**
- * @brief request the download url of a file
- *
- * @param modID id of the mod caller is interested in
- * @param fileID id of the file the caller is interested in
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @param game the game with which the mods are associated
- * @return int an id to identify the request
- **/
- int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
-
- /**
- * @brief toggle endorsement state of the mod
- * @param modID id of the mod (assumed to be for the current game)
- * @param endorse true if the mod should be endorsed, false for un-endorse
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @return int an id to identify the request
- */
- int requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData, const QString &subModule)
- {
- return requestToggleEndorsement(gameName, modID, endorse, receiver, userData, subModule, getGame(gameName));
- }
-
- /**
- * @brief toggle endorsement state of the mod
- * @param modID id of the mod
- * @param endorse true if the mod should be endorsed, false for un-endorse
- * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
- * @param userData user data to be returned with the result
- * @param game the game with which the mods are associated
- * @return int an id to identify the request
- */
- int requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData, const QString &subModule,
- MOBase::IPluginGame const *game);
-
- /**
- * @param directory the directory to store cache files
- **/
- void setCacheDirectory(const QString &directory);
-
- /**
- * MO has to send a "Nexus Client Vx.y.z" as part of the user agent to be allowed to use the API
- * @param nmmVersion the version of nmm to impersonate
- **/
- void setNMMVersion(const QString &nmmVersion);
-
- /**
- * @brief called when the log-in completes. This was, requests waiting for the log-in can be run
- */
- void loginCompleted();
-
- std::vector<std::pair<QString, QString>> getGameChoices(const MOBase::IPluginGame *game);
-
-public:
-
- /**
- * @brief guess the mod id from a filename as delivered by Nexus
- * @param fileName name of the file
- * @return the guessed mod id
- * @note this currently doesn't fit well with the remaining interface but this is the best place for the function
- */
- static void interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query);
-
- /**
- * @brief get the currently managed game
- */
- MOBase::IPluginGame const *managedGame() const;
-
- /**
- * @brief see if the passed URL is related to the current game
- *
- * Arguably, this should optionally take a gameplugin pointer
- */
- bool isURLGameRelated(QUrl const &url) const;
-
- /**
- * @brief Get the nexus page for the current game
- *
- * Arguably, this should optionally take a gameplugin pointer
- */
- QString getGameURL(QString gameName) const;
-
- /**
- * @brief Get the URL for the mod web page
- * @param modID
- */
- QString getModURL(int modID, QString gameName) const;
-
- /**
- * @brief Checks if the specified URL might correspond to a nexus mod
- * @param modID
- * @param url
- * @return
- */
- bool isModURL(int modID, QString const &url) const;
-
- void setPluginContainer(PluginContainer *pluginContainer);
-
-signals:
-
- void requestNXMDownload(const QString &url);
-
- void needLogin();
-
- void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
- void nxmUpdatesAvailable(const std::vector<int> &modIDs, QVariant userData, QVariant resultData, int requestID);
- void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
- void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
- void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
- void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
- void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString);
-
-private slots:
-
- void requestFinished();
- void requestError(QNetworkReply::NetworkError error);
- void requestTimeout();
-
- void downloadRequestedNXM(const QString &url);
-
- void fakeFiles();
-
-private:
-
- struct NXMRequestInfo {
- int m_ModID;
- std::vector<int> m_ModIDList;
- int m_FileID;
- QNetworkReply *m_Reply;
- enum Type {
- TYPE_DESCRIPTION,
- TYPE_FILES,
- TYPE_FILEINFO,
- TYPE_DOWNLOADURL,
- TYPE_TOGGLEENDORSEMENT,
- TYPE_GETUPDATES
- } m_Type;
- QVariant m_UserData;
- QTimer *m_Timeout;
- QString m_URL;
- QString m_SubModule;
- QString m_GameName;
- int m_NexusGameID;
- bool m_Reroute;
- int m_ID;
- int m_Endorse;
-
- NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
- NXMRequestInfo(std::vector<int> modIDList, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
- NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
-
- private:
- static QAtomicInt s_NextID;
- };
-
- static const int MAX_ACTIVE_DOWNLOADS = 2;
-
-private:
-
- NexusInterface(PluginContainer *pluginContainer);
- void nextRequest();
- void requestFinished(std::list<NXMRequestInfo>::iterator iter);
- bool requiresLogin(const NXMRequestInfo &info);
- MOBase::IPluginGame *getGame(QString gameName) const;
- 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;
- QString m_NMMVersion;
-
- PluginContainer *m_PluginContainer;
-
-};
-
-#endif // NEXUSINTERFACE_H
+/*
+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/>.
+*/
+
+#ifndef NEXUSINTERFACE_H
+#define NEXUSINTERFACE_H
+
+#include <utility.h>
+#include <versioninfo.h>
+#include <imodrepositorybridge.h>
+#include <plugincontainer.h>
+
+#include <QNetworkReply>
+#include <QNetworkDiskCache>
+#include <QQueue>
+#include <QVariant>
+#include <QTimer>
+
+#include <list>
+#include <set>
+
+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.
+ * Therefore, the objects need to store the id of the requests they started and then filter
+ * the result.
+ * NexusBridge does this automatically. Users connect to the signals of NexusBridge they intend
+ * to handle and only receive the signals the caused
+ **/
+class NexusBridge : public MOBase::IModRepositoryBridge
+{
+
+ Q_OBJECT
+
+public:
+
+ NexusBridge(PluginContainer *pluginContainer, const QString &subModule = "");
+
+ /**
+ * @brief request description for a mod
+ *
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ * @param url the url to request from
+ **/
+ virtual void requestDescription(QString gameName, int modID, QVariant userData);
+
+ /**
+ * @brief request a list of the files belonging to a mod
+ *
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestFiles(QString gameName, int modID, QVariant userData);
+
+ /**
+ * @brief request info about a single file of a mod
+ *
+ * @param modID id of the mod caller is interested in
+ * @param fileID id of the file the caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData);
+
+ /**
+ * @brief request the download url of a file
+ *
+ * @param modID id of the mod caller is interested in
+ * @param fileID id of the file the caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData);
+
+ /**
+ * @brief requestToggleEndorsement
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ */
+ virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData);
+
+ /**
+ * @brief requestToggleTracking
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ */
+ virtual void requestToggleTracking(QString gameName, int modID, bool track, QVariant userData);
+
+public slots:
+
+ void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
+ void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
+ void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID);
+ void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ 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 &errorMessage);
+
+private:
+
+ NexusInterface *m_Interface;
+ QString m_SubModule;
+ std::set<int> m_RequestIDs;
+
+};
+
+
+/**
+ * @brief Makes asynchronous requests to the nexus API
+ *
+ * This class can be used to make asynchronous requests to the Nexus API.
+ * Currently, responses are sent to all receivers that have sent a request of the relevant type, so the
+ * recipient has to filter the response by the id returned when making the request
+ **/
+class NexusInterface : public QObject
+{
+ Q_OBJECT
+
+public:
+
+ enum UpdatePeriod {
+ NONE,
+ DAY,
+ WEEK,
+ MONTH
+ };
+
+public:
+
+ ~NexusInterface();
+
+ static NexusInterface *instance(PluginContainer *pluginContainer);
+
+ /**
+ * @return the access manager object used to connect to nexus
+ **/
+ NXMAccessManager *getAccessManager();
+
+ /**
+ * @brief cleanup this interface. this is destructive, afterwards it can't be used again
+ */
+ void cleanup();
+
+ /**
+ * @brief clear webcache and cookies associated with this access manager
+ */
+ void clearCache();
+
+ /**
+ * @brief request description for a mod
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in (assumed to be for the current game)
+ * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable)
+ * @param userData user data to be returned with the result
+ * @return int an id to identify the request
+ **/
+ int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestDescription(gameName, modID, receiver, userData, subModule, getGame(gameName));
+ }
+
+ /**
+ * @brief request description for a mod
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable)
+ * @param userData user data to be returned with the result
+ * @param game Game with which the mod is associated
+ * @return int an id to identify the request
+ **/
+ int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule,
+ MOBase::IPluginGame const *game);
+
+ /**
+ * @brief request description for a mod
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in (assumed to be for the current game)
+ * @param receiver the object to receive the result asynchronously via a signal (nxmModInfoAvailable)
+ * @param userData user data to be returned with the result
+ * @return int an id to identify the request
+ **/
+ int requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestModInfo(gameName, modID, receiver, userData, subModule, getGame(gameName));
+ }
+
+ /**
+ * @brief request mod info
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmModInfoAvailable)
+ * @param userData user data to be returned with the result
+ * @param game Game with which the mod is associated
+ * @return int an id to identify the request
+ **/
+ int requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule,
+ MOBase::IPluginGame const *game);
+
+ int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData,
+ const QString &subModule)
+ {
+ return requestUpdateInfo(gameName, period, receiver, userData, subModule, getGame(gameName));
+ }
+
+ int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData,
+ const QString &subModule, const MOBase::IPluginGame *game);
+
+ /**
+ * @brief request nexus descriptions for multiple mods at once
+ * @param modID id of the mod the caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable)
+ * @param userData user data to be returned with the result
+ * @param gameName the game with which the mods are associated
+ * @return int an id to identify the request
+ */
+ int requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule);
+
+ /**
+ * @brief request a list of the files belonging to a mod
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in (assumed to be for the current game)
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @return int an id to identify the request
+ **/
+ int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestFiles(gameName, modID, receiver, userData, subModule, getGame(gameName));
+ }
+
+
+ /**
+ * @brief request a list of the files belonging to a mod
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @param game the game with which the mods are associated
+ * @return int an id to identify the request
+ **/
+ int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule,
+ MOBase::IPluginGame const *game);
+
+ /**
+ * @brief request info about a single file of a mod
+ *
+ * @param gameName name of the game short name to request the download from
+ * @param modID id of the mod caller is interested in (assumed to be for the current game)
+ * @param fileID id of the file the caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @return int an id to identify the request
+ **/
+ int requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule);
+
+ /**
+ * @brief request the download url of a file
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in (assumed to be for the current game)
+ * @param fileID id of the file the caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @return int an id to identify the request
+ **/
+ int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestDownloadURL(gameName, modID, fileID, receiver, userData, subModule, getGame(gameName));
+ }
+
+ /**
+ * @brief request the download url of a file
+ *
+ * @param gameName the game short name to support multiple game sources
+ * @param modID id of the mod caller is interested in
+ * @param fileID id of the file the caller is interested in
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @param game the game with which the mods are associated
+ * @return int an id to identify the request
+ **/
+ int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
+
+
+ int requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule);
+
+
+ /**
+ * @param gameName the game short name to support multiple game sources
+ * @brief toggle endorsement state of the mod
+ * @param modID id of the mod (assumed to be for the current game)
+ * @param endorse true if the mod should be endorsed, false for un-endorse
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @return int an id to identify the request
+ */
+ int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName));
+ }
+
+ /**
+ * @param gameName the game short name to support multiple game sources
+ * @brief toggle endorsement state of the mod
+ * @param modID id of the mod
+ * @param endorse true if the mod should be endorsed, false for un-endorse
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @param game the game with which the mods are associated
+ * @return int an id to identify the request
+ */
+ int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule,
+ MOBase::IPluginGame const *game);
+
+ int requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule);
+
+ /**
+ * @param gameName the game short name to support multiple game sources
+ * @brief toggle tracking state of the mod
+ * @param modID id of the mod
+ * @param track true if the mod should be tracked, false for not tracked
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @param game the game with which the mods are associated
+ * @return int an id to identify the request
+ */
+ int requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestToggleTracking(gameName, modID, track, receiver, userData, subModule, getGame(gameName));
+ }
+
+ /**
+ * @param gameName the game short name to support multiple game sources
+ * @brief toggle tracking state of the mod
+ * @param modID id of the mod
+ * @param track true if the mod should be tracked, false for not tracked
+ * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable)
+ * @param userData user data to be returned with the result
+ * @param game the game with which the mods are associated
+ * @return int an id to identify the request
+ */
+ int requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData, const QString &subModule,
+ MOBase::IPluginGame const *game);
+
+ /**
+ *
+ */
+ int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule)
+ {
+ return requestInfoFromMd5(gameName, hash, receiver, userData, subModule, getGame(gameName));
+ }
+
+ /**
+ *
+ */
+ int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule,
+ MOBase::IPluginGame const *game);
+
+ /**
+ * @param directory the directory to store cache files
+ **/
+ void setCacheDirectory(const QString &directory);
+
+ /**
+ * @brief called when the log-in completes. This was, requests waiting for the log-in can be run
+ */
+ void loginCompleted();
+
+ std::vector<std::pair<QString, QString>> getGameChoices(const MOBase::IPluginGame *game);
+
+public:
+
+ /**
+ * @brief guess the mod id from a filename as delivered by Nexus
+ * @param fileName name of the file
+ * @return the guessed mod id
+ * @note this currently doesn't fit well with the remaining interface but this is the best place for the function
+ */
+ static void interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query);
+
+ /**
+ * @brief get the currently managed game
+ */
+ MOBase::IPluginGame const *managedGame() const;
+
+ /**
+ * @brief see if the passed URL is related to the current game
+ *
+ * Arguably, this should optionally take a gameplugin pointer
+ */
+ bool isURLGameRelated(QUrl const &url) const;
+
+ /**
+ * @brief Get the nexus page for the current game
+ *
+ * Arguably, this should optionally take a gameplugin pointer
+ */
+ QString getGameURL(QString gameName) const;
+
+ /**
+ * @brief Get the URL for the mod web page
+ * @param modID
+ */
+ QString getModURL(int modID, QString gameName) const;
+
+ /**
+ * @brief Checks if the specified URL might correspond to a nexus mod
+ * @param modID
+ * @param url
+ * @return
+ */
+ bool isModURL(int modID, QString const &url) const;
+
+ void setPluginContainer(PluginContainer *pluginContainer);
+
+signals:
+
+ void requestNXMDownload(const QString &url);
+
+ void needLogin();
+
+ void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID);
+ void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
+ void nxmFileInfoFromMd5Available(QString gameName, QVariant userData, QVariant resultData, int requestID);
+ void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
+ void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID);
+ void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
+ 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);
+
+public slots:
+
+ void setRateMax(const QString&, bool, std::tuple<int,int,int,int> limits);
+
+private slots:
+
+ void requestFinished();
+ void requestError(QNetworkReply::NetworkError error);
+ void requestTimeout();
+
+ void downloadRequestedNXM(const QString &url);
+
+ void fakeFiles();
+
+private:
+
+ struct NXMRequestInfo {
+ int m_ModID;
+ QString m_ModVersion;
+ std::vector<int> m_ModIDList;
+ int m_FileID;
+ QNetworkReply *m_Reply;
+ enum Type {
+ TYPE_DESCRIPTION,
+ TYPE_MODINFO,
+ TYPE_FILES,
+ TYPE_FILEINFO,
+ TYPE_DOWNLOADURL,
+ TYPE_ENDORSEMENTS,
+ TYPE_TOGGLEENDORSEMENT,
+ TYPE_GETUPDATES,
+ TYPE_CHECKUPDATES,
+ TYPE_TOGGLETRACKING,
+ TYPE_TRACKEDMODS,
+ TYPE_FILEINFO_MD5,
+ } m_Type;
+ UpdatePeriod m_UpdatePeriod;
+ QVariant m_UserData;
+ QTimer *m_Timeout;
+ QString m_URL;
+ QString m_SubModule;
+ QString m_GameName;
+ int m_NexusGameID;
+ bool m_Reroute;
+ int m_ID;
+ int m_Endorse;
+ int m_Track;
+ QByteArray m_Hash;
+ QMap<QNetworkReply::NetworkError,QList<int>> m_AllowedErrors;
+ bool m_IgnoreGenericErrorHandler;
+
+ NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
+ NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
+ NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
+ NXMRequestInfo(Type type, QVariant userData, const QString &subModule);
+ NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
+ NXMRequestInfo(QByteArray &hash, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game);
+
+ private:
+ static QAtomicInt s_NextID;
+ };
+
+ static const int MAX_ACTIVE_DOWNLOADS = 6;
+
+private:
+
+ NexusInterface(PluginContainer *pluginContainer);
+ void nextRequest();
+ void requestFinished(std::list<NXMRequestInfo>::iterator iter);
+ MOBase::IPluginGame *getGame(QString gameName) const;
+ 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;
+
+};
+
+#endif // NEXUSINTERFACE_H
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index 426c8b9c..b1e1ad89 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -1,351 +1,308 @@
-/*
-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 "nxmaccessmanager.h"
-
-#include "iplugingame.h"
-#include "nxmurl.h"
-#include "report.h"
-#include "utility.h"
-#include "selfupdater.h"
-#include "persistentcookiejar.h"
-#include "settings.h"
-#include <QMessageBox>
-#include <QPushButton>
-#include <QNetworkProxy>
-#include <QNetworkRequest>
-#include <QNetworkCookie>
-#include <QNetworkCookieJar>
-#include <QCoreApplication>
-#include <QDir>
-#include <QUrlQuery>
-#include <QThread>
-#include <QJsonDocument>
-#include <QJsonArray>
-
-using namespace MOBase;
-
-namespace {
- QString const Nexus_Management_URL("https://legacy-api.nexusmods.com");
-}
-
-// unfortunately Nexus doesn't seem to document these states, all I know is all these listed
-// are considered premium (27 should be lifetime premium)
-const std::set<int> NXMAccessManager::s_PremiumAccountStates { 4, 6, 13, 27, 31, 32 };
-
-
-NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
- : QNetworkAccessManager(parent)
- , m_LoginReply(nullptr)
- , m_MOVersion(moVersion)
-{
- m_LoginTimeout.setSingleShot(true);
- m_LoginTimeout.setInterval(30000);
- setCookieJar(new PersistentCookieJar(
- QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
-
- if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) {
- // why is this necessary all of a sudden?
- setNetworkAccessible(QNetworkAccessManager::Accessible);
- }
-}
-
-NXMAccessManager::~NXMAccessManager()
-{
- if (m_LoginReply != nullptr) {
- m_LoginReply->deleteLater();
- m_LoginReply = nullptr;
- }
-}
-
-void NXMAccessManager::setNMMVersion(const QString &nmmVersion)
-{
- m_NMMVersion = nmmVersion;
-}
-
-QNetworkReply *NXMAccessManager::createRequest(
- QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
- QIODevice *device)
-{
- if (request.url().scheme() != "nxm") {
- return QNetworkAccessManager::createRequest(operation, request, device);
- }
- if (operation == GetOperation) {
- emit requestNXMDownload(request.url().toString());
-
- // eat the request, everything else will be done by the download manager
- return QNetworkAccessManager::createRequest(QNetworkAccessManager::GetOperation,
- QNetworkRequest(QUrl()));
- } else if (operation == PostOperation) {
- return QNetworkAccessManager::createRequest(operation, request, device);;
- } else {
- return QNetworkAccessManager::createRequest(operation, request, device);
- }
-}
-
-
-void NXMAccessManager::showCookies() const
-{
- QUrl url(Nexus_Management_URL + "/");
- for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) {
- qDebug("%s - %s (expires: %s)",
- cookie.name().constData(), cookie.value().constData(),
- qPrintable(cookie.expirationDate().toString()));
- }
-}
-
-void NXMAccessManager::clearCookies()
-{
- PersistentCookieJar *jar = qobject_cast<PersistentCookieJar*>(cookieJar());
- if (jar != nullptr) {
- jar->clear();
- } else {
- qWarning("failed to clear cookies, invalid cookie jar");
- }
-}
-
-void NXMAccessManager::startLoginCheck()
-{
- if (hasLoginCookies()) {
- qDebug("validating login cookies");
- QNetworkRequest request(Nexus_Management_URL + "/Sessions/?Validate");
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
- request.setRawHeader("User-Agent", userAgent().toUtf8());
-
- m_LoginReply = get(request);
- m_LoginTimeout.start();
- m_LoginState = LOGIN_CHECKING;
- connect(m_LoginReply, SIGNAL(finished()), this, SLOT(loginChecked()));
- connect(m_LoginReply, SIGNAL(error(QNetworkReply::NetworkError)),
- this, SLOT(loginError(QNetworkReply::NetworkError)));
- }
-}
-
-
-void NXMAccessManager::retrieveCredentials()
-{
- qDebug("retrieving credentials");
-
- QNetworkRequest request(Nexus_Management_URL + "/Core/Libs/Flamework/Entities/User?GetCredentials");
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
- request.setRawHeader("User-Agent", userAgent().toUtf8());
-
- QNetworkReply *reply = get(request);
- QTimer timeout;
- connect(&timeout, &QTimer::timeout, [reply] () {
- reply->deleteLater();
- });
- timeout.start();
-
- connect(reply, &QNetworkReply::finished, [reply, this] () {
- QJsonDocument jdoc = QJsonDocument::fromJson(reply->readAll());
- QJsonArray credentialsData = jdoc.array();
- emit credentialsReceived(credentialsData.at(2).toString(),
- s_PremiumAccountStates.find(credentialsData.at(1).toInt())
- != s_PremiumAccountStates.end());
- reply->deleteLater();
- });
-
- connect(reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
- [=] (QNetworkReply::NetworkError) {
- qDebug("failed to retrieve account credentials: %s", qPrintable(reply->errorString()));
- reply->deleteLater();
- });
-}
-
-
-bool NXMAccessManager::loggedIn() const
-{
- if (m_LoginState == LOGIN_CHECKING) {
- QProgressDialog progress;
- progress.setLabelText(tr("Verifying Nexus login"));
- progress.show();
- while (m_LoginState == LOGIN_CHECKING) {
- QCoreApplication::processEvents();
- QThread::msleep(100);
- }
- progress.hide();
- }
-
- return m_LoginState == LOGIN_VALID;
-}
-
-
-void NXMAccessManager::refuseLogin()
-{
- m_LoginState = LOGIN_REFUSED;
-}
-
-
-bool NXMAccessManager::loginAttempted() const
-{
- return m_LoginState != LOGIN_NOT_CHECKED;
-}
-
-
-bool NXMAccessManager::loginWaiting() const
-{
- return m_LoginReply != nullptr;
-}
-
-
-void NXMAccessManager::login(const QString &username, const QString &password)
-{
- if (m_LoginReply != nullptr) {
- return;
- }
-
- if (m_LoginState == LOGIN_VALID) {
- emit loginSuccessful(false);
- return;
- }
- m_Username = username;
- m_Password = password;
- pageLogin();
-}
-
-
-QString NXMAccessManager::userAgent(const QString &subModule) const
-{
- QStringList comments;
- comments << "Nexus Client v" + m_NMMVersion;
- if (!subModule.isEmpty()) {
- comments << "module: " + subModule;
- }
-
- return QString("Mod Organizer/%1 (%2)").arg(m_MOVersion, comments.join("; "));
-}
-
-
-void NXMAccessManager::pageLogin()
-{
- qDebug("logging %s in on Nexus", qPrintable(m_Username));
- QString requestString = (Nexus_Management_URL + "/Sessions/?Login&uri=%1")
- .arg(QString(QUrl::toPercentEncoding(Nexus_Management_URL)));
-
- QNetworkRequest request(requestString);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
-
- QByteArray postDataQuery;
- QUrlQuery postData;
- postData.addQueryItem("username", m_Username);
- postData.addQueryItem("password", QUrl::toPercentEncoding(m_Password));
- postDataQuery = postData.query(QUrl::EncodeReserved).toUtf8();
-
- request.setRawHeader("User-Agent", userAgent().toUtf8());
-
- m_ProgressDialog = new QProgressDialog(nullptr);
- m_ProgressDialog->setLabelText(tr("Logging into Nexus"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- m_ProgressDialog->show();
- QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback
-
- m_LoginReply = post(request, postDataQuery);
- m_LoginTimeout.start();
- m_LoginState = LOGIN_CHECKING;
- connect(m_LoginReply, SIGNAL(finished()), this, SLOT(loginFinished()));
- connect(m_LoginReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(loginError(QNetworkReply::NetworkError)));
-}
-
-
-void NXMAccessManager::loginTimeout()
-{
- m_LoginReply->deleteLater();
- m_LoginReply = nullptr;
- m_LoginAttempted = false; // this usually means we might have success later
- m_Username.clear();
- m_Password.clear();
- m_LoginState = LOGIN_NOT_VALID;
-
- emit loginFailed(tr("timeout"));
-}
-
-
-void NXMAccessManager::loginError(QNetworkReply::NetworkError)
-{
- qDebug("login error");
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
- m_Username.clear();
- m_Password.clear();
- m_LoginState = LOGIN_NOT_VALID;
-
- if (m_LoginReply != nullptr) {
- emit loginFailed(m_LoginReply->errorString());
- m_LoginReply->deleteLater();
- m_LoginReply = nullptr;
- } else {
- emit loginFailed(tr("Unknown error"));
- }
-}
-
-
-bool NXMAccessManager::hasLoginCookies() const
-{
- QUrl url(Nexus_Management_URL + "/");
- QList<QNetworkCookie> cookies = cookieJar()->cookiesForUrl(url);
- for (const QNetworkCookie &cookie : cookies) {
- if (cookie.name() == "sid") {
- return true;
- }
- }
- return false;
-}
-
-
-void NXMAccessManager::loginFinished()
-{
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
-
- m_LoginReply->deleteLater();
- m_LoginReply = nullptr;
- m_Username.clear();
- m_Password.clear();
-
- if (hasLoginCookies()) {
- m_LoginState = LOGIN_VALID;
- retrieveCredentials();
- emit loginSuccessful(true);
- } else {
- m_LoginState = LOGIN_NOT_VALID;
- emit loginFailed(tr("Please check your password"));
- }
-}
-
-
-void NXMAccessManager::loginChecked()
-{
- QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
- QByteArray data = reply->readAll();
- m_LoginState = data == "null" ? LOGIN_NOT_VALID
- : LOGIN_VALID;
- if (m_LoginState == LOGIN_VALID) {
- retrieveCredentials();
- } else {
- qDebug("cookies seem to be invalid");
- }
- m_LoginReply->deleteLater();
- m_LoginReply = nullptr;
-}
+/*
+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 "nxmaccessmanager.h"
+
+#include "iplugingame.h"
+#include "nxmurl.h"
+#include "report.h"
+#include "utility.h"
+#include "selfupdater.h"
+#include "persistentcookiejar.h"
+#include "settings.h"
+#include <QMessageBox>
+#include <QPushButton>
+#include <QNetworkProxy>
+#include <QNetworkRequest>
+#include <QNetworkCookie>
+#include <QNetworkCookieJar>
+#include <QCoreApplication>
+#include <QDir>
+#include <QUrlQuery>
+#include <QThread>
+#include <QJsonDocument>
+#include <QJsonArray>
+
+using namespace MOBase;
+
+namespace {
+ QString const nexusBaseUrl("https://api.nexusmods.com/v1");
+}
+
+NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
+ : QNetworkAccessManager(parent)
+ , m_ValidateReply(nullptr)
+ , m_MOVersion(moVersion)
+{
+ m_ValidateTimeout.setSingleShot(true);
+ m_ValidateTimeout.setInterval(30000);
+ connect(&m_ValidateTimeout, SIGNAL(timeout()), this, SLOT(validateTimeout()));
+ setCookieJar(new PersistentCookieJar(
+ QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
+
+ if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) {
+ // why is this necessary all of a sudden?
+ setNetworkAccessible(QNetworkAccessManager::Accessible);
+ }
+}
+
+NXMAccessManager::~NXMAccessManager()
+{
+ if (m_ValidateReply != nullptr) {
+ m_ValidateReply->deleteLater();
+ m_ValidateReply = nullptr;
+ }
+}
+
+QNetworkReply *NXMAccessManager::createRequest(
+ QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
+ QIODevice *device)
+{
+ if (request.url().scheme() != "nxm") {
+ return QNetworkAccessManager::createRequest(operation, request, device);
+ }
+ if (operation == GetOperation) {
+ emit requestNXMDownload(request.url().toString());
+
+ // eat the request, everything else will be done by the download manager
+ return QNetworkAccessManager::createRequest(QNetworkAccessManager::GetOperation,
+ QNetworkRequest(QUrl()));
+ } else if (operation == PostOperation) {
+ return QNetworkAccessManager::createRequest(operation, request, device);;
+ } else {
+ return QNetworkAccessManager::createRequest(operation, request, device);
+ }
+}
+
+
+void NXMAccessManager::showCookies() const
+{
+ QUrl url(nexusBaseUrl + "/");
+ for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) {
+ qDebug("%s - %s (expires: %s)",
+ cookie.name().constData(), cookie.value().constData(),
+ qUtf8Printable(cookie.expirationDate().toString()));
+ }
+}
+
+void NXMAccessManager::clearCookies()
+{
+ PersistentCookieJar *jar = qobject_cast<PersistentCookieJar*>(cookieJar());
+ if (jar != nullptr) {
+ jar->clear();
+ } else {
+ qWarning("failed to clear cookies, invalid cookie jar");
+ }
+}
+
+void NXMAccessManager::startValidationCheck()
+{
+ qDebug("Checking Nexus API Key...");
+ QString requestString = nexusBaseUrl + "/users/validate";
+
+ QNetworkRequest request(requestString);
+ request.setRawHeader("APIKEY", m_ApiKey.toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, userAgent().toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Protocol-Version", "1.0.0");
+ request.setRawHeader("Application-Name", "MO2");
+ request.setRawHeader("Application-Version", m_MOVersion.toUtf8());
+
+ m_ProgressDialog = new QProgressDialog(nullptr);
+ m_ProgressDialog->setLabelText(tr("Validating Nexus Connection"));
+ QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
+ buttons.at(0)->setEnabled(false);
+ m_ProgressDialog->show();
+ QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback
+
+ m_ValidateReply = get(request);
+ m_ValidateTimeout.start();
+ m_ValidateState = VALIDATE_CHECKING;
+ connect(m_ValidateReply, SIGNAL(finished()), this, SLOT(validateFinished()));
+ connect(m_ValidateReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(validateError(QNetworkReply::NetworkError)));
+}
+
+
+bool NXMAccessManager::validated() const
+{
+ if (m_ValidateState == VALIDATE_CHECKING) {
+ QProgressDialog progress;
+ progress.setLabelText(tr("Validating Nexus Connection"));
+ QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
+ buttons.at(0)->setEnabled(false);
+ progress.show();
+ while (m_ValidateState == VALIDATE_CHECKING) {
+ QCoreApplication::processEvents();
+ QThread::msleep(100);
+ }
+ progress.hide();
+ }
+
+ return m_ValidateState == VALIDATE_VALID;
+}
+
+
+void NXMAccessManager::refuseValidation()
+{
+ m_ValidateState = VALIDATE_REFUSED;
+}
+
+
+bool NXMAccessManager::validateAttempted() const
+{
+ return m_ValidateState != VALIDATE_NOT_CHECKED;
+}
+
+
+bool NXMAccessManager::validateWaiting() const
+{
+ return m_ValidateReply != nullptr;
+}
+
+
+void NXMAccessManager::apiCheck(const QString &apiKey)
+{
+ if (m_ValidateReply != nullptr) {
+ return;
+ }
+
+ if (m_ValidateState == VALIDATE_VALID) {
+ emit validateSuccessful(false);
+ return;
+ }
+ m_ApiKey = apiKey;
+ startValidationCheck();
+}
+
+
+QString NXMAccessManager::userAgent(const QString &subModule) const
+{
+ QStringList comments;
+ QString os;
+ if (QSysInfo::productType() == "windows")
+ comments << ((QSysInfo::kernelType() == "winnt") ? "Windows_NT " : "Windows ") + QSysInfo::kernelVersion();
+ else
+ comments << QSysInfo::kernelType().left(1).toUpper() + QSysInfo::kernelType().mid(1)
+ << QSysInfo::productType().left(1).toUpper() + QSysInfo::kernelType().mid(1) + " " + QSysInfo::productVersion();
+ if (!subModule.isEmpty()) {
+ comments << "module: " + subModule;
+ }
+ comments << ((QSysInfo::buildCpuArchitecture() == "x86_64") ? "x64" : "x86");
+
+ return QString("Mod Organizer/%1 (%2) Qt/%3").arg(m_MOVersion, comments.join("; "), qVersion());
+}
+
+
+QString NXMAccessManager::apiKey() const
+{
+ return m_ApiKey;
+}
+
+
+void NXMAccessManager::validateTimeout()
+{
+ m_ValidateTimeout.stop();
+ if (m_ProgressDialog != nullptr) {
+ m_ProgressDialog->hide();
+ m_ProgressDialog->deleteLater();
+ m_ProgressDialog = nullptr;
+ }
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_VALID;
+
+ 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"));
+}
+
+
+void NXMAccessManager::validateError(QNetworkReply::NetworkError)
+{
+ m_ValidateTimeout.stop();
+ if (m_ProgressDialog != nullptr) {
+ m_ProgressDialog->hide();
+ m_ProgressDialog->deleteLater();
+ m_ProgressDialog = nullptr;
+ }
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_VALID;
+
+ if (m_ValidateReply != nullptr) {
+ m_ValidateReply->disconnect();
+ QString error = m_ValidateReply->errorString();
+ m_ValidateReply->deleteLater();
+ m_ValidateReply = nullptr;
+ emit validateFailed(error);
+ } else {
+ emit validateFailed(tr("Unknown error"));
+ }
+}
+
+
+void NXMAccessManager::validateFinished()
+{
+ m_ValidateTimeout.stop();
+ if (m_ProgressDialog != nullptr) {
+ m_ProgressDialog->deleteLater();
+ m_ProgressDialog = nullptr;
+ }
+
+ if (m_ValidateReply != nullptr) {
+ QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll());
+ if (!jdoc.isNull()) {
+ QJsonObject credentialsData = jdoc.object();
+ if (credentialsData.contains("user_id")) {
+ 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()
+ ));
+
+ emit credentialsReceived(name, premium, limits);
+
+ m_ValidateReply->deleteLater();
+ m_ValidateReply = nullptr;
+
+ m_ValidateState = VALIDATE_VALID;
+ emit validateSuccessful(true);
+ } else {
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_VALID;
+ emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString()));
+ }
+ } else {
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_CHECKED;
+ emit validateFailed(tr("Could not parse response. Invalid JSON."));
+ }
+ }
+ else {
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_CHECKED;
+ emit validateFailed(tr("Unknown error."));
+ }
+}
diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h
index c58c4cc3..da7736cc 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -1,133 +1,117 @@
-/*
-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/>.
-*/
-
-#ifndef NXMACCESSMANAGER_H
-#define NXMACCESSMANAGER_H
-
-
-#include <QNetworkAccessManager>
-#include <QTimer>
-#include <QNetworkReply>
-#include <QProgressDialog>
-#include <set>
-
-namespace MOBase { class IPluginGame; }
-
-/**
- * @brief access manager extended to handle nxm links
- **/
-class NXMAccessManager : public QNetworkAccessManager
-{
- Q_OBJECT
-public:
-
- explicit NXMAccessManager(QObject *parent, const QString &moVersion);
-
- ~NXMAccessManager();
-
- void setNMMVersion(const QString &nmmVersion);
-
- bool loggedIn() const;
-
- bool loginAttempted() const;
- bool loginWaiting() const;
-
- void login(const QString &username, const QString &password);
-
- void showCookies() const;
-
- void clearCookies();
-
- QString userAgent(const QString &subModule = QString()) const;
-
- void startLoginCheck();
-
- void refuseLogin();
-
-signals:
-
- /**
- * @brief emitted when a nxm:// link is opened
- *
- * @param url the nxm-link
- **/
- void requestNXMDownload(const QString &url);
-
- /**
- * @brief emitted after a successful login or if login was not necessary
- *
- * @param necessary true if a login was necessary and succeeded, false if the user is still logged in
- **/
- void loginSuccessful(bool necessary);
-
- void loginFailed(const QString &message);
-
- void credentialsReceived(const QString &userName, bool premium);
-
-private slots:
-
- void loginChecked();
- void loginFinished();
- void loginError(QNetworkReply::NetworkError errorCode);
- void loginTimeout();
-
-protected:
-
- virtual QNetworkReply *createRequest(
- QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
- QIODevice *device);
-
-private:
-
- void pageLogin();
-// void dlLogin();
-
- bool hasLoginCookies() const;
-
- void retrieveCredentials();
-
-private:
-
- static const std::set<int> s_PremiumAccountStates;
-
-private:
-
- QTimer m_LoginTimeout;
- QNetworkReply *m_LoginReply;
- QProgressDialog *m_ProgressDialog { nullptr };
-
- QString m_MOVersion;
- QString m_NMMVersion;
-
- QString m_Username;
- QString m_Password;
-
- bool m_LoginAttempted;
- enum {
- LOGIN_NOT_CHECKED,
- LOGIN_CHECKING,
- LOGIN_NOT_VALID,
- LOGIN_ATTEMPT_FAILED,
- LOGIN_REFUSED,
- LOGIN_VALID
- } m_LoginState = LOGIN_NOT_CHECKED;
-
-};
-
-#endif // NXMACCESSMANAGER_H
+/*
+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/>.
+*/
+
+#ifndef NXMACCESSMANAGER_H
+#define NXMACCESSMANAGER_H
+
+
+#include <QNetworkAccessManager>
+#include <QTimer>
+#include <QNetworkReply>
+#include <QProgressDialog>
+#include <set>
+
+namespace MOBase { class IPluginGame; }
+
+/**
+ * @brief access manager extended to handle nxm links
+ **/
+class NXMAccessManager : public QNetworkAccessManager
+{
+ Q_OBJECT
+public:
+
+ explicit NXMAccessManager(QObject *parent, const QString &moVersion);
+
+ ~NXMAccessManager();
+
+ bool validated() const;
+
+ bool validateAttempted() const;
+ bool validateWaiting() const;
+
+ void apiCheck(const QString &apiKey);
+
+ void showCookies() const;
+
+ void clearCookies();
+
+ QString userAgent(const QString &subModule = QString()) const;
+
+ QString apiKey() const;
+
+ void startValidationCheck();
+
+ void refuseValidation();
+
+signals:
+
+ /**
+ * @brief emitted when a nxm:// link is opened
+ *
+ * @param url the nxm-link
+ **/
+ void requestNXMDownload(const QString &url);
+
+ /**
+ * @brief emitted after a successful login or if login was not necessary
+ *
+ * @param necessary true if a login was necessary and succeeded, false if the user is still logged in
+ **/
+ void validateSuccessful(bool necessary);
+
+ void validateFailed(const QString &message);
+
+ void credentialsReceived(const QString &userName, bool premium, std::tuple<int, int, int, int> limits);
+
+private slots:
+
+ void validateFinished();
+ void validateError(QNetworkReply::NetworkError errorCode);
+ void validateTimeout();
+
+protected:
+
+ virtual QNetworkReply *createRequest(
+ QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
+ QIODevice *device);
+
+private:
+
+ QTimer m_ValidateTimeout;
+ QNetworkReply *m_ValidateReply;
+ QProgressDialog *m_ProgressDialog { nullptr };
+
+ QString m_MOVersion;
+
+ QString m_ApiKey;
+
+ bool m_ValidateAttempted;
+ enum {
+ VALIDATE_NOT_CHECKED,
+ VALIDATE_CHECKING,
+ VALIDATE_NOT_VALID,
+ VALIDATE_ATTEMPT_FAILED,
+ VALIDATE_REFUSED,
+ VALIDATE_VALID
+ } m_ValidateState = VALIDATE_NOT_CHECKED;
+
+};
+
+#endif // NXMACCESSMANAGER_H
diff --git a/src/organizer.pro b/src/organizer.pro
index 582fde9f..ddf676f2 100644
--- a/src/organizer.pro
+++ b/src/organizer.pro
@@ -53,7 +53,6 @@ SOURCES += \
executableslist.cpp \
editexecutablesdialog.cpp \
downloadmanager.cpp \
- downloadlistwidgetcompact.cpp \
downloadlistwidget.cpp \
downloadlistsortproxy.cpp \
downloadlist.cpp \
@@ -92,7 +91,10 @@ SOURCES += \
modinfobackup.cpp \
modinfooverwrite.cpp \
modinfoforeign.cpp \
- listdialog.cpp
+ listdialog.cpp \
+ lcdnumber.cpp \
+ forcedloaddialog.cpp \
+ forcedloaddialogwidget.cpp
HEADERS += \
@@ -129,7 +131,6 @@ HEADERS += \
executableslist.h \
editexecutablesdialog.h \
downloadmanager.h \
- downloadlistwidgetcompact.h \
downloadlistwidget.h \
downloadlistsortproxy.h \
downloadlist.h \
@@ -169,7 +170,10 @@ HEADERS += \
modinfobackup.h \
modinfooverwrite.h \
modinfoforeign.h \
- listdialog.h
+ listdialog.h \
+ lcdnumber.h \
+ forcedloaddialog.h \
+ forcedloaddialogwidget.h
FORMS += \
transfersavesdialog.ui \
@@ -188,8 +192,6 @@ FORMS += \
installdialog.ui \
finddialog.ui \
editexecutablesdialog.ui \
- downloadlistwidgetcompact.ui \
- downloadlistwidget.ui \
credentialsdialog.ui \
categoriesdialog.ui \
activatemodsdialog.ui \
@@ -199,7 +201,9 @@ FORMS += \
previewdialog.ui \
browserdialog.ui \
aboutdialog.ui \
- listdialog.ui
+ listdialog.ui \
+ forcedloaddialog.ui \
+ forcedloaddialogwidget.ui
RESOURCES += \
resources.qrc \
diff --git a/src/organizer_en.ts b/src/organizer_en.ts
index b076bac6..28ddc09c 100644
--- a/src/organizer_en.ts
+++ b/src/organizer_en.ts
@@ -26,7 +26,7 @@
</message>
<message>
<location filename="aboutdialog.ui" line="160"/>
- <source>Current Maintainers</source>
+ <source>Lead Developers/ Maintainers</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -35,67 +35,92 @@
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="206"/>
- <source>Major Contributors</source>
+ <location filename="aboutdialog.ui" line="201"/>
+ <source>Mo2 devs and Contributors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="227"/>
+ <location filename="aboutdialog.ui" line="216"/>
+ <source>Project579</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="aboutdialog.ui" line="221"/>
+ <source>przester</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="aboutdialog.ui" line="232"/>
<source>Translators</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="252"/>
+ <location filename="aboutdialog.ui" line="257"/>
<source>Cyb3r (Dutch)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="262"/>
+ <location filename="aboutdialog.ui" line="267"/>
<source>fruttyx (French)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="277"/>
+ <location filename="aboutdialog.ui" line="282"/>
<source>Yoplala (French)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="282"/>
+ <location filename="aboutdialog.ui" line="287"/>
<source>Faron (German)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="292"/>
+ <location filename="aboutdialog.ui" line="297"/>
<source>Mordan (Greek)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="305"/>
+ <location filename="aboutdialog.ui" line="310"/>
<source>Yoosk (Polish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="310"/>
+ <location filename="aboutdialog.ui" line="315"/>
<source>Brgodfx (Portuguese)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="330"/>
+ <location filename="aboutdialog.ui" line="320"/>
+ <source>zDas (Portuguese)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="aboutdialog.ui" line="340"/>
<source>Jax (Swedish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="335"/>
+ <location filename="aboutdialog.ui" line="345"/>
+ <source>yohru (Japanese)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="aboutdialog.ui" line="350"/>
<source>...and all other Transifex contributors!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="349"/>
+ <location filename="aboutdialog.ui" line="364"/>
<source>Other Supporters &amp;&amp; Contributors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="471"/>
+ <location filename="aboutdialog.ui" line="374"/>
+ <source>Tannin (Original Creator)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="aboutdialog.ui" line="491"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
@@ -279,12 +304,12 @@ p, li { white-space: pre-wrap; }
<context>
<name>DirectoryRefresher</name>
<message>
- <location filename="directoryrefresher.cpp" line="94"/>
+ <location filename="directoryrefresher.cpp" line="113"/>
<source>failed to parse bsa %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="directoryrefresher.cpp" line="160"/>
+ <location filename="directoryrefresher.cpp" line="188"/>
<source>failed to read mod (%1): %2</source>
<translation type="unfinished"></translation>
</message>
@@ -292,435 +317,248 @@ p, li { white-space: pre-wrap; }
<context>
<name>DownloadList</name>
<message>
- <location filename="downloadlist.cpp" line="64"/>
+ <location filename="downloadlist.cpp" line="71"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="65"/>
- <source>Filetime</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlist.cpp" line="66"/>
+ <location filename="downloadlist.cpp" line="72"/>
<source>Size</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="67"/>
- <source>Done</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlist.cpp" line="83"/>
- <source>Information missing, please select &quot;Query Info&quot; from the context menu to re-retrieve.</source>
+ <location filename="downloadlist.cpp" line="73"/>
+ <source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="90"/>
- <source>pending download</source>
+ <location filename="downloadlist.cpp" line="74"/>
+ <source>Filetime</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>DownloadListWidget</name>
<message>
- <location filename="downloadlistwidget.ui" line="17"/>
- <location filename="downloadlistwidget.ui" line="61"/>
- <source>Placeholder</source>
+ <location filename="downloadlist.cpp" line="89"/>
+ <source>&lt; game %1 mod %2 file %3 &gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.ui" line="102"/>
- <location filename="downloadlistwidget.cpp" line="169"/>
- <location filename="downloadlistwidget.cpp" line="171"/>
- <source>Done - Double Click to install</source>
+ <location filename="downloadlist.cpp" line="90"/>
+ <source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="135"/>
- <location filename="downloadlistwidget.cpp" line="137"/>
- <source>Paused - Double Click to resume</source>
+ <location filename="downloadlist.cpp" line="91"/>
+ <source>Pending</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="155"/>
- <location filename="downloadlistwidget.cpp" line="157"/>
- <source>Installed - Double Click to re-install</source>
+ <location filename="downloadlist.cpp" line="101"/>
+ <source>Started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="162"/>
- <location filename="downloadlistwidget.cpp" line="164"/>
- <source>Uninstalled - Double Click to re-install</source>
+ <location filename="downloadlist.cpp" line="102"/>
+ <source>Canceling</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>DownloadListWidgetCompact</name>
<message>
- <location filename="downloadlistwidgetcompact.ui" line="17"/>
- <location filename="downloadlistwidgetcompact.ui" line="56"/>
- <source>Placeholder</source>
+ <location filename="downloadlist.cpp" line="103"/>
+ <source>Pausing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.ui" line="129"/>
- <source>Done</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-<context>
- <name>DownloadListWidgetCompactDelegate</name>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="109"/>
- <source>&lt; game %1 mod %2 file %3 &gt;</source>
+ <location filename="downloadlist.cpp" line="104"/>
+ <source>Canceled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="114"/>
- <source>Pending</source>
+ <location filename="downloadlist.cpp" line="105"/>
+ <source>Paused</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="141"/>
- <source>Paused</source>
+ <location filename="downloadlist.cpp" line="106"/>
+ <source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="143"/>
- <source>Fetching Info 1</source>
+ <location filename="downloadlist.cpp" line="107"/>
+ <location filename="downloadlist.cpp" line="108"/>
+ <location filename="downloadlist.cpp" line="109"/>
+ <source>Fetching Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="145"/>
- <source>Fetching Info 2</source>
+ <location filename="downloadlist.cpp" line="110"/>
+ <source>Downloaded</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="150"/>
+ <location filename="downloadlist.cpp" line="111"/>
<source>Installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="152"/>
+ <location filename="downloadlist.cpp" line="112"/>
<source>Uninstalled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="154"/>
- <source>Done</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="233"/>
- <location filename="downloadlistwidgetcompact.cpp" line="288"/>
- <location filename="downloadlistwidgetcompact.cpp" line="297"/>
- <location filename="downloadlistwidgetcompact.cpp" line="306"/>
- <location filename="downloadlistwidgetcompact.cpp" line="315"/>
- <location filename="downloadlistwidgetcompact.cpp" line="324"/>
- <location filename="downloadlistwidgetcompact.cpp" line="333"/>
- <source>Are you sure?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="234"/>
- <source>This will permanently delete the selected download.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="289"/>
- <source>This will remove all finished downloads from this list and from disk.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="298"/>
- <source>This will remove all installed downloads from this list and from disk.</source>
+ <location filename="downloadlist.cpp" line="130"/>
+ <source>Pending download</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="307"/>
- <source>This will remove all uninstalled downloads from this list and from disk.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="316"/>
- <source>This will permanently remove all finished downloads from this list (but NOT from disk).</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="325"/>
- <source>This will permanently remove all installed downloads from this list (but NOT from disk).</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="334"/>
- <source>This will permanently remove all uninstalled downloads from this list (but NOT from disk).</source>
+ <location filename="downloadlist.cpp" line="134"/>
+ <source>Information missing, please select &quot;Query Info&quot; from the context menu to re-retrieve.</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>DownloadListWidget</name>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="363"/>
+ <location filename="downloadlistwidget.cpp" line="201"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="365"/>
+ <location filename="downloadlistwidget.cpp" line="203"/>
<source>Query Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="367"/>
+ <location filename="downloadlistwidget.cpp" line="205"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="369"/>
+ <location filename="downloadlistwidget.cpp" line="206"/>
<source>Open File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="370"/>
- <location filename="downloadlistwidgetcompact.cpp" line="381"/>
- <location filename="downloadlistwidgetcompact.cpp" line="385"/>
+ <location filename="downloadlistwidget.cpp" line="207"/>
+ <location filename="downloadlistwidget.cpp" line="219"/>
+ <location filename="downloadlistwidget.cpp" line="224"/>
<source>Show in Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="372"/>
+ <location filename="downloadlistwidget.cpp" line="211"/>
+ <location filename="downloadlistwidget.cpp" line="222"/>
<source>Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="374"/>
+ <location filename="downloadlistwidget.cpp" line="213"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="376"/>
+ <location filename="downloadlistwidget.cpp" line="215"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="379"/>
+ <location filename="downloadlistwidget.cpp" line="217"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="380"/>
+ <location filename="downloadlistwidget.cpp" line="218"/>
<source>Pause</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="383"/>
- <source>Remove</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidgetcompact.cpp" line="384"/>
+ <location filename="downloadlistwidget.cpp" line="223"/>
<source>Resume</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="389"/>
+ <location filename="downloadlistwidget.cpp" line="229"/>
<source>Delete Installed...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="390"/>
+ <location filename="downloadlistwidget.cpp" line="230"/>
<source>Delete Uninstalled...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="391"/>
+ <location filename="downloadlistwidget.cpp" line="231"/>
<source>Delete All...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="394"/>
+ <location filename="downloadlistwidget.cpp" line="235"/>
<source>Hide Installed...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="395"/>
+ <location filename="downloadlistwidget.cpp" line="236"/>
<source>Hide Uninstalled...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="396"/>
+ <location filename="downloadlistwidget.cpp" line="237"/>
<source>Hide All...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidgetcompact.cpp" line="400"/>
+ <location filename="downloadlistwidget.cpp" line="239"/>
<source>Un-Hide All...</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>DownloadListWidgetDelegate</name>
- <message>
- <location filename="downloadlistwidget.cpp" line="112"/>
- <source>&lt; game %1 mod %2 file %3 &gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="115"/>
- <source>Pending</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="142"/>
- <source>Fetching Info 1</source>
- <translation type="unfinished"></translation>
- </message>
<message>
- <location filename="downloadlistwidget.cpp" line="145"/>
- <source>Fetching Info 2</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="248"/>
- <location filename="downloadlistwidget.cpp" line="302"/>
- <location filename="downloadlistwidget.cpp" line="311"/>
- <location filename="downloadlistwidget.cpp" line="320"/>
+ <location filename="downloadlistwidget.cpp" line="262"/>
+ <location filename="downloadlistwidget.cpp" line="317"/>
+ <location filename="downloadlistwidget.cpp" line="326"/>
+ <location filename="downloadlistwidget.cpp" line="335"/>
<source>Delete Files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="249"/>
+ <location filename="downloadlistwidget.cpp" line="263"/>
<source>This will permanently delete the selected download.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="303"/>
+ <location filename="downloadlistwidget.cpp" line="318"/>
<source>This will remove all finished downloads from this list and from disk.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="312"/>
+ <location filename="downloadlistwidget.cpp" line="327"/>
<source>This will remove all installed downloads from this list and from disk.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="321"/>
+ <location filename="downloadlistwidget.cpp" line="336"/>
<source>This will remove all uninstalled downloads from this list and from disk.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="329"/>
- <location filename="downloadlistwidget.cpp" line="338"/>
- <location filename="downloadlistwidget.cpp" line="347"/>
+ <location filename="downloadlistwidget.cpp" line="344"/>
+ <location filename="downloadlistwidget.cpp" line="353"/>
+ <location filename="downloadlistwidget.cpp" line="362"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="330"/>
+ <location filename="downloadlistwidget.cpp" line="345"/>
<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="339"/>
+ <location filename="downloadlistwidget.cpp" line="354"/>
<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="348"/>
+ <location filename="downloadlistwidget.cpp" line="363"/>
<source>This will remove all uninstalled downloads from this list (but NOT from disk).</source>
<translation type="unfinished"></translation>
</message>
- <message>
- <location filename="downloadlistwidget.cpp" line="376"/>
- <source>Install</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="378"/>
- <source>Query Info</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="380"/>
- <source>Visit on Nexus</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="383"/>
- <source>Open File</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="384"/>
- <location filename="downloadlistwidget.cpp" line="397"/>
- <location filename="downloadlistwidget.cpp" line="401"/>
- <source>Show in Folder</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="388"/>
- <location filename="downloadlistwidget.cpp" line="399"/>
- <source>Delete</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="390"/>
- <source>Un-Hide</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="392"/>
- <source>Hide</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="395"/>
- <source>Cancel</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="396"/>
- <source>Pause</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="400"/>
- <source>Resume</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="406"/>
- <source>Delete Installed...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="407"/>
- <source>Delete Uninstalled...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="408"/>
- <source>Delete All...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="412"/>
- <source>Hide Installed...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="413"/>
- <source>Hide Uninstalled...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="414"/>
- <source>Hide All...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="downloadlistwidget.cpp" line="418"/>
- <source>Un-Hide All...</source>
- <translation type="unfinished"></translation>
- </message>
</context>
<context>
<name>DownloadManager</name>
@@ -730,301 +568,317 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="389"/>
+ <location filename="downloadmanager.cpp" line="390"/>
<source>Memory allocation error (in refreshing directory).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="479"/>
+ <location filename="downloadmanager.cpp" line="490"/>
<source>failed to download %1: could not open output file: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="502"/>
+ <location filename="downloadmanager.cpp" line="513"/>
<source>Download again?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="502"/>
- <source>A file with the same name has already been downloaded. Do you want to download it again? The new file will receive a different name.</source>
+ <location filename="downloadmanager.cpp" line="513"/>
+ <source>A file with the same name &quot;%1&quot; has already been downloaded. Do you want to download it again? The new file will receive a different name.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="546"/>
+ <location filename="downloadmanager.cpp" line="557"/>
<source>Wrong Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="546"/>
+ <location filename="downloadmanager.cpp" line="557"/>
<source>The download link is for a mod for &quot;%1&quot; but this instance of MO has been set up for &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="554"/>
<location filename="downloadmanager.cpp" line="565"/>
- <source>Already Started</source>
+ <source>There is already a download queued for this file.
+
+Mod %1
+File %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="554"/>
- <source>A download for this mod file has already been queued.</source>
+ <location filename="downloadmanager.cpp" line="571"/>
+ <source>Already Queued</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="565"/>
- <source>There is already a download started for this file (mod: %1, file: %2).</source>
+ <location filename="downloadmanager.cpp" line="580"/>
+ <source>There is already a download started for this file.
+
+Mod %1: %2
+File %3: %4</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadmanager.cpp" line="615"/>
+ <source>Already Started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="588"/>
- <location filename="downloadmanager.cpp" line="725"/>
+ <location filename="downloadmanager.cpp" line="643"/>
+ <location filename="downloadmanager.cpp" line="776"/>
<source>remove: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="607"/>
+ <location filename="downloadmanager.cpp" line="662"/>
<source>failed to delete %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="614"/>
+ <location filename="downloadmanager.cpp" line="669"/>
<source>failed to delete meta file for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="674"/>
+ <location filename="downloadmanager.cpp" line="729"/>
<source>restore: invalid download index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="747"/>
+ <location filename="downloadmanager.cpp" line="798"/>
<source>cancel: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="760"/>
+ <location filename="downloadmanager.cpp" line="811"/>
<source>pause: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="780"/>
+ <location filename="downloadmanager.cpp" line="833"/>
<source>resume: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="791"/>
+ <location filename="downloadmanager.cpp" line="844"/>
<source>resume (int): invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="815"/>
+ <location filename="downloadmanager.cpp" line="868"/>
<source>No known download urls. Sorry, this download can&apos;t be resumed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="856"/>
+ <location filename="downloadmanager.cpp" line="909"/>
+ <location filename="downloadmanager.cpp" line="961"/>
<source>query: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="878"/>
+ <location filename="downloadmanager.cpp" line="931"/>
<source>Please enter the nexus mod id</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="878"/>
+ <location filename="downloadmanager.cpp" line="931"/>
<source>Mod ID:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="888"/>
+ <location filename="downloadmanager.cpp" line="942"/>
<source>Please select the source game code for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="907"/>
+ <location filename="downloadmanager.cpp" line="1001"/>
<source>VisitNexus: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="928"/>
+ <location filename="downloadmanager.cpp" line="1022"/>
<source>Nexus ID for this Mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="935"/>
+ <location filename="downloadmanager.cpp" line="1029"/>
<source>OpenFile: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="952"/>
+ <location filename="downloadmanager.cpp" line="1046"/>
<source>OpenFileInDownloadsFolder: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="988"/>
+ <location filename="downloadmanager.cpp" line="1082"/>
<source>get pending: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="997"/>
+ <location filename="downloadmanager.cpp" line="1091"/>
<source>get path: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1006"/>
+ <location filename="downloadmanager.cpp" line="1100"/>
<source>Main</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1007"/>
+ <location filename="downloadmanager.cpp" line="1101"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1008"/>
+ <location filename="downloadmanager.cpp" line="1102"/>
<source>Optional</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1009"/>
+ <location filename="downloadmanager.cpp" line="1103"/>
<source>Old</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1010"/>
- <source>Misc</source>
+ <location filename="downloadmanager.cpp" line="1104"/>
+ <source>Miscellaneous</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1011"/>
+ <location filename="downloadmanager.cpp" line="1105"/>
+ <source>Deleted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadmanager.cpp" line="1106"/>
<source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1018"/>
+ <location filename="downloadmanager.cpp" line="1113"/>
<source>display name: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1038"/>
+ <location filename="downloadmanager.cpp" line="1133"/>
<source>file name: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1047"/>
+ <location filename="downloadmanager.cpp" line="1142"/>
<source>file time: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1061"/>
+ <location filename="downloadmanager.cpp" line="1156"/>
<source>file size: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1071"/>
+ <location filename="downloadmanager.cpp" line="1166"/>
<source>progress: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1081"/>
+ <location filename="downloadmanager.cpp" line="1176"/>
<source>state: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1091"/>
+ <location filename="downloadmanager.cpp" line="1186"/>
<source>infocomplete: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1106"/>
- <location filename="downloadmanager.cpp" line="1114"/>
+ <location filename="downloadmanager.cpp" line="1201"/>
+ <location filename="downloadmanager.cpp" line="1209"/>
<source>mod id: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1122"/>
+ <location filename="downloadmanager.cpp" line="1217"/>
<source>ishidden: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1131"/>
+ <location filename="downloadmanager.cpp" line="1226"/>
<source>file info: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1141"/>
+ <location filename="downloadmanager.cpp" line="1236"/>
<source>mark installed: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1186"/>
+ <location filename="downloadmanager.cpp" line="1281"/>
<source>mark uninstalled: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1359"/>
+ <location filename="downloadmanager.cpp" line="1458"/>
<source>Memory allocation error (in processing progress event).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1369"/>
+ <location filename="downloadmanager.cpp" line="1468"/>
<source>Memory allocation error (in processing downloaded data).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1495"/>
+ <location filename="downloadmanager.cpp" line="1584"/>
<source>Information updated</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1497"/>
- <location filename="downloadmanager.cpp" line="1511"/>
+ <location filename="downloadmanager.cpp" line="1586"/>
+ <location filename="downloadmanager.cpp" line="1601"/>
<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="1499"/>
+ <location filename="downloadmanager.cpp" line="1588"/>
<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="1628"/>
+ <location filename="downloadmanager.cpp" line="1716"/>
<source>No download server available. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1671"/>
+ <location filename="downloadmanager.cpp" line="1881"/>
<source>Failed to request file info from nexus: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1698"/>
+ <location filename="downloadmanager.cpp" line="1908"/>
<source>Warning: Content type is: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1703"/>
+ <location filename="downloadmanager.cpp" line="1913"/>
<source>Download header content length: %1 downloaded file size: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1705"/>
+ <location filename="downloadmanager.cpp" line="1915"/>
<source>Download failed: %1 (%2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1727"/>
+ <location filename="downloadmanager.cpp" line="1937"/>
<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="1810"/>
+ <location filename="downloadmanager.cpp" line="2020"/>
<source>failed to re-open %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1851"/>
+ <location filename="downloadmanager.cpp" line="2061"/>
<source>Unable to write download to drive (return %1).
Check the drive&apos;s available storage.
@@ -1143,92 +997,107 @@ Right now the only case I know of where this needs to be overwritten is for the
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="197"/>
+ <location filename="editexecutablesdialog.ui" line="199"/>
+ <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"/>
+ <source>Force Load Libraries (*)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.ui" line="225"/>
+ <source>Configure Libraries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.ui" line="234"/>
<source>Use Application&apos;s Icon for shortcuts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="204"/>
+ <location filename="editexecutablesdialog.ui" line="241"/>
<source>(*) This setting is profile-specific</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="213"/>
- <location filename="editexecutablesdialog.ui" line="216"/>
+ <location filename="editexecutablesdialog.ui" line="250"/>
+ <location filename="editexecutablesdialog.ui" line="253"/>
<source>Add an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="219"/>
- <location filename="editexecutablesdialog.cpp" line="217"/>
+ <location filename="editexecutablesdialog.ui" line="256"/>
+ <location filename="editexecutablesdialog.cpp" line="272"/>
<source>Add</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="230"/>
- <location filename="editexecutablesdialog.ui" line="233"/>
+ <location filename="editexecutablesdialog.ui" line="267"/>
+ <location filename="editexecutablesdialog.ui" line="270"/>
<source>Remove the selected executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="236"/>
+ <location filename="editexecutablesdialog.ui" line="273"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="264"/>
+ <location filename="editexecutablesdialog.ui" line="301"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="146"/>
+ <location filename="editexecutablesdialog.cpp" line="195"/>
<source>Select a binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="147"/>
+ <location filename="editexecutablesdialog.cpp" line="196"/>
<source>Executable (%1)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="172"/>
+ <location filename="editexecutablesdialog.cpp" line="221"/>
<source>Java (32-bit) required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="173"/>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="189"/>
+ <location filename="editexecutablesdialog.cpp" line="242"/>
<source>Select a directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="196"/>
+ <location filename="editexecutablesdialog.cpp" line="249"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="196"/>
+ <location filename="editexecutablesdialog.cpp" line="249"/>
<source>Really remove &quot;%1&quot; from executables?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="221"/>
+ <location filename="editexecutablesdialog.cpp" line="276"/>
<source>Modify</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="267"/>
- <location filename="editexecutablesdialog.cpp" line="287"/>
+ <location filename="editexecutablesdialog.cpp" line="337"/>
+ <location filename="editexecutablesdialog.cpp" line="357"/>
<source>Save Changes?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="268"/>
- <location filename="editexecutablesdialog.cpp" line="288"/>
+ <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>
<translation type="unfinished"></translation>
</message>
@@ -1281,6 +1150,85 @@ Right now the only case I know of where this needs to be overwritten is for the
</message>
</context>
<context>
+ <name>ForcedLoadDialog</name>
+ <message>
+ <location filename="forcedloaddialog.ui" line="14"/>
+ <source>Forced Load Settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialog.ui" line="45"/>
+ <location filename="forcedloaddialog.ui" line="48"/>
+ <source>Adds a row to the table.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialog.ui" line="51"/>
+ <source>Add Row</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialog.ui" line="58"/>
+ <location filename="forcedloaddialog.ui" line="61"/>
+ <source>Deletes the selected row from the table.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialog.ui" line="64"/>
+ <source>Delete Row</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ForcedLoadDialogWidget</name>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="23"/>
+ <location filename="forcedloaddialogwidget.ui" line="26"/>
+ <source>If checked, the specified library will be force loaded for the specified process.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="36"/>
+ <location filename="forcedloaddialogwidget.ui" line="39"/>
+ <source>The name of the process that should be forced to load a library.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="42"/>
+ <source>Process name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="67"/>
+ <location filename="forcedloaddialogwidget.ui" line="70"/>
+ <source>Browse for a process.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="73"/>
+ <location filename="forcedloaddialogwidget.ui" line="105"/>
+ <source>...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="80"/>
+ <location filename="forcedloaddialogwidget.ui" line="83"/>
+ <source>The path to the library to be loaded. This may be a path relative to the managed game&apos;s directory or may be an absolute path to somewhere else.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="86"/>
+ <source>Library to load</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="forcedloaddialogwidget.ui" line="99"/>
+ <location filename="forcedloaddialogwidget.ui" line="102"/>
+ <source>Browse for a library.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
<name>InstallDialog</name>
<message>
<location filename="installdialog.ui" line="20"/>
@@ -1345,99 +1293,99 @@ p, li { white-space: pre-wrap; }
<context>
<name>InstallationManager</name>
<message>
- <location filename="installationmanager.cpp" line="119"/>
+ <location filename="installationmanager.cpp" line="120"/>
<source>Password required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="120"/>
+ <location filename="installationmanager.cpp" line="121"/>
<source>Password</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="190"/>
- <location filename="installationmanager.cpp" line="289"/>
+ <location filename="installationmanager.cpp" line="191"/>
+ <location filename="installationmanager.cpp" line="290"/>
<source>Extracting files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="488"/>
+ <location filename="installationmanager.cpp" line="489"/>
<source>failed to create backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="497"/>
+ <location filename="installationmanager.cpp" line="498"/>
<source>Mod Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="497"/>
+ <location filename="installationmanager.cpp" line="498"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="550"/>
+ <location filename="installationmanager.cpp" line="551"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="551"/>
+ <location filename="installationmanager.cpp" line="552"/>
<source>The name you entered is invalid, please enter a different one.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="706"/>
+ <location filename="installationmanager.cpp" line="720"/>
<source>File format &quot;%1&quot; not supported</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="868"/>
+ <location filename="installationmanager.cpp" line="884"/>
<source>None of the available installer plugins were able to handle that archive.
This is likely due to a corrupted or incompatible download or unrecognized archive format.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="879"/>
+ <location filename="installationmanager.cpp" line="896"/>
<source>no error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="882"/>
+ <location filename="installationmanager.cpp" line="899"/>
<source>7z.dll not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="885"/>
+ <location filename="installationmanager.cpp" line="902"/>
<source>7z.dll isn&apos;t valid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="888"/>
+ <location filename="installationmanager.cpp" line="905"/>
<source>archive not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="891"/>
+ <location filename="installationmanager.cpp" line="908"/>
<source>failed to open archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="894"/>
+ <location filename="installationmanager.cpp" line="911"/>
<source>unsupported archive type</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="897"/>
+ <location filename="installationmanager.cpp" line="914"/>
<source>internal library error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="900"/>
+ <location filename="installationmanager.cpp" line="917"/>
<source>archive invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="904"/>
+ <location filename="installationmanager.cpp" line="921"/>
<source>unknown archive error</source>
<translation type="unfinished"></translation>
</message>
@@ -1481,7 +1429,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
<context>
<name>LogBuffer</name>
<message>
- <location filename="logbuffer.cpp" line="83"/>
+ <location filename="logbuffer.cpp" line="86"/>
<source>failed to write log to %1: %2</source>
<translation type="unfinished"></translation>
</message>
@@ -1512,17 +1460,17 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="130"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="142"/>
<source>failed to write to %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="161"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="178"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="186"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="203"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
@@ -1547,7 +1495,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
<name>MainWindow</name>
<message>
<location filename="mainwindow.ui" line="46"/>
- <location filename="mainwindow.ui" line="596"/>
+ <location filename="mainwindow.ui" line="565"/>
<source>Categories</source>
<translation type="unfinished"></translation>
</message>
@@ -1613,61 +1561,83 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="mainwindow.ui" line="273"/>
- <location filename="mainwindow.ui" line="859"/>
+ <location filename="mainwindow.ui" line="831"/>
<source>Restore Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="287"/>
- <location filename="mainwindow.ui" line="879"/>
- <location filename="mainwindow.cpp" line="4047"/>
+ <location filename="mainwindow.ui" line="851"/>
+ <location filename="mainwindow.cpp" line="4663"/>
<source>Create Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="403"/>
+ <location filename="mainwindow.ui" line="301"/>
+ <location filename="mainwindow.ui" line="865"/>
+ <source>Active:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="314"/>
+ <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"/>
<source>List of available mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="406"/>
+ <location filename="mainwindow.ui" line="344"/>
<source>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag &amp; drop mods to change their &quot;installation&quot; orders.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="494"/>
+ <location filename="mainwindow.ui" line="432"/>
+ <location filename="mainwindow.ui" line="584"/>
+ <location filename="mainwindow.ui" line="966"/>
+ <location filename="mainwindow.ui" line="1302"/>
<source>Filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="551"/>
- <source>Clear all Filters</source>
+ <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="591"/>
- <source>No groups</source>
+ <location filename="mainwindow.ui" line="465"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This tracks the number of queued Nexus API requests on the left (&lt;span style=&quot; font-weight:600;&quot;&gt;Q&lt;/span&gt;) and the remaining daily (&lt;span style=&quot; font-weight:600;&quot;&gt;D&lt;/span&gt;) and hourly (&lt;span style=&quot; font-weight:600;&quot;&gt;H&lt;/span&gt;) 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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="601"/>
- <source>Nexus IDs</source>
+ <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="615"/>
- <location filename="mainwindow.ui" line="1002"/>
- <location filename="mainwindow.ui" line="1341"/>
- <source>Namefilter</source>
+ <location filename="mainwindow.ui" line="520"/>
+ <source>Clear all Filters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="650"/>
+ <location filename="mainwindow.ui" line="560"/>
+ <source>No groups</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="570"/>
+ <source>Nexus IDs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="619"/>
<source>Pick a program to run.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="653"/>
+ <location filename="mainwindow.ui" line="622"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1677,12 +1647,12 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="701"/>
+ <location filename="mainwindow.ui" line="670"/>
<source>Run program</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="704"/>
+ <location filename="mainwindow.ui" line="673"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1691,17 +1661,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="714"/>
+ <location filename="mainwindow.ui" line="683"/>
<source>Run</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="755"/>
+ <location filename="mainwindow.ui" line="724"/>
<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="758"/>
+ <location filename="mainwindow.ui" line="727"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1710,27 +1680,32 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="765"/>
+ <location filename="mainwindow.ui" line="734"/>
<source>Shortcut</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="815"/>
+ <location filename="mainwindow.ui" line="784"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="835"/>
+ <location filename="mainwindow.ui" line="807"/>
<source>Sort</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="941"/>
+ <location filename="mainwindow.ui" line="878"/>
+ <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"/>
<source>List of available esp/esm files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="944"/>
+ <location filename="mainwindow.ui" line="908"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1739,27 +1714,27 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1015"/>
+ <location filename="mainwindow.ui" line="979"/>
<source>Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1035"/>
+ <location filename="mainwindow.ui" line="999"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. &lt;p&gt;Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).&lt;/p&gt;&lt;p&gt;If there is a matching plugin, the game will load them no matter what.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1038"/>
+ <location filename="mainwindow.ui" line="1002"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;What is an archive?&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1053"/>
+ <location filename="mainwindow.ui" line="1017"/>
<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="1056"/>
+ <location filename="mainwindow.ui" line="1020"/>
<source>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they &quot;compete&quot; 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!
@@ -1767,61 +1742,72 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1090"/>
+ <location filename="mainwindow.ui" line="1049"/>
<source>Data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1108"/>
+ <location filename="mainwindow.ui" line="1067"/>
<source>refresh data-directory overview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1111"/>
+ <location filename="mainwindow.ui" line="1070"/>
<source>Refresh the overview. This may take a moment.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1114"/>
- <location filename="mainwindow.ui" line="1237"/>
- <location filename="mainwindow.cpp" line="3922"/>
- <location filename="mainwindow.cpp" line="4825"/>
+ <location filename="mainwindow.ui" line="1073"/>
+ <location filename="mainwindow.ui" line="1216"/>
+ <location filename="mainwindow.cpp" line="4536"/>
+ <location filename="mainwindow.cpp" line="5478"/>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1130"/>
+ <location filename="mainwindow.ui" line="1089"/>
<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="1143"/>
+ <location filename="mainwindow.ui" line="1102"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1148"/>
+ <location filename="mainwindow.ui" line="1107"/>
<source>Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1158"/>
- <location filename="mainwindow.ui" line="1161"/>
- <source>Filter the above list so that only conflicts are displayed.</source>
+ <location filename="mainwindow.ui" line="1119"/>
+ <location filename="mainwindow.ui" line="1122"/>
+ <source>Filters the above list so that only conflicts are displayed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1164"/>
+ <location filename="mainwindow.ui" line="1125"/>
<source>Show only conflicts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1172"/>
+ <location filename="mainwindow.ui" line="1132"/>
+ <location filename="mainwindow.ui" line="1138"/>
+ <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"/>
+ <source>Show files from Archives</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="1151"/>
<source>Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1196"/>
+ <location filename="mainwindow.ui" line="1175"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1832,996 +1818,1097 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1216"/>
+ <location filename="mainwindow.ui" line="1195"/>
<source>Downloads</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1234"/>
+ <location filename="mainwindow.ui" line="1213"/>
<source>Refresh downloads view</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1265"/>
+ <location filename="mainwindow.ui" line="1244"/>
<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="1321"/>
+ <location filename="mainwindow.ui" line="1282"/>
<source>Show Hidden</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1386"/>
+ <location filename="mainwindow.ui" line="1347"/>
<source>Tool Bar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1429"/>
+ <location filename="mainwindow.ui" line="1390"/>
<source>Install Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1432"/>
+ <location filename="mainwindow.ui" line="1393"/>
<source>Install &amp;Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1435"/>
+ <location filename="mainwindow.ui" line="1396"/>
<source>Install a new mod from an archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1438"/>
+ <location filename="mainwindow.ui" line="1399"/>
<source>Ctrl+M</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1447"/>
+ <location filename="mainwindow.ui" line="1408"/>
<source>Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1450"/>
+ <location filename="mainwindow.ui" line="1411"/>
<source>&amp;Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1453"/>
+ <location filename="mainwindow.ui" line="1414"/>
<source>Configure Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1456"/>
+ <location filename="mainwindow.ui" line="1417"/>
<source>Ctrl+P</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1465"/>
+ <location filename="mainwindow.ui" line="1426"/>
<source>Executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1468"/>
+ <location filename="mainwindow.ui" line="1429"/>
<source>&amp;Executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1471"/>
+ <location filename="mainwindow.ui" line="1432"/>
<source>Configure the executables that can be started through Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1474"/>
+ <location filename="mainwindow.ui" line="1435"/>
<source>Ctrl+E</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1483"/>
- <location filename="mainwindow.ui" line="1489"/>
+ <location filename="mainwindow.ui" line="1444"/>
+ <location filename="mainwindow.ui" line="1450"/>
<source>Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1486"/>
+ <location filename="mainwindow.ui" line="1447"/>
<source>&amp;Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1492"/>
+ <location filename="mainwindow.ui" line="1453"/>
<source>Ctrl+I</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1501"/>
+ <location filename="mainwindow.ui" line="1462"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1504"/>
+ <location filename="mainwindow.ui" line="1465"/>
<source>&amp;Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1507"/>
+ <location filename="mainwindow.ui" line="1468"/>
<source>Configure settings and workarounds</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1510"/>
+ <location filename="mainwindow.ui" line="1471"/>
<source>Ctrl+S</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1519"/>
+ <location filename="mainwindow.ui" line="1480"/>
<source>Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1522"/>
+ <location filename="mainwindow.ui" line="1483"/>
<source>Search nexus network for more mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1525"/>
+ <location filename="mainwindow.ui" line="1486"/>
<source>Ctrl+N</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1537"/>
- <location filename="mainwindow.cpp" line="4765"/>
+ <location filename="mainwindow.ui" line="1498"/>
+ <location filename="mainwindow.cpp" line="5427"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1540"/>
+ <location filename="mainwindow.ui" line="1501"/>
<source>Mod Organizer is up-to-date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1552"/>
- <location filename="mainwindow.cpp" line="645"/>
- <source>No Problems</source>
+ <location filename="mainwindow.ui" line="1513"/>
+ <location filename="mainwindow.cpp" line="690"/>
+ <source>No Notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1555"/>
- <source>This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them.
-
-!Work in progress!
-Right now this has very limited functionality</source>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1567"/>
- <location filename="mainwindow.ui" line="1570"/>
+ <location filename="mainwindow.ui" line="1525"/>
+ <location filename="mainwindow.ui" line="1528"/>
<source>Help</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1573"/>
+ <location filename="mainwindow.ui" line="1531"/>
<source>Ctrl+H</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1582"/>
+ <location filename="mainwindow.ui" line="1540"/>
<source>Endorse MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1585"/>
- <location filename="mainwindow.cpp" line="4848"/>
+ <location filename="mainwindow.ui" line="1543"/>
+ <location filename="mainwindow.cpp" line="5501"/>
<source>Endorse Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1590"/>
+ <location filename="mainwindow.ui" line="1548"/>
<source>Copy Log to Clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1599"/>
+ <location filename="mainwindow.ui" line="1557"/>
<source>Change Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1602"/>
+ <location filename="mainwindow.ui" line="1560"/>
<source>Open the Instance selection dialog to manage a different Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="329"/>
+ <location filename="mainwindow.cpp" line="337"/>
<source>Toolbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="330"/>
+ <location filename="mainwindow.cpp" line="338"/>
<source>Desktop</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="331"/>
+ <location filename="mainwindow.cpp" line="339"/>
<source>Start Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="356"/>
+ <location filename="mainwindow.cpp" line="364"/>
<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="633"/>
- <source>Problems</source>
+ <location filename="mainwindow.cpp" line="510"/>
+ <source>Crash on exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="511"/>
+ <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="634"/>
- <source>There are potential problems with your setup</source>
+ <location filename="mainwindow.cpp" line="679"/>
+ <source>There are notifications to read</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="646"/>
- <source>Everything seems to be in order</source>
+ <location filename="mainwindow.cpp" line="691"/>
+ <source>There are no notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="708"/>
+ <location filename="mainwindow.cpp" line="758"/>
+ <location filename="mainwindow.cpp" line="4673"/>
+ <location filename="mainwindow.cpp" line="4677"/>
+ <source>Endorse</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="762"/>
+ <source>Won&apos;t Endorse</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="777"/>
<source>Help on UI</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="712"/>
- <source>Documentation Wiki</source>
+ <location filename="mainwindow.cpp" line="781"/>
+ <source>Documentation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="716"/>
+ <location filename="mainwindow.cpp" line="785"/>
+ <source>Chat on Discord</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="789"/>
<source>Report Issue</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="720"/>
+ <location filename="mainwindow.cpp" line="793"/>
<source>Tutorials</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="759"/>
+ <location filename="mainwindow.cpp" line="832"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="760"/>
+ <location filename="mainwindow.cpp" line="833"/>
<source>About Qt</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="812"/>
+ <location filename="mainwindow.cpp" line="892"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="813"/>
+ <location filename="mainwindow.cpp" line="893"/>
<source>Please enter a name for the new profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="821"/>
+ <location filename="mainwindow.cpp" line="901"/>
<source>failed to create profile: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="865"/>
+ <location filename="mainwindow.cpp" line="945"/>
<source>Show tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="866"/>
+ <location filename="mainwindow.cpp" line="946"/>
<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 &quot;Help&quot;-menu.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="902"/>
+ <location filename="mainwindow.cpp" line="982"/>
<source>Downloads in progress</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="903"/>
+ <location filename="mainwindow.cpp" line="983"/>
<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="1020"/>
+ <location filename="mainwindow.cpp" line="1100"/>
<source>Plugin &quot;%1&quot; failed: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1022"/>
+ <location filename="mainwindow.cpp" line="1102"/>
<source>Plugin &quot;%1&quot; failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1100"/>
+ <location filename="mainwindow.cpp" line="1186"/>
<source>Browse Mod Page</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1296"/>
+ <location filename="mainwindow.cpp" line="1393"/>
<source>Also in: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1307"/>
+ <location filename="mainwindow.cpp" line="1404"/>
<source>No conflict</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1393"/>
+ <location filename="mainwindow.cpp" line="1489"/>
<source>&lt;Edit...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1671"/>
+ <location filename="mainwindow.cpp" line="1767"/>
<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="1733"/>
+ <location filename="mainwindow.cpp" line="1829"/>
<source>Activating Network Proxy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1794"/>
- <source>Notice: Your current MO version (%1) is lower than the previous version (%2).&lt;br&gt;The GUI may not downgrade gracefully, so you may experience oddities.&lt;br&gt;However, there should be no serious issues.</source>
+ <location filename="mainwindow.cpp" line="1905"/>
+ <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="1898"/>
+ <location filename="mainwindow.cpp" line="2007"/>
<source>Choose Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1899"/>
+ <location filename="mainwindow.cpp" line="2008"/>
<source>Mod Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2074"/>
+ <location filename="mainwindow.cpp" line="2195"/>
<source>Start Tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2075"/>
+ <location filename="mainwindow.cpp" line="2196"/>
<source>You&apos;re about to start a tutorial. For technical reasons it&apos;s not possible to end the tutorial early. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2229"/>
+ <location filename="mainwindow.cpp" line="2374"/>
<source>failed to spawn notepad.exe: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2269"/>
+ <location filename="mainwindow.cpp" line="2414"/>
<source>failed to change origin name: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2293"/>
+ <location filename="mainwindow.cpp" line="2438"/>
<source>failed to move &quot;%1&quot; from mod &quot;%2&quot; to &quot;%3&quot;: %4</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2317"/>
+ <location filename="mainwindow.cpp" line="2462"/>
<source>&lt;Contains %1&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2352"/>
+ <location filename="mainwindow.cpp" line="2497"/>
<source>&lt;Checked&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2353"/>
+ <location filename="mainwindow.cpp" line="2498"/>
<source>&lt;Unchecked&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2354"/>
+ <location filename="mainwindow.cpp" line="2499"/>
<source>&lt;Update&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2355"/>
+ <location filename="mainwindow.cpp" line="2500"/>
<source>&lt;Mod Backup&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2356"/>
+ <location filename="mainwindow.cpp" line="2501"/>
<source>&lt;Managed by MO&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2357"/>
+ <location filename="mainwindow.cpp" line="2502"/>
<source>&lt;Managed outside MO&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2358"/>
+ <location filename="mainwindow.cpp" line="2503"/>
<source>&lt;No category&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2359"/>
+ <location filename="mainwindow.cpp" line="2504"/>
<source>&lt;Conflicted&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2360"/>
+ <location filename="mainwindow.cpp" line="2505"/>
<source>&lt;Not Endorsed&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2406"/>
+ <location filename="mainwindow.cpp" line="2551"/>
<source>failed to rename mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2419"/>
+ <location filename="mainwindow.cpp" line="2564"/>
<source>Overwrite?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2420"/>
+ <location filename="mainwindow.cpp" line="2565"/>
<source>This will replace the existing mod &quot;%1&quot;. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2423"/>
+ <location filename="mainwindow.cpp" line="2568"/>
<source>failed to remove mod &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2427"/>
- <location filename="mainwindow.cpp" line="4597"/>
- <location filename="mainwindow.cpp" line="4621"/>
+ <location filename="mainwindow.cpp" line="2572"/>
+ <location filename="mainwindow.cpp" line="5253"/>
+ <location filename="mainwindow.cpp" line="5277"/>
<source>failed to rename &quot;%1&quot; to &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2487"/>
- <location filename="mainwindow.cpp" line="3626"/>
- <location filename="mainwindow.cpp" line="3634"/>
- <location filename="mainwindow.cpp" line="4172"/>
+ <location filename="mainwindow.cpp" line="2644"/>
+ <location filename="mainwindow.cpp" line="4239"/>
+ <location filename="mainwindow.cpp" line="4247"/>
+ <location filename="mainwindow.cpp" line="4802"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2488"/>
+ <location filename="mainwindow.cpp" line="2645"/>
<source>Remove the following mods?&lt;br&gt;&lt;ul&gt;%1&lt;/ul&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2501"/>
+ <location filename="mainwindow.cpp" line="2660"/>
<source>failed to remove mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2533"/>
- <location filename="mainwindow.cpp" line="2536"/>
- <location filename="mainwindow.cpp" line="2546"/>
+ <location filename="mainwindow.cpp" line="2692"/>
+ <location filename="mainwindow.cpp" line="2695"/>
+ <location filename="mainwindow.cpp" line="2705"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2533"/>
+ <location filename="mainwindow.cpp" line="2692"/>
<source>Installation file no longer exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2537"/>
+ <location filename="mainwindow.cpp" line="2696"/>
<source>Mods installed with old versions of MO can&apos;t be reinstalled in this way.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2547"/>
+ <location filename="mainwindow.cpp" line="2706"/>
<source>Failed to create backup.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2564"/>
+ <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="2580"/>
- <location filename="mainwindow.cpp" line="2607"/>
+ <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="2663"/>
+ <location filename="mainwindow.cpp" line="2750"/>
+ <location filename="mainwindow.cpp" line="2758"/>
+ <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"/>
+ <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"/>
<source>Failed to display overwrite dialog: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2851"/>
+ <location filename="mainwindow.cpp" line="3139"/>
+ <source>Opening Nexus Links</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="3140"/>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2861"/>
+ <location filename="mainwindow.cpp" line="3181"/>
+ <source>Opening Web Pages</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="3182"/>
+ <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="2988"/>
- <location filename="mainwindow.cpp" line="3125"/>
- <location filename="mainwindow.cpp" line="3984"/>
+ <location filename="mainwindow.cpp" line="3398"/>
+ <source>&lt;table cellspacing=&quot;5&quot;&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;All&lt;/th&gt;&lt;th&gt;Visible&lt;/th&gt;&lt;tr&gt;&lt;td&gt;Enabled mods:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%1 / %2&lt;/td&gt;&lt;td align=right&gt;%3 / %4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Unmanaged/DLCs:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%5&lt;/td&gt;&lt;td align=right&gt;%6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mod backups:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%7&lt;/td&gt;&lt;td align=right&gt;%8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Separators:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%9&lt;/td&gt;&lt;td align=right&gt;%10&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="3453"/>
+ <source>&lt;table cellspacing=&quot;6&quot;&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Active &lt;/th&gt;&lt;th&gt;Total&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;All plugins:&lt;/td&gt;&lt;td align=right&gt;%1 &lt;/td&gt;&lt;td align=right&gt;%2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESMs:&lt;/td&gt;&lt;td align=right&gt;%3 &lt;/td&gt;&lt;td align=right&gt;%4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESPs:&lt;/td&gt;&lt;td align=right&gt;%7 &lt;/td&gt;&lt;td align=right&gt;%8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESMs+ESPs:&lt;/td&gt;&lt;td align=right&gt;%9 &lt;/td&gt;&lt;td align=right&gt;%10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESLs:&lt;/td&gt;&lt;td align=right&gt;%5 &lt;/td&gt;&lt;td align=right&gt;%6&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</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"/>
<source>Create Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2989"/>
+ <location filename="mainwindow.cpp" line="3486"/>
<source>This will create an empty mod.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2998"/>
- <location filename="mainwindow.cpp" line="3135"/>
+ <location filename="mainwindow.cpp" line="3495"/>
+ <location filename="mainwindow.cpp" line="3633"/>
<source>A mod with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3026"/>
+ <location filename="mainwindow.cpp" line="3523"/>
<source>Create Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3027"/>
+ <location filename="mainwindow.cpp" line="3524"/>
<source>This will create a new separator.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3034"/>
+ <location filename="mainwindow.cpp" line="3531"/>
<source>A separator with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3126"/>
+ <location filename="mainwindow.cpp" line="3624"/>
<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="3167"/>
- <location filename="mainwindow.cpp" line="5165"/>
- <source>Are you sure?</source>
+ <location filename="mainwindow.cpp" line="3705"/>
+ <source>Move successful.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3168"/>
- <source>About to recursively delete:
-</source>
+ <location filename="mainwindow.cpp" line="3726"/>
+ <location filename="mainwindow.cpp" line="6085"/>
+ <source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3520"/>
- <source>Not logged in, endorsement information will be wrong</source>
+ <location filename="mainwindow.cpp" line="3727"/>
+ <source>About to recursively delete:
+</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3528"/>
+ <location filename="mainwindow.cpp" line="4121"/>
<source>Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3529"/>
+ <location filename="mainwindow.cpp" line="4122"/>
<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="3549"/>
- <location filename="mainwindow.cpp" line="4733"/>
+ <location filename="mainwindow.cpp" line="4142"/>
+ <location filename="mainwindow.cpp" line="5395"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3550"/>
+ <location filename="mainwindow.cpp" line="4143"/>
<source>I don&apos;t know a versioning scheme where %1 is newer than %2.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3626"/>
+ <location filename="mainwindow.cpp" line="4239"/>
<source>Really enable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3634"/>
+ <location filename="mainwindow.cpp" line="4247"/>
<source>Really disable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3714"/>
+ <location filename="mainwindow.cpp" line="4327"/>
<source>Export to csv</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3717"/>
+ <location filename="mainwindow.cpp" line="4330"/>
<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="3720"/>
+ <location filename="mainwindow.cpp" line="4333"/>
<source>Select what mods you want export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3721"/>
+ <location filename="mainwindow.cpp" line="4334"/>
<source>All installed mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3722"/>
+ <location filename="mainwindow.cpp" line="4335"/>
<source>Only active (checked) mods from your current profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3723"/>
+ <location filename="mainwindow.cpp" line="4336"/>
<source>All currently visible mods in the mod list</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3744"/>
+ <location filename="mainwindow.cpp" line="4357"/>
<source>Choose what Columns to export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3747"/>
+ <location filename="mainwindow.cpp" line="4360"/>
<source>Mod_Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3749"/>
+ <location filename="mainwindow.cpp" line="4362"/>
<source>Mod_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3751"/>
+ <location filename="mainwindow.cpp" line="4364"/>
+ <source>Notes_column</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4365"/>
<source>Mod_Status</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3752"/>
+ <location filename="mainwindow.cpp" line="4366"/>
<source>Primary_Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3753"/>
+ <location filename="mainwindow.cpp" line="4367"/>
<source>Nexus_ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3754"/>
+ <location filename="mainwindow.cpp" line="4368"/>
<source>Mod_Nexus_URL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3755"/>
+ <location filename="mainwindow.cpp" line="4369"/>
<source>Mod_Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3756"/>
+ <location filename="mainwindow.cpp" line="4370"/>
<source>Install_Date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3757"/>
+ <location filename="mainwindow.cpp" line="4371"/>
<source>Download_File_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3859"/>
+ <location filename="mainwindow.cpp" line="4478"/>
<source>export failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3878"/>
+ <location filename="mainwindow.cpp" line="4497"/>
<source>Open Game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3880"/>
+ <location filename="mainwindow.cpp" line="4499"/>
<source>Open MyGames folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3882"/>
+ <location filename="mainwindow.cpp" line="4501"/>
<source>Open INIs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3886"/>
+ <location filename="mainwindow.cpp" line="4505"/>
<source>Open Instance folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3888"/>
+ <location filename="mainwindow.cpp" line="4507"/>
<source>Open Mods folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3890"/>
+ <location filename="mainwindow.cpp" line="4509"/>
<source>Open Profile folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3892"/>
+ <location filename="mainwindow.cpp" line="4511"/>
<source>Open Downloads folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3896"/>
+ <location filename="mainwindow.cpp" line="4515"/>
<source>Open MO2 Install folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3898"/>
+ <location filename="mainwindow.cpp" line="4517"/>
<source>Open MO2 Plugins folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3900"/>
+ <location filename="mainwindow.cpp" line="4519"/>
<source>Open MO2 Logs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3909"/>
+ <location filename="mainwindow.cpp" line="4527"/>
<source>Install Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3911"/>
+ <location filename="mainwindow.cpp" line="4528"/>
<source>Create empty mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3913"/>
+ <location filename="mainwindow.cpp" line="4529"/>
<source>Create Separator</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3917"/>
+ <location filename="mainwindow.cpp" line="4533"/>
<source>Enable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3918"/>
+ <location filename="mainwindow.cpp" line="4534"/>
<source>Disable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3920"/>
- <source>Check all for update</source>
+ <location filename="mainwindow.cpp" line="4535"/>
+ <source>Check for updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3924"/>
+ <location filename="mainwindow.cpp" line="4537"/>
<source>Export to csv...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3936"/>
- <location filename="mainwindow.cpp" line="3952"/>
+ <location filename="mainwindow.cpp" line="4546"/>
+ <location filename="mainwindow.cpp" line="4562"/>
<source>Send to</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3937"/>
- <location filename="mainwindow.cpp" line="3953"/>
+ <location filename="mainwindow.cpp" line="4547"/>
+ <location filename="mainwindow.cpp" line="4563"/>
<source>Top</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3938"/>
- <location filename="mainwindow.cpp" line="3954"/>
+ <location filename="mainwindow.cpp" line="4548"/>
+ <location filename="mainwindow.cpp" line="4564"/>
<source>Bottom</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3939"/>
- <location filename="mainwindow.cpp" line="3955"/>
+ <location filename="mainwindow.cpp" line="4549"/>
+ <location filename="mainwindow.cpp" line="4565"/>
<source>Priority...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3940"/>
+ <location filename="mainwindow.cpp" line="4550"/>
<source>Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3976"/>
+ <location filename="mainwindow.cpp" line="4589"/>
<source>All Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3983"/>
+ <location filename="mainwindow.cpp" line="4597"/>
<source>Sync to Mods...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3985"/>
+ <location filename="mainwindow.cpp" line="4599"/>
+ <source>Move content to Mod...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4600"/>
<source>Clear Overwrite...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3987"/>
- <location filename="mainwindow.cpp" line="4088"/>
+ <location filename="mainwindow.cpp" line="4602"/>
+ <location filename="mainwindow.cpp" line="4720"/>
<source>Open in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3989"/>
+ <location filename="mainwindow.cpp" line="4604"/>
<source>Restore Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3990"/>
+ <location filename="mainwindow.cpp" line="4605"/>
<source>Remove Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3993"/>
- <location filename="mainwindow.cpp" line="4012"/>
+ <location filename="mainwindow.cpp" line="4608"/>
+ <location filename="mainwindow.cpp" line="4627"/>
<source>Change Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3997"/>
- <location filename="mainwindow.cpp" line="4017"/>
+ <location filename="mainwindow.cpp" line="4612"/>
+ <location filename="mainwindow.cpp" line="4632"/>
<source>Primary Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4001"/>
+ <location filename="mainwindow.cpp" line="4616"/>
<source>Rename Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4002"/>
+ <location filename="mainwindow.cpp" line="4617"/>
<source>Remove Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4005"/>
+ <location filename="mainwindow.cpp" line="4620"/>
<source>Select Color...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4007"/>
+ <location filename="mainwindow.cpp" line="4622"/>
<source>Reset Color</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4024"/>
+ <location filename="mainwindow.cpp" line="4639"/>
<source>Change versioning scheme</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4029"/>
+ <location filename="mainwindow.cpp" line="4643"/>
+ <source>Force-check updates</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4645"/>
<source>Un-ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4031"/>
+ <location filename="mainwindow.cpp" line="4648"/>
<source>Ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4037"/>
- <location filename="mainwindow.cpp" line="5276"/>
+ <location filename="mainwindow.cpp" line="4653"/>
+ <location filename="mainwindow.cpp" line="6198"/>
<source>Enable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4038"/>
- <location filename="mainwindow.cpp" line="5277"/>
+ <location filename="mainwindow.cpp" line="4654"/>
+ <location filename="mainwindow.cpp" line="6199"/>
<source>Disable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4044"/>
+ <location filename="mainwindow.cpp" line="4660"/>
<source>Rename Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4045"/>
+ <location filename="mainwindow.cpp" line="4661"/>
<source>Reinstall Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4046"/>
+ <location filename="mainwindow.cpp" line="4662"/>
<source>Remove Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4054"/>
+ <location filename="mainwindow.cpp" line="4670"/>
<source>Un-Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4057"/>
- <location filename="mainwindow.cpp" line="4061"/>
- <source>Endorse</source>
+ <location filename="mainwindow.cpp" line="4674"/>
+ <source>Won&apos;t endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4058"/>
- <source>Won&apos;t endorse</source>
+ <location filename="mainwindow.cpp" line="4680"/>
+ <source>Endorsement state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4064"/>
- <source>Endorsement state unknown</source>
+ <location filename="mainwindow.cpp" line="4690"/>
+ <source>Start tracking</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4693"/>
+ <source>Stop tracking</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4696"/>
+ <source>Tracked state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4075"/>
+ <location filename="mainwindow.cpp" line="4707"/>
<source>Ignore missing data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4079"/>
+ <location filename="mainwindow.cpp" line="4711"/>
<source>Mark as converted/working</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4083"/>
+ <location filename="mainwindow.cpp" line="4715"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4085"/>
+ <location filename="mainwindow.cpp" line="4717"/>
<source>Visit web page</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4092"/>
+ <location filename="mainwindow.cpp" line="4724"/>
<source>Information...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4099"/>
- <location filename="mainwindow.cpp" line="5324"/>
+ <location filename="mainwindow.cpp" line="4731"/>
+ <location filename="mainwindow.cpp" line="6251"/>
<source>Exception: </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4101"/>
- <location filename="mainwindow.cpp" line="5326"/>
+ <location filename="mainwindow.cpp" line="4733"/>
+ <location filename="mainwindow.cpp" line="6253"/>
<source>Unknown exception</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4132"/>
+ <location filename="mainwindow.cpp" line="4762"/>
<source>&lt;All&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4134"/>
+ <location filename="mainwindow.cpp" line="4764"/>
<source>&lt;Multiple&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4169"/>
+ <location filename="mainwindow.cpp" line="4799"/>
<source>%1 more</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="4173"/>
+ <location filename="mainwindow.cpp" line="4803"/>
<source>Are you sure you want to remove the following %n save(s)?&lt;br&gt;&lt;ul&gt;%1&lt;/ul&gt;&lt;br&gt;Removed saves will be sent to the Recycle Bin.</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -2829,12 +2916,12 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4218"/>
+ <location filename="mainwindow.cpp" line="4848"/>
<source>Enable Mods...</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="4233"/>
+ <location filename="mainwindow.cpp" line="4863"/>
<source>Delete %n save(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -2842,22 +2929,22 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4275"/>
+ <location filename="mainwindow.cpp" line="4905"/>
<source>failed to remove %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4297"/>
+ <location filename="mainwindow.cpp" line="4927"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4327"/>
+ <location filename="mainwindow.cpp" line="4957"/>
<source>Restarting MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4328"/>
+ <location filename="mainwindow.cpp" line="4958"/>
<source>Changing the managed game directory requires restarting MO.
Any pending downloads will be paused.
@@ -2865,335 +2952,373 @@ Click OK to restart MO now.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4348"/>
+ <location filename="mainwindow.cpp" line="4978"/>
<source>Can&apos;t change download directory while downloads are in progress!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4469"/>
+ <location filename="mainwindow.cpp" line="5124"/>
<source>failed to write to file %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4475"/>
+ <location filename="mainwindow.cpp" line="5130"/>
<source>%1 written</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4516"/>
+ <location filename="mainwindow.cpp" line="5172"/>
<source>Select binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4516"/>
+ <location filename="mainwindow.cpp" line="5172"/>
<source>Binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4542"/>
+ <location filename="mainwindow.cpp" line="5198"/>
<source>Enter Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4543"/>
+ <location filename="mainwindow.cpp" line="5199"/>
<source>Please enter a name for the executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4557"/>
+ <location filename="mainwindow.cpp" line="5213"/>
<source>Not an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4557"/>
+ <location filename="mainwindow.cpp" line="5213"/>
<source>This is not a recognized executable.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4582"/>
- <location filename="mainwindow.cpp" line="4607"/>
+ <location filename="mainwindow.cpp" line="5238"/>
+ <location filename="mainwindow.cpp" line="5263"/>
<source>Replace file?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4582"/>
+ <location filename="mainwindow.cpp" line="5238"/>
<source>There already is a hidden version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4585"/>
- <location filename="mainwindow.cpp" line="4610"/>
+ <location filename="mainwindow.cpp" line="5241"/>
+ <location filename="mainwindow.cpp" line="5266"/>
<source>File operation failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4585"/>
- <location filename="mainwindow.cpp" line="4610"/>
+ <location filename="mainwindow.cpp" line="5241"/>
+ <location filename="mainwindow.cpp" line="5266"/>
<source>Failed to remove &quot;%1&quot;. Maybe you lack the required file permissions?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4607"/>
+ <location filename="mainwindow.cpp" line="5263"/>
<source>There already is a visible version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4651"/>
- <location filename="mainwindow.cpp" line="5937"/>
+ <location filename="mainwindow.cpp" line="5307"/>
+ <location filename="mainwindow.cpp" line="6865"/>
<source>Set Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4651"/>
+ <location filename="mainwindow.cpp" line="5307"/>
<source>Set the priority of the selected plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4700"/>
+ <location filename="mainwindow.cpp" line="5362"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4713"/>
+ <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="4733"/>
+ <location filename="mainwindow.cpp" line="5395"/>
<source>Sorry, can&apos;t preview anything. This function currently does not support extracting from bsas.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4767"/>
+ <location filename="mainwindow.cpp" line="5429"/>
<source>Update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4805"/>
+ <location filename="mainwindow.cpp" line="5458"/>
<source>Open/Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4806"/>
+ <location filename="mainwindow.cpp" line="5459"/>
<source>Add as Executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4810"/>
+ <location filename="mainwindow.cpp" line="5463"/>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4816"/>
+ <location filename="mainwindow.cpp" line="5469"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4818"/>
+ <location filename="mainwindow.cpp" line="5471"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4824"/>
+ <location filename="mainwindow.cpp" line="5477"/>
<source>Write To File...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4849"/>
+ <location filename="mainwindow.cpp" line="5502"/>
<source>Do you want to endorse Mod Organizer on %1 now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4974"/>
+ <location filename="mainwindow.cpp" line="5516"/>
+ <source>Abstain from Endorsing Mod Organizer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5517"/>
+ <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"/>
+ <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"/>
+ <source>Please reconsider endorsing MO2 on Nexus!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5837"/>
<source>Thank you!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4974"/>
+ <location filename="mainwindow.cpp" line="5837"/>
<source>Thank you for your endorsement!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5009"/>
+ <location filename="mainwindow.cpp" line="5840"/>
+ <source>Okay.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5840"/>
+ <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"/>
+ <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"/>
<source>Request to Nexus failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5024"/>
- <location filename="mainwindow.cpp" line="5086"/>
+ <location filename="mainwindow.cpp" line="5937"/>
+ <location filename="mainwindow.cpp" line="5999"/>
<source>failed to read %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5036"/>
- <location filename="mainwindow.cpp" line="5514"/>
+ <location filename="mainwindow.cpp" line="5949"/>
+ <location filename="mainwindow.cpp" line="6441"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5036"/>
+ <location filename="mainwindow.cpp" line="5949"/>
<source>failed to extract %1 (errorcode %2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5068"/>
+ <location filename="mainwindow.cpp" line="5981"/>
<source>Extract BSA</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5097"/>
+ <location filename="mainwindow.cpp" line="6010"/>
<source>This archive contains invalid hashes. Some files may be broken.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5143"/>
+ <location filename="mainwindow.cpp" line="6056"/>
<source>Extract...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5166"/>
+ <location filename="mainwindow.cpp" line="6086"/>
<source>This will restart MO, continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5213"/>
+ <location filename="mainwindow.cpp" line="6133"/>
<source>Edit Categories...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5214"/>
+ <location filename="mainwindow.cpp" line="6134"/>
<source>Deselect filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5265"/>
+ <location filename="mainwindow.cpp" line="6187"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5281"/>
+ <location filename="mainwindow.cpp" line="6203"/>
<source>Enable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5282"/>
+ <location filename="mainwindow.cpp" line="6204"/>
<source>Disable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5303"/>
+ <location filename="mainwindow.cpp" line="6225"/>
<source>Unlock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5306"/>
+ <location filename="mainwindow.cpp" line="6228"/>
<source>Lock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5310"/>
+ <location filename="mainwindow.cpp" line="6238"/>
<source>Open Origin in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5317"/>
+ <location filename="mainwindow.cpp" line="6243"/>
<source>Open Origin Info...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5460"/>
+ <location filename="mainwindow.cpp" line="6387"/>
<source>depends on missing &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5464"/>
+ <location filename="mainwindow.cpp" line="6391"/>
<source>incompatible with &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5490"/>
+ <location filename="mainwindow.cpp" line="6417"/>
<source>Please wait while LOOT is running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5587"/>
+ <location filename="mainwindow.cpp" line="6514"/>
<source>loot failed. Exit code was: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5609"/>
+ <location filename="mainwindow.cpp" line="6536"/>
<source>failed to start loot</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5612"/>
+ <location filename="mainwindow.cpp" line="6539"/>
<source>failed to run loot: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5616"/>
+ <location filename="mainwindow.cpp" line="6543"/>
<source>Errors occured</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5663"/>
+ <location filename="mainwindow.cpp" line="6590"/>
<source>Backup of load order created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5673"/>
+ <location filename="mainwindow.cpp" line="6600"/>
<source>Choose backup to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5686"/>
+ <location filename="mainwindow.cpp" line="6613"/>
<source>No Backups</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5686"/>
+ <location filename="mainwindow.cpp" line="6613"/>
<source>There are no backups to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5707"/>
- <location filename="mainwindow.cpp" line="5729"/>
+ <location filename="mainwindow.cpp" line="6634"/>
+ <location filename="mainwindow.cpp" line="6656"/>
<source>Restore failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5708"/>
- <location filename="mainwindow.cpp" line="5730"/>
+ <location filename="mainwindow.cpp" line="6635"/>
+ <location filename="mainwindow.cpp" line="6657"/>
<source>Failed to restore the backup. Errorcode: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5719"/>
+ <location filename="mainwindow.cpp" line="6646"/>
<source>Backup of modlist created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5825"/>
+ <location filename="mainwindow.cpp" line="6752"/>
<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="5827"/>
+ <location filename="mainwindow.cpp" line="6754"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5828"/>
+ <location filename="mainwindow.cpp" line="6755"/>
<source>Rename new file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5829"/>
+ <location filename="mainwindow.cpp" line="6756"/>
<source>Ignore file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5937"/>
+ <location filename="mainwindow.cpp" line="6865"/>
<source>Set the priority of the selected mods</source>
<translation type="unfinished"></translation>
</message>
@@ -3201,8 +3326,8 @@ Click OK to restart MO now.</source>
<context>
<name>MessageDialog</name>
<message>
- <location filename="messagedialog.ui" line="150"/>
- <location filename="messagedialog.ui" line="180"/>
+ <location filename="messagedialog.ui" line="32"/>
+ <location filename="messagedialog.ui" line="71"/>
<source>Placeholder</source>
<translation type="unfinished"></translation>
</message>
@@ -3276,16 +3401,16 @@ Click OK to restart MO now.</source>
</message>
<message>
<location filename="modinfo.cpp" line="113"/>
- <source>invalid content type %1</source>
+ <source>invalid content type: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="136"/>
- <source>invalid mod index %1</source>
+ <location filename="modinfo.cpp" line="137"/>
+ <source>invalid mod index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="166"/>
+ <location filename="modinfo.cpp" line="182"/>
<source>remove: invalid mod index %1</source>
<translation type="unfinished"></translation>
</message>
@@ -3565,41 +3690,46 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="781"/>
+ <location filename="modinfodialog.ui" line="782"/>
+ <source>Web page URL (only used if invalid NexusID) :</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialog.ui" line="795"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="787"/>
- <location filename="modinfodialog.ui" line="790"/>
- <location filename="modinfodialog.ui" line="793"/>
+ <location filename="modinfodialog.ui" line="801"/>
+ <location filename="modinfodialog.ui" line="804"/>
+ <location filename="modinfodialog.ui" line="807"/>
<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="800"/>
- <location filename="modinfodialog.ui" line="803"/>
- <location filename="modinfodialog.ui" line="806"/>
+ <location filename="modinfodialog.ui" line="814"/>
+ <location filename="modinfodialog.ui" line="817"/>
+ <location filename="modinfodialog.ui" line="823"/>
<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="814"/>
+ <location filename="modinfodialog.ui" line="831"/>
<source>Filetree</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="828"/>
+ <location filename="modinfodialog.ui" line="845"/>
<source>Open Mod in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="853"/>
+ <location filename="modinfodialog.ui" line="870"/>
<source>A directory view of this mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="856"/>
+ <location filename="modinfodialog.ui" line="873"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -3609,288 +3739,292 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="883"/>
+ <location filename="modinfodialog.ui" line="900"/>
<source>Previous</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="890"/>
+ <location filename="modinfodialog.ui" line="907"/>
<source>Next</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="910"/>
+ <location filename="modinfodialog.ui" line="927"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="228"/>
+ <location filename="modinfodialog.cpp" line="234"/>
<source>&amp;Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="229"/>
+ <location filename="modinfodialog.cpp" line="235"/>
<source>&amp;Rename</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="230"/>
+ <location filename="modinfodialog.cpp" line="236"/>
<source>&amp;Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="231"/>
+ <location filename="modinfodialog.cpp" line="237"/>
<source>&amp;Unhide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="232"/>
+ <location filename="modinfodialog.cpp" line="238"/>
<source>&amp;Open</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="233"/>
+ <location filename="modinfodialog.cpp" line="239"/>
<source>&amp;New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="485"/>
- <location filename="modinfodialog.cpp" line="500"/>
+ <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="485"/>
- <location filename="modinfodialog.cpp" line="500"/>
+ <location filename="modinfodialog.cpp" line="504"/>
+ <location filename="modinfodialog.cpp" line="519"/>
<source>Save changes to &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="690"/>
+ <location filename="modinfodialog.cpp" line="710"/>
<source>File Exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="690"/>
+ <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="707"/>
+ <location filename="modinfodialog.cpp" line="727"/>
<source>failed to move file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="732"/>
+ <location filename="modinfodialog.cpp" line="752"/>
<source>failed to create directory &quot;optional&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="776"/>
- <location filename="modinfodialog.cpp" line="1500"/>
+ <location filename="modinfodialog.cpp" line="796"/>
<source>Info requested, please wait</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="830"/>
+ <location filename="modinfodialog.cpp" line="804"/>
<source>Main</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="831"/>
+ <location filename="modinfodialog.cpp" line="805"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="832"/>
+ <location filename="modinfodialog.cpp" line="806"/>
<source>Optional</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="833"/>
+ <location filename="modinfodialog.cpp" line="807"/>
<source>Old</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="834"/>
- <source>Misc</source>
+ <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="835"/>
+ <location filename="modinfodialog.cpp" line="810"/>
<source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="846"/>
+ <location filename="modinfodialog.cpp" line="821"/>
<source>Current Version: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="850"/>
+ <location filename="modinfodialog.cpp" line="825"/>
<source>No update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="893"/>
- <source>(description incomplete, please visit nexus)</source>
+ <location filename="modinfodialog.cpp" line="842"/>
+ <source>&lt;div style=&quot;text-align: center;&quot;&gt;&lt;h1&gt;Uh oh!&lt;/h1&gt;&lt;p&gt;Sorry, there is no description available for this mod. :(&lt;/p&gt;&lt;/div&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="908"/>
+ <location filename="modinfodialog.cpp" line="858"/>
<source>&lt;a href=&quot;%1&quot;&gt;Visit on Nexus&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1001"/>
+ <location filename="modinfodialog.cpp" line="957"/>
<source>Failed to delete %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1017"/>
- <location filename="modinfodialog.cpp" line="1023"/>
- <location filename="modinfodialog.cpp" line="1042"/>
- <location filename="modinfodialog.cpp" line="1047"/>
+ <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="1017"/>
- <location filename="modinfodialog.cpp" line="1042"/>
+ <location filename="modinfodialog.cpp" line="973"/>
+ <location filename="modinfodialog.cpp" line="998"/>
<source>Are sure you want to delete &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1023"/>
- <location filename="modinfodialog.cpp" line="1047"/>
+ <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="1121"/>
- <location filename="modinfodialog.cpp" line="1127"/>
+ <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="1133"/>
+ <location filename="modinfodialog.cpp" line="1089"/>
<source>Failed to create &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1237"/>
- <location filename="modinfodialog.cpp" line="1261"/>
+ <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="1237"/>
+ <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="1240"/>
- <location filename="modinfodialog.cpp" line="1264"/>
+ <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="1240"/>
- <location filename="modinfodialog.cpp" line="1264"/>
+ <location filename="modinfodialog.cpp" line="1196"/>
+ <location filename="modinfodialog.cpp" line="1220"/>
<source>Failed to remove &quot;%1&quot;. Maybe you lack the required file permissions?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1251"/>
- <location filename="modinfodialog.cpp" line="1274"/>
+ <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="1261"/>
+ <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="1337"/>
+ <location filename="modinfodialog.cpp" line="1293"/>
<source>Select binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1337"/>
+ <location filename="modinfodialog.cpp" line="1293"/>
<source>Binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1409"/>
+ <location filename="modinfodialog.cpp" line="1365"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1422"/>
+ <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="1438"/>
+ <location filename="modinfodialog.cpp" line="1394"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1438"/>
+ <location filename="modinfodialog.cpp" line="1394"/>
<source>Sorry, can&apos;t preview anything. This function currently does not support extracting from bsas.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1452"/>
+ <location filename="modinfodialog.cpp" line="1408"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1454"/>
+ <location filename="modinfodialog.cpp" line="1410"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1457"/>
- <location filename="modinfodialog.cpp" line="1477"/>
+ <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="1461"/>
- <location filename="modinfodialog.cpp" line="1481"/>
+ <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="1529"/>
+ <location filename="modinfodialog.cpp" line="1488"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1529"/>
+ <location filename="modinfodialog.cpp" line="1488"/>
<source>Please enter a name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1533"/>
- <location filename="modinfodialog.cpp" line="1536"/>
+ <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="1533"/>
+ <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="1536"/>
+ <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="1550"/>
+ <location filename="modinfodialog.cpp" line="1509"/>
<source>Create Tweak</source>
<translation type="unfinished"></translation>
</message>
@@ -3921,7 +4055,7 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModInfoOverwrite</name>
<message>
- <location filename="modinfooverwrite.cpp" line="48"/>
+ <location filename="modinfooverwrite.cpp" line="51"/>
<source>This pseudo mod contains files from the virtual data tree that got modified (i.e. by the construction kit)</source>
<translation type="unfinished"></translation>
</message>
@@ -3929,18 +4063,18 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModInfoRegular</name>
<message>
- <location filename="modinforegular.cpp" line="178"/>
- <location filename="modinforegular.cpp" line="181"/>
+ <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="560"/>
+ <location filename="modinforegular.cpp" line="651"/>
<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="564"/>
+ <location filename="modinforegular.cpp" line="655"/>
<source>Categories: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
@@ -4042,17 +4176,17 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="modlist.cpp" line="161"/>
- <source>Overwrites files</source>
+ <source>Overwrites loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="modlist.cpp" line="162"/>
- <source>Overwritten files</source>
+ <source>Overwritten loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="modlist.cpp" line="163"/>
- <source>Overwrites &amp; Overwritten</source>
+ <source>Loose files Overwrites &amp; Overwritten</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -4062,162 +4196,212 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="modlist.cpp" line="165"/>
- <source>Alternate game source</source>
+ <source>Overwrites an archive with loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="257"/>
+ <location filename="modlist.cpp" line="166"/>
+ <source>Archive is overwritten by loose files</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="167"/>
+ <source>Overwrites another archive file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="168"/>
+ <source>Overwritten by another archive file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="169"/>
+ <source>Archive files overwrites &amp; overwritten</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="170"/>
+ <source>&lt;br&gt;This mod is for a different game, make sure it&apos;s compatible or it could cause crashes.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="172"/>
+ <source>Mod is being tracked on the website</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="264"/>
<source>Non-MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="289"/>
+ <location filename="modlist.cpp" line="296"/>
<source>invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="437"/>
+ <location filename="modlist.cpp" line="456"/>
<source>installed version: &quot;%1&quot;, newest version: &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="439"/>
+ <location filename="modlist.cpp" line="458"/>
<source>The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to &quot;upgrade&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="447"/>
+ <location filename="modlist.cpp" line="463"/>
+ <source>This file has been marked as &quot;Old&quot;. There is most likely an updated version of this file available.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="465"/>
+ <source>This file has been marked as &quot;Deleted&quot;! You may want to check for an update or remove the nexus ID from this mod!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="472"/>
+ <source>%1 minute(s) and %2 second(s)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="473"/>
+ <source>This mod will be available to check in %2.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="481"/>
<source>Categories: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="478"/>
+ <location filename="modlist.cpp" line="512"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="483"/>
+ <location filename="modlist.cpp" line="517"/>
<source>Name is already in use by another mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1022"/>
+ <location filename="modlist.cpp" line="1101"/>
<source>drag&amp;drop failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1101"/>
+ <location filename="modlist.cpp" line="1180"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1102"/>
+ <location filename="modlist.cpp" line="1181"/>
<source>Are you sure you want to remove &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1165"/>
+ <location filename="modlist.cpp" line="1248"/>
<source>Flags</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1166"/>
+ <location filename="modlist.cpp" line="1249"/>
<source>Content</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1167"/>
+ <location filename="modlist.cpp" line="1250"/>
<source>Mod Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1168"/>
+ <location filename="modlist.cpp" line="1251"/>
<source>Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1169"/>
+ <location filename="modlist.cpp" line="1252"/>
<source>Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1170"/>
+ <location filename="modlist.cpp" line="1253"/>
<source>Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1171"/>
+ <location filename="modlist.cpp" line="1254"/>
<source>Source Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1172"/>
+ <location filename="modlist.cpp" line="1255"/>
<source>Nexus ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1173"/>
+ <location filename="modlist.cpp" line="1256"/>
<source>Installation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1174"/>
+ <location filename="modlist.cpp" line="1257"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1175"/>
- <location filename="modlist.cpp" line="1211"/>
+ <location filename="modlist.cpp" line="1258"/>
+ <location filename="modlist.cpp" line="1294"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1183"/>
+ <location filename="modlist.cpp" line="1266"/>
<source>Name of your mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1184"/>
+ <location filename="modlist.cpp" line="1267"/>
<source>Version of the mod (if available)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1185"/>
+ <location filename="modlist.cpp" line="1268"/>
<source>Installation priority of your mod. The higher, the more &quot;important&quot; it is and thus overwrites files from mods with lower priority.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1187"/>
+ <location filename="modlist.cpp" line="1270"/>
<source>Category of the mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1188"/>
+ <location filename="modlist.cpp" line="1271"/>
<source>The source game which was the origin of this mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1189"/>
+ <location filename="modlist.cpp" line="1272"/>
<source>Id of the mod as used on Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1190"/>
+ <location filename="modlist.cpp" line="1273"/>
<source>Emblemes to highlight things that might require attention.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1191"/>
+ <location filename="modlist.cpp" line="1274"/>
<source>Depicts the content of the mod:&lt;br&gt;&lt;table cellspacing=7&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/plugin&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Game plugins (esp/esm/esl)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/interface&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Interface&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/mesh&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Meshes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/bsa&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;BSA&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/texture&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Textures&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/sound&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Sounds&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/music&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Music&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/string&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Strings&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/script&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Scripts (Papyrus)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/skse&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Script Extender plugins&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/skyproc&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;SkyProc Patcher&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/menu&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Mod Configuration Menu&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/inifile&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;INI files&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/modgroup&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;ModGroup files&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1209"/>
+ <location filename="modlist.cpp" line="1292"/>
<source>Time this mod was installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1210"/>
+ <location filename="modlist.cpp" line="1293"/>
<source>User notes about the mod</source>
<translation type="unfinished"></translation>
</message>
@@ -4225,7 +4409,7 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModListSortProxy</name>
<message>
- <location filename="modlistsortproxy.cpp" line="399"/>
+ <location filename="modlistsortproxy.cpp" line="506"/>
<source>Drag&amp;Drop is only supported when sorting by priority</source>
<translation type="unfinished"></translation>
</message>
@@ -4259,35 +4443,41 @@ p, li { white-space: pre-wrap; }
<context>
<name>NXMAccessManager</name>
<message>
- <location filename="nxmaccessmanager.cpp" line="177"/>
- <source>Verifying Nexus login</source>
+ <location filename="nxmaccessmanager.cpp" line="128"/>
+ <location filename="nxmaccessmanager.cpp" line="146"/>
+ <source>Validating Nexus Connection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nxmaccessmanager.cpp" line="254"/>
- <source>Logging into Nexus</source>
+ <location filename="nxmaccessmanager.cpp" line="235"/>
+ <source>There was a timeout during the request</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nxmaccessmanager.cpp" line="277"/>
- <source>timeout</source>
+ <location filename="nxmaccessmanager.cpp" line="257"/>
+ <source>Unknown error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nxmaccessmanager.cpp" line="297"/>
- <source>Unknown error</source>
+ <location filename="nxmaccessmanager.cpp" line="295"/>
+ <source>Validation failed, please reauthenticate in the Settings -&gt; Nexus tab: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nxmaccessmanager.cpp" line="333"/>
- <source>Please check your password</source>
+ <location filename="nxmaccessmanager.cpp" line="300"/>
+ <source>Could not parse response. Invalid JSON.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="306"/>
+ <source>Unknown error.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NXMUrl</name>
<message>
- <location filename="../../uibase/src/nxmurl.cpp" line="30"/>
+ <location filename="../../uibase/src/nxmurl.cpp" line="33"/>
<source>invalid nxm-link: %1</source>
<translation type="unfinished"></translation>
</message>
@@ -4295,17 +4485,17 @@ p, li { white-space: pre-wrap; }
<context>
<name>NexusInterface</name>
<message>
- <location filename="nexusinterface.cpp" line="209"/>
+ <location filename="nexusinterface.cpp" line="246"/>
<source>Failed to guess mod id for &quot;%1&quot;, please pick the correct one</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="543"/>
+ <location filename="nexusinterface.cpp" line="806"/>
<source>empty response</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="572"/>
+ <location filename="nexusinterface.cpp" line="873"/>
<source>invalid response</source>
<translation type="unfinished"></translation>
</message>
@@ -4313,213 +4503,234 @@ p, li { white-space: pre-wrap; }
<context>
<name>OrganizerCore</name>
<message>
- <location filename="organizercore.cpp" line="412"/>
- <location filename="organizercore.cpp" line="439"/>
+ <location filename="organizercore.cpp" line="408"/>
+ <location filename="organizercore.cpp" line="435"/>
<source>Failed to write settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="413"/>
+ <location filename="organizercore.cpp" line="409"/>
<source>An error occured trying to update MO settings to %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="434"/>
+ <location filename="organizercore.cpp" line="430"/>
<source>File is write protected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="436"/>
+ <location filename="organizercore.cpp" line="432"/>
<source>Invalid file format (probably a bug)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="437"/>
+ <location filename="organizercore.cpp" line="433"/>
<source>Unknown error %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="440"/>
+ <location filename="organizercore.cpp" line="436"/>
<source>An error occured 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="706"/>
<source>Download started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="709"/>
+ <location filename="organizercore.cpp" line="698"/>
<source>Download failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="975"/>
- <location filename="organizercore.cpp" line="1033"/>
- <source>Installation successful</source>
+ <location filename="organizercore.cpp" line="981"/>
+ <location filename="organizercore.cpp" line="1018"/>
+ <location filename="organizercore.cpp" line="1029"/>
+ <location filename="organizercore.cpp" line="1087"/>
+ <source>Installation cancelled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="983"/>
- <location filename="organizercore.cpp" line="1043"/>
- <source>Configure Mod</source>
+ <location filename="organizercore.cpp" line="982"/>
+ <location filename="organizercore.cpp" line="1030"/>
+ <source>Another installation is currently in progress.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="984"/>
- <location filename="organizercore.cpp" line="1044"/>
- <source>This mod contains ini tweaks. Do you want to configure them now?</source>
+ <location filename="organizercore.cpp" line="994"/>
+ <location filename="organizercore.cpp" line="1059"/>
+ <source>Installation successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="996"/>
- <location filename="organizercore.cpp" line="1054"/>
- <source>mod &quot;%1&quot; not found</source>
+ <location filename="organizercore.cpp" line="1002"/>
+ <location filename="organizercore.cpp" line="1069"/>
+ <source>Configure Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="999"/>
- <location filename="organizercore.cpp" line="1061"/>
- <source>Installation cancelled</source>
+ <location filename="organizercore.cpp" line="1003"/>
+ <location filename="organizercore.cpp" line="1070"/>
+ <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"/>
+ <source>mod not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1000"/>
- <location filename="organizercore.cpp" line="1062"/>
+ <location filename="organizercore.cpp" line="1019"/>
+ <location filename="organizercore.cpp" line="1088"/>
<source>The mod was not installed completely.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1266"/>
- <source>Executable &quot;%1&quot; not found</source>
+ <location filename="organizercore.cpp" line="1301"/>
+ <source>Executable not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1294"/>
+ <location filename="organizercore.cpp" line="1336"/>
<source>Start Steam?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1295"/>
+ <location filename="organizercore.cpp" line="1337"/>
<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="1324"/>
+ <location filename="organizercore.cpp" line="1360"/>
+ <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.
+
+Restart MO as administrator?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="organizercore.cpp" line="1408"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1332"/>
+ <location filename="organizercore.cpp" line="1416"/>
<source>Windows Event Log Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1333"/>
+ <location filename="organizercore.cpp" line="1417"/>
<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="1345"/>
+ <location filename="organizercore.cpp" line="1429"/>
<source>Blacklisted Executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1346"/>
+ <location filename="organizercore.cpp" line="1430"/>
<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="1435"/>
+ <location filename="organizercore.cpp" line="1527"/>
<source>No profile set</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1724"/>
+ <location filename="organizercore.cpp" line="1834"/>
<source>Failed to refresh list of esps: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1817"/>
+ <location filename="organizercore.cpp" line="1943"/>
<source>Multiple esps/esls activated, please check that they don&apos;t conflict.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1892"/>
+ <location filename="organizercore.cpp" line="2031"/>
<source>Download?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1893"/>
+ <location filename="organizercore.cpp" line="2032"/>
<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="2027"/>
+ <location filename="organizercore.cpp" line="2167"/>
+ <location filename="organizercore.cpp" line="2216"/>
<source>failed to update mod list: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2034"/>
- <location filename="organizercore.cpp" line="2051"/>
+ <location filename="organizercore.cpp" line="2223"/>
+ <location filename="organizercore.cpp" line="2240"/>
<source>login successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2058"/>
+ <location filename="organizercore.cpp" line="2247"/>
<source>Login failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2059"/>
+ <location filename="organizercore.cpp" line="2248"/>
<source>Login failed, try again?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2068"/>
+ <location filename="organizercore.cpp" line="2257"/>
<source>login failed: %1. Download will not be associated with an account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2076"/>
+ <location filename="organizercore.cpp" line="2265"/>
<source>login failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2086"/>
+ <location filename="organizercore.cpp" line="2275"/>
<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="2139"/>
+ <location filename="organizercore.cpp" line="2328"/>
<source>MO1 &quot;Script Extender&quot; load mechanism has left hook.dll in your game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2142"/>
- <location filename="organizercore.cpp" line="2158"/>
+ <location filename="organizercore.cpp" line="2331"/>
+ <location filename="organizercore.cpp" line="2347"/>
<source>Description missing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2151"/>
+ <location filename="organizercore.cpp" line="2340"/>
<source>&lt;a href=&quot;%1&quot;&gt;hook.dll&lt;/a&gt; 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 &quot;Script Extender&quot;, 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="2185"/>
+ <location filename="organizercore.cpp" line="2374"/>
<source>failed to save load order: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2257"/>
+ <location filename="organizercore.cpp" line="2446"/>
<source>The designated write target &quot;%1&quot; is not enabled.</source>
<translation type="unfinished"></translation>
</message>
@@ -4563,7 +4774,7 @@ Continue?</source>
</message>
<message>
<location filename="overwriteinfodialog.cpp" line="113"/>
- <source>%1 not found</source>
+ <source>mod not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -4606,18 +4817,18 @@ Continue?</source>
<context>
<name>PluginContainer</name>
<message>
- <location filename="plugincontainer.cpp" line="326"/>
+ <location filename="plugincontainer.cpp" line="330"/>
<source>Some plugins could not be loaded</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="329"/>
- <location filename="plugincontainer.cpp" line="347"/>
+ <location filename="plugincontainer.cpp" line="333"/>
+ <location filename="plugincontainer.cpp" line="351"/>
<source>Description missing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="338"/>
+ <location filename="plugincontainer.cpp" line="342"/>
<source>The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version:</source>
<translation type="unfinished"></translation>
</message>
@@ -4625,135 +4836,135 @@ Continue?</source>
<context>
<name>PluginList</name>
<message>
- <location filename="pluginlist.cpp" line="103"/>
+ <location filename="pluginlist.cpp" line="104"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="104"/>
+ <location filename="pluginlist.cpp" line="105"/>
<source>Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="105"/>
+ <location filename="pluginlist.cpp" line="106"/>
<source>Mod Index</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="106"/>
+ <location filename="pluginlist.cpp" line="107"/>
<source>Flags</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="107"/>
- <location filename="pluginlist.cpp" line="119"/>
+ <location filename="pluginlist.cpp" line="108"/>
+ <location filename="pluginlist.cpp" line="120"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="115"/>
+ <location filename="pluginlist.cpp" line="116"/>
<source>Name of your mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="116"/>
+ <location filename="pluginlist.cpp" line="117"/>
<source>Load priority of your mod. The higher, the more &quot;important&quot; it is and thus overwrites data from plugins with lower priority.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="118"/>
+ <location filename="pluginlist.cpp" line="119"/>
<source>The modindex determines the formids of objects originating from this mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="221"/>
+ <location filename="pluginlist.cpp" line="228"/>
<source>failed to update esp info for file %1 (source id: %2), error: %3</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="294"/>
- <source>esp not found: %1</source>
+ <location filename="pluginlist.cpp" line="301"/>
+ <source>Plugin not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="342"/>
- <location filename="pluginlist.cpp" line="354"/>
+ <location filename="pluginlist.cpp" line="349"/>
+ <location filename="pluginlist.cpp" line="361"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="342"/>
+ <location filename="pluginlist.cpp" line="349"/>
<source>Really enable all plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="354"/>
+ <location filename="pluginlist.cpp" line="361"/>
<source>Really disable all plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="459"/>
+ <location filename="pluginlist.cpp" line="466"/>
<source>The file containing locked plugin indices is broken</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="955"/>
- <location filename="pluginlist.cpp" line="959"/>
+ <location filename="pluginlist.cpp" line="963"/>
+ <location filename="pluginlist.cpp" line="967"/>
<source>&lt;b&gt;Origin&lt;/b&gt;: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="956"/>
+ <location filename="pluginlist.cpp" line="964"/>
<source>&lt;br&gt;&lt;b&gt;&lt;i&gt;This plugin can&apos;t be disabled (enforced by the game).&lt;/i&gt;&lt;/b&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="961"/>
+ <location filename="pluginlist.cpp" line="969"/>
<source>Author</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="964"/>
+ <location filename="pluginlist.cpp" line="972"/>
<source>Description</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="967"/>
+ <location filename="pluginlist.cpp" line="975"/>
<source>Missing Masters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="974"/>
+ <location filename="pluginlist.cpp" line="982"/>
<source>Enabled Masters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="977"/>
+ <location filename="pluginlist.cpp" line="985"/>
<source>Loads Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="978"/>
+ <location filename="pluginlist.cpp" line="986"/>
<source>There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="983"/>
+ <location filename="pluginlist.cpp" line="991"/>
<source>Loads INI settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="984"/>
+ <location filename="pluginlist.cpp" line="992"/>
<source>There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="988"/>
+ <location filename="pluginlist.cpp" line="996"/>
<source>This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="1163"/>
+ <location filename="pluginlist.cpp" line="1171"/>
<source>failed to restore load order for %1</source>
<translation type="unfinished"></translation>
</message>
@@ -4761,7 +4972,7 @@ Continue?</source>
<context>
<name>PluginListSortProxy</name>
<message>
- <location filename="pluginlistsortproxy.cpp" line="120"/>
+ <location filename="pluginlistsortproxy.cpp" line="119"/>
<source>Drag&amp;Drop is only supported when sorting by priority or mod index</source>
<translation type="unfinished"></translation>
</message>
@@ -4783,7 +4994,7 @@ Continue?</source>
<name>ProblemsDialog</name>
<message>
<location filename="problemsdialog.ui" line="14"/>
- <source>Problems</source>
+ <source>Notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -4801,13 +5012,13 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="problemsdialog.cpp" line="42"/>
- <location filename="problemsdialog.cpp" line="43"/>
+ <location filename="problemsdialog.cpp" line="51"/>
+ <location filename="problemsdialog.cpp" line="52"/>
<source>Fix</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="problemsdialog.cpp" line="49"/>
+ <location filename="problemsdialog.cpp" line="58"/>
<source>No guided fix</source>
<translation type="unfinished"></translation>
</message>
@@ -4815,86 +5026,90 @@ p, li { white-space: pre-wrap; }
<context>
<name>Profile</name>
<message>
- <location filename="profile.cpp" line="78"/>
- <source>invalid profile name %1</source>
+ <location filename="profile.cpp" line="81"/>
+ <source>invalid profile name: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="82"/>
+ <location filename="profile.cpp" line="85"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="256"/>
+ <location filename="profile.cpp" line="259"/>
<source>failed to write mod list: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="267"/>
+ <location filename="profile.cpp" line="270"/>
<source>failed to update tweaked ini file, wrong settings may be used: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="287"/>
+ <location filename="profile.cpp" line="290"/>
<source>failed to create tweaked ini: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="312"/>
+ <location filename="profile.cpp" line="315"/>
<source>failed to open %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="371"/>
+ <location filename="profile.cpp" line="374"/>
<source>&quot;%1&quot; is missing or inaccessible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="416"/>
- <location filename="profile.cpp" line="455"/>
- <location filename="profile.cpp" line="552"/>
- <location filename="profile.cpp" line="571"/>
- <location filename="profile.cpp" line="581"/>
- <source>invalid index %1</source>
+ <location filename="profile.cpp" line="419"/>
+ <location filename="profile.cpp" line="458"/>
+ <location filename="profile.cpp" line="548"/>
+ <location filename="profile.cpp" line="569"/>
+ <location filename="profile.cpp" line="579"/>
+ <location filename="profile.cpp" line="598"/>
+ <location filename="profile.cpp" line="608"/>
+ <source>invalid mod index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="533"/>
- <source>Overwrite directory couldn&apos;t be parsed</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="profile.cpp" line="544"/>
+ <location filename="profile.cpp" line="540"/>
<source>invalid priority %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="751"/>
+ <location filename="profile.cpp" line="779"/>
<source>Delete profile-specific save games?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="752"/>
+ <location filename="profile.cpp" line="780"/>
<source>Do you want to delete the profile-specific save games? (If you select &quot;No&quot;, the save games will show up again if you re-enable profile-specific save games)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="782"/>
+ <location filename="profile.cpp" line="810"/>
<source>Missing profile-specific game INI files!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="783"/>
- <source>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.</source>
+ <location filename="profile.cpp" line="811"/>
+ <source>Some of your profile-specific game INI files were missing. They will now be copied from the vanilla game folder. You might want to double-check your settings.
+
+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>
- <location filename="profile.cpp" line="798"/>
+ <location filename="profile.cpp" line="827"/>
<source>Delete profile-specific game INI files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="799"/>
+ <location filename="profile.cpp" line="828"/>
<source>Do you want to delete the profile-specific game INI files? (If you select &quot;No&quot;, the INI files will be used again if you re-enable profile-specific game INI files.)</source>
<translation type="unfinished"></translation>
</message>
@@ -4944,49 +5159,47 @@ p, li { white-space: pre-wrap; }
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;This is the list of profiles. Each Profile contains its own list and installation order of enabled mods (from a shared pool), a configuration of enabled esps/esms, a copy of the games ini-file and an optional savegame filter.&lt;/p&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Note&lt;/span&gt; For technical reasons it&apos;s currently not possible to have seperate load-orders for esps. This means you can&apos;t load moda.esp before modb.esp in one profile and the other way around in another.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8.25pt;&quot;&gt;This is the list of profiles. Each profile contains its own list and installation order of enabled mods (from a shared pool), its own list and load order of enabled plugins (esps/esms), a copy of the games ini-file and an optional savegame filter.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="38"/>
+ <location filename="profilesdialog.ui" line="36"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, save games are stored locally to this profile and will not appear when starting with a different profile.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="41"/>
+ <location filename="profilesdialog.ui" line="39"/>
<source>If checked, save games are local to this profile and will not appear when starting with a different profile.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="44"/>
+ <location filename="profilesdialog.ui" line="42"/>
<source>Use profile-specific Save Games</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="51"/>
+ <location filename="profilesdialog.ui" line="49"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked MO2 will use his own profile-specific game INI files, so that the &amp;quot;Global&amp;quot; ones in MyGames can be left vanilla. This different set of INI files is then offered to the game instead of the default one.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="54"/>
+ <location filename="profilesdialog.ui" line="52"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, MO2 will use a local set of game INI files (configuration and settings files), different from the default ones found in MyGames. This way changes to the INI settings will only affect this profile and the Global INI files can remain vanilla. MO2 will then show the profile INI files to the game instead of the Global ones.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="57"/>
+ <location filename="profilesdialog.ui" line="55"/>
<source>Use profile-specific Game INI Files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="67"/>
+ <location filename="profilesdialog.ui" line="65"/>
<source>This ensures data files from mods are actually used. You want to enable this unless you use a different tool for Archive Invalidation.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="70"/>
+ <location filename="profilesdialog.ui" line="68"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -4998,65 +5211,65 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="80"/>
+ <location filename="profilesdialog.ui" line="78"/>
<source>Automatic Archive Invalidation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="91"/>
- <location filename="profilesdialog.ui" line="94"/>
+ <location filename="profilesdialog.ui" line="89"/>
+ <location filename="profilesdialog.ui" line="92"/>
<source>Create a new profile from scratch</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="97"/>
+ <location filename="profilesdialog.ui" line="95"/>
<source>Create</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="107"/>
+ <location filename="profilesdialog.ui" line="105"/>
<source>Clone the selected profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="110"/>
+ <location filename="profilesdialog.ui" line="108"/>
<source>This creates a new profile with the same settings and active mods as the selected one.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="113"/>
+ <location filename="profilesdialog.ui" line="111"/>
<source>Copy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="123"/>
- <location filename="profilesdialog.ui" line="126"/>
+ <location filename="profilesdialog.ui" line="121"/>
+ <location filename="profilesdialog.ui" line="124"/>
<source>Delete the selected Profile. This can not be un-done!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="129"/>
+ <location filename="profilesdialog.ui" line="127"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="139"/>
+ <location filename="profilesdialog.ui" line="137"/>
<source>Rename</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="149"/>
- <location filename="profilesdialog.ui" line="152"/>
+ <location filename="profilesdialog.ui" line="147"/>
+ <location filename="profilesdialog.ui" line="150"/>
<source>Transfer save games to the selected profile.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="155"/>
+ <location filename="profilesdialog.ui" line="153"/>
<source>Transfer Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="178"/>
+ <location filename="profilesdialog.ui" line="176"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
@@ -5148,12 +5361,31 @@ p, li { white-space: pre-wrap; }
</message>
</context>
<context>
+ <name>QApplication</name>
+ <message>
+ <location filename="../../uibase/src/registry.cpp" line="36"/>
+ <source>INI file is read-only</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/registry.cpp" line="37"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="133"/>
+ <source>Mod Organizer is attempting to write to &quot;%1&quot; which is currently set to read-only. Clear the read-only flag to allow the write?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/textviewer.cpp" line="132"/>
+ <source>File is read-only</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
<name>QObject</name>
<message>
<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="733"/>
+ <location filename="organizercore.cpp" line="722"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
@@ -5199,12 +5431,12 @@ p, li { white-space: pre-wrap; }
<location filename="categories.cpp" line="306"/>
<location filename="categories.cpp" line="316"/>
<location filename="categories.cpp" line="326"/>
- <source>invalid index %1</source>
+ <source>invalid category index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="categories.cpp" line="337"/>
- <source>invalid category id %1</source>
+ <source>invalid category id: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -5253,12 +5485,13 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="helper.cpp" line="55"/>
+ <location filename="helper.cpp" line="56"/>
+ <location filename="helper.cpp" line="65"/>
<source>helper failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="helper.cpp" line="71"/>
+ <location filename="helper.cpp" line="81"/>
<source>failed to determine account name</source>
<translation type="unfinished"></translation>
</message>
@@ -5269,7 +5502,7 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="87"/>
+ <location filename="installationmanager.cpp" line="88"/>
<source>archive.dll not loaded: &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
@@ -5322,88 +5555,89 @@ If the folder was still in use, restart MO and try again.</source>
<message>
<location filename="instancemanager.cpp" line="140"/>
<source>Enter a new name or select one from the suggested list:
-(This is just the name of the Instance, not the game selection)</source>
+(This is just a name for the Instance and can be whatever you wish,
+ the actual game selection will happen on the next screen regardless of chosen name)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="150"/>
- <location filename="instancemanager.cpp" line="221"/>
+ <location filename="instancemanager.cpp" line="151"/>
+ <location filename="instancemanager.cpp" line="222"/>
<source>Canceled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="156"/>
+ <location filename="instancemanager.cpp" line="157"/>
<source>Invalid instance name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="157"/>
+ <location filename="instancemanager.cpp" line="158"/>
<source>The instance name &quot;%1&quot; is invalid. Use the name &quot;%2&quot; instead?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="172"/>
+ <location filename="instancemanager.cpp" line="173"/>
<source>The instance &quot;%1&quot; already exists.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="173"/>
+ <location filename="instancemanager.cpp" line="174"/>
<source>Please choose a different instance name, like: &quot;%1 1&quot; .</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="192"/>
+ <location filename="instancemanager.cpp" line="193"/>
<source>Choose Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="193"/>
+ <location filename="instancemanager.cpp" line="194"/>
<source>Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="205"/>
+ <location filename="instancemanager.cpp" line="206"/>
<source>New</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="206"/>
+ <location filename="instancemanager.cpp" line="207"/>
<source>Create a new instance.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="210"/>
+ <location filename="instancemanager.cpp" line="211"/>
<source>Portable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="211"/>
+ <location filename="instancemanager.cpp" line="212"/>
<source>Use MO folder for data.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="215"/>
+ <location filename="instancemanager.cpp" line="216"/>
<source>Manage Instances</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="216"/>
+ <location filename="instancemanager.cpp" line="217"/>
<source>Delete an Instance.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="267"/>
- <location filename="profile.cpp" line="66"/>
+ <location filename="instancemanager.cpp" line="268"/>
+ <location filename="profile.cpp" line="69"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="270"/>
+ <location filename="instancemanager.cpp" line="271"/>
<source>Data directory created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="271"/>
+ <location filename="instancemanager.cpp" line="272"/>
<source>New data directory created at %1. If you don&apos;t want to store a lot of data there, reconfigure the storage directories via settings.</source>
<translation type="unfinished"></translation>
</message>
@@ -5415,7 +5649,7 @@ If the folder was still in use, restart MO and try again.</source>
<message>
<location filename="loadmechanism.cpp" line="103"/>
<location filename="loadmechanism.cpp" line="112"/>
- <source>%1 not found</source>
+ <source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -5473,116 +5707,139 @@ 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="734"/>
+ <location filename="organizercore.cpp" line="723"/>
<source>Failed to create &quot;%1&quot;. Your user account probably lacks permission.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="296"/>
+ <location filename="main.cpp" line="298"/>
<source>Plugin to handle %1 no longer installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="353"/>
+ <location filename="main.cpp" line="315"/>
<source>Could not use configuration settings for game &quot;%1&quot;, path &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="357"/>
- <location filename="main.cpp" line="375"/>
+ <location filename="main.cpp" line="319"/>
+ <location filename="main.cpp" line="342"/>
+ <location filename="main.cpp" line="359"/>
+ <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"/>
<source>Please select the game to manage</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="385"/>
+ <location filename="main.cpp" line="368"/>
+ <source>Canceled finding %1 in &quot;%2&quot;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="main.cpp" line="369"/>
+ <source>Canceled finding game in &quot;%1&quot;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="main.cpp" line="374"/>
+ <source>%1 not identified in &quot;%2&quot;. 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 &quot;%1&quot;. The directory is required to contain the game binary.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="537"/>
+ <location filename="main.cpp" line="533"/>
<source>Please select the game edition you have (MO can&apos;t start the game correctly if this is set incorrectly!)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="574"/>
+ <location filename="main.cpp" line="570"/>
<source>failed to start shortcut: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="596"/>
+ <location filename="main.cpp" line="592"/>
<source>failed to start application: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="682"/>
- <location filename="settings.cpp" line="1095"/>
+ <location filename="main.cpp" line="686"/>
+ <location filename="settings.cpp" line="1211"/>
<source>Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="683"/>
+ <location filename="main.cpp" line="687"/>
<source>An instance of Mod Organizer is already running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="698"/>
+ <location filename="main.cpp" line="702"/>
<source>Failed to set up instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="878"/>
+ <location filename="mainwindow.cpp" line="958"/>
<source>Please use &quot;Help&quot; from the toolbar to get usage instructions to all elements</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1357"/>
- <location filename="mainwindow.cpp" line="4426"/>
+ <location filename="mainwindow.cpp" line="1453"/>
+ <location filename="mainwindow.cpp" line="5079"/>
<source>&lt;Manage...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1369"/>
+ <location filename="mainwindow.cpp" line="1465"/>
<source>failed to parse profile %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="170"/>
+ <location filename="organizercore.cpp" line="171"/>
<source>Failed to start &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="173"/>
+ <location filename="organizercore.cpp" line="174"/>
<source>Waiting</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="174"/>
+ <location filename="organizercore.cpp" line="175"/>
<source>Please press OK once you&apos;re logged into steam.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="173"/>
+ <location filename="plugincontainer.cpp" line="177"/>
<source>failed to initialize plugin %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="259"/>
+ <location filename="plugincontainer.cpp" line="263"/>
<source>Plugin error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="260"/>
+ <location filename="plugincontainer.cpp" line="264"/>
<source>It appears the plugin &quot;%1&quot; failed to load last startup and caused MO to crash. Do you want to disable it?
(Please note: If this is the first time you see this message for this plugin you may want to give it another try. The plugin may be able to recover from the problem)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="535"/>
+ <location filename="pluginlist.cpp" line="542"/>
<source>failed to access %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="549"/>
+ <location filename="pluginlist.cpp" line="556"/>
<source>failed to set file time %1</source>
<translation type="unfinished"></translation>
</message>
@@ -5592,37 +5849,39 @@ If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1102"/>
+ <location filename="settings.cpp" line="1218"/>
<source>Script Extender</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1109"/>
+ <location filename="settings.cpp" line="1225"/>
<source>Proxy DLL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="146"/>
+ <location filename="spawn.cpp" line="147"/>
<source>failed to spawn &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="153"/>
+ <location filename="spawn.cpp" line="152"/>
<source>Elevation required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="154"/>
+ <location filename="spawn.cpp" line="153"/>
<source>This process requires elevation to run.
This is a potential security risk so I highly advice you to investigate if
&quot;%1&quot;
can be installed to work without elevation.
-Start elevated anyway? (you will be asked if you want to allow ModOrganizer.exe to make changes to the system)</source>
+Restart Mod Organizer as an elevated process?
+You will be asked if you want to allow helper.exe to make changes to the system. You will need to relaunch the process above manually.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="169"/>
+ <location filename="spawn.cpp" line="164"/>
+ <location filename="spawn.cpp" line="178"/>
<source>failed to spawn &quot;%1&quot;: %2</source>
<translation type="unfinished"></translation>
</message>
@@ -5742,58 +6001,58 @@ Start elevated anyway? (you will be asked if you want to allow ModOrganizer.exe
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="173"/>
+ <location filename="selfupdater.cpp" line="178"/>
<source>New update available (%1)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="174"/>
+ <location filename="selfupdater.cpp" line="179"/>
<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="178"/>
+ <location filename="selfupdater.cpp" line="183"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="195"/>
+ <location filename="selfupdater.cpp" line="200"/>
<source>Download failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="196"/>
+ <location filename="selfupdater.cpp" line="201"/>
<source>Failed to find correct download, please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="211"/>
+ <location filename="selfupdater.cpp" line="216"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="212"/>
+ <location filename="selfupdater.cpp" line="217"/>
<source>Download in progress</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="303"/>
+ <location filename="selfupdater.cpp" line="308"/>
<source>Download failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="314"/>
+ <location filename="selfupdater.cpp" line="319"/>
<source>Failed to install update: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="339"/>
+ <location filename="selfupdater.cpp" line="344"/>
<source>Failed to start %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="349"/>
+ <location filename="selfupdater.cpp" line="353"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
@@ -5801,40 +6060,46 @@ Select Show Details option to see the full change-log.</source>
<context>
<name>Settings</name>
<message>
- <location filename="settings.cpp" line="146"/>
+ <location filename="settings.cpp" line="143"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="147"/>
+ <location filename="settings.cpp" line="144"/>
<source>Sorry, failed to start the helper application</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="495"/>
- <location filename="settings.cpp" line="514"/>
+ <location filename="settings.cpp" line="552"/>
+ <location filename="settings.cpp" line="571"/>
<source>attempt to store setting for unknown plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="877"/>
+ <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="878"/>
- <source>Failed to create &quot;%1&quot;, you may not have the necessary permission. path remains unchanged.</source>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1146"/>
- <source>Restart Mod Organizer?</source>
+ <location filename="settings.cpp" line="803"/>
+ <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="1147"/>
- <source>In order to reset the window geometries, MO must be restarted.
-Restart it now?</source>
+ <location filename="settings.cpp" line="987"/>
+ <source>Failed to create &quot;%1&quot;, you may not have the necessary permission. path remains unchanged.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settings.cpp" line="802"/>
+ <source>Restart Mod Organizer?</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -5915,319 +6180,353 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
</message>
<message>
<location filename="settingsdialog.ui" line="105"/>
- <source>Reset Colors</source>
+ <location filename="settingsdialog.ui" line="108"/>
+ <source>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="112"/>
- <source>Is overwritten (loose files)</source>
+ <location filename="settingsdialog.ui" line="111"/>
+ <source>Show mod list separator colors on the scrollbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="119"/>
- <source>Mod Contains selected Plugin</source>
+ <location filename="settingsdialog.ui" line="121"/>
+ <source>Plugin is Contained in selected Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="126"/>
- <source>Plugin is Contained in selected Mod</source>
+ <location filename="settingsdialog.ui" line="128"/>
+ <source>Is overwritten (loose files)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="133"/>
+ <location filename="settingsdialog.ui" line="135"/>
<source>Is overwriting (loose files)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="140"/>
- <location filename="settingsdialog.ui" line="143"/>
- <source>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</source>
+ <location filename="settingsdialog.ui" line="142"/>
+ <source>Reset Colors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="146"/>
- <source>Show mod list separator colors on the scrollbar</source>
+ <location filename="settingsdialog.ui" line="149"/>
+ <source>Mod Contains selected Plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="159"/>
- <location filename="settingsdialog.ui" line="162"/>
+ <location filename="settingsdialog.ui" line="156"/>
+ <source>Is overwritten (archive files)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="163"/>
+ <source>Is overwriting (archive files)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="173"/>
+ <location filename="settingsdialog.ui" line="176"/>
<source>Modify the categories available to arrange your mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="165"/>
+ <location filename="settingsdialog.ui" line="179"/>
<source>Configure Mod Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="178"/>
+ <location filename="settingsdialog.ui" line="192"/>
<source>Reset stored information from dialogs.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="181"/>
+ <location filename="settingsdialog.ui" line="195"/>
<source>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="184"/>
+ <location filename="settingsdialog.ui" line="198"/>
<source>Reset Dialogs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="191"/>
+ <location filename="settingsdialog.ui" line="205"/>
<source>If checked, the download interface will be more compact.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="194"/>
+ <location filename="settingsdialog.ui" line="208"/>
<source>Compact Download Interface</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="214"/>
+ <location filename="settingsdialog.ui" line="228"/>
<source>If checked, the download list will display meta information instead of file names.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="217"/>
+ <location filename="settingsdialog.ui" line="231"/>
<source>Download Meta Information</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="228"/>
+ <location filename="settingsdialog.ui" line="242"/>
<source>Paths</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="236"/>
- <location filename="settingsdialog.ui" line="253"/>
- <location filename="settingsdialog.ui" line="350"/>
- <location filename="settingsdialog.ui" line="408"/>
+ <location filename="settingsdialog.ui" line="250"/>
+ <location filename="settingsdialog.ui" line="267"/>
+ <location filename="settingsdialog.ui" line="364"/>
+ <location filename="settingsdialog.ui" line="422"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="263"/>
+ <location filename="settingsdialog.ui" line="277"/>
<source>Caches</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="270"/>
+ <location filename="settingsdialog.ui" line="284"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="277"/>
- <location filename="settingsdialog.ui" line="280"/>
+ <location filename="settingsdialog.ui" line="291"/>
+ <location filename="settingsdialog.ui" line="294"/>
<source>Directory where downloads are stored.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="297"/>
+ <location filename="settingsdialog.ui" line="311"/>
<source>Downloads</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="317"/>
+ <location filename="settingsdialog.ui" line="331"/>
<source>Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="340"/>
+ <location filename="settingsdialog.ui" line="354"/>
<source>Directory where mods are stored.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="343"/>
+ <location filename="settingsdialog.ui" line="357"/>
<source>Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don&apos;t exist in the new location (with the same name).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="367"/>
+ <location filename="settingsdialog.ui" line="381"/>
<source>Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="387"/>
+ <location filename="settingsdialog.ui" line="401"/>
<source>Managed Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="394"/>
+ <location filename="settingsdialog.ui" line="408"/>
<source>Base Directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="401"/>
+ <location filename="settingsdialog.ui" line="415"/>
<source>Use %BASE_DIR% to refer to the Base Directory.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="430"/>
+ <location filename="settingsdialog.ui" line="444"/>
<source>Important: All directories have to be writeable!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="438"/>
- <location filename="settingsdialog.ui" line="454"/>
+ <location filename="settingsdialog.ui" line="452"/>
+ <location filename="settingsdialog.ui" line="468"/>
<source>Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="444"/>
- <source>Allows automatic log-in when the Nexus-Page for the game is clicked.</source>
+ <location filename="settingsdialog.ui" line="458"/>
+ <source>Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="447"/>
+ <location filename="settingsdialog.ui" line="461"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &amp;quot;Connect to Nexus&amp;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <oldsource>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;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&apos;re worried someone might steal your password, don&apos;t store it here.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="463"/>
- <source>If checked and if correct credentials are entered below, log-in to Nexus (for browsing and downloading) is automatic.</source>
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;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&apos;re worried someone might steal your password, don&apos;t store it here.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="466"/>
- <source>Automatically Log-In to Nexus</source>
+ <location filename="settingsdialog.ui" line="476"/>
+ <source>Connect to Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="475"/>
- <location filename="settingsdialog.ui" line="661"/>
- <source>Username</source>
+ <location filename="settingsdialog.ui" line="500"/>
+ <source>Clear the stored Nexus API key and force reauthorization.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="489"/>
- <location filename="settingsdialog.ui" line="671"/>
- <source>Password</source>
+ <location filename="settingsdialog.ui" line="503"/>
+ <source>Revoke Nexus Authorization</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="510"/>
- <source>Remove cache and cookies. Forces a new login.</source>
+ <location filename="settingsdialog.ui" line="514"/>
+ <source>Remove cache and cookies.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="513"/>
+ <location filename="settingsdialog.ui" line="517"/>
<source>Clear Cache</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="542"/>
+ <location filename="settingsdialog.ui" line="548"/>
<source>Disable automatic internet features</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="545"/>
+ <location filename="settingsdialog.ui" line="551"/>
<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="548"/>
+ <location filename="settingsdialog.ui" line="554"/>
<source>Offline Mode</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="555"/>
+ <location filename="settingsdialog.ui" line="561"/>
<source>Use a proxy for network connections.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="558"/>
+ <location filename="settingsdialog.ui" line="564"/>
<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="561"/>
+ <location filename="settingsdialog.ui" line="567"/>
<source>Use HTTP Proxy (Uses System Settings)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="570"/>
+ <location filename="settingsdialog.ui" line="574"/>
+ <source>Endorsement Integration</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="584"/>
+ <location filename="settingsdialog.ui" line="587"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="590"/>
+ <source>Hide API Request Counter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="601"/>
<source>Associate with &quot;Download with manager&quot; links</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="599"/>
+ <location filename="settingsdialog.ui" line="630"/>
<source>Known Servers (updated on download)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="620"/>
+ <location filename="settingsdialog.ui" line="651"/>
<source>Preferred Servers (Drag &amp; Drop)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="655"/>
+ <location filename="settingsdialog.ui" line="686"/>
<source>Steam</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="701"/>
+ <location filename="settingsdialog.ui" line="692"/>
+ <source>Username</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="702"/>
+ <source>Password</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="732"/>
<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="728"/>
+ <location filename="settingsdialog.ui" line="759"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="753"/>
+ <location filename="settingsdialog.ui" line="784"/>
<source>Author:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="767"/>
+ <location filename="settingsdialog.ui" line="798"/>
<source>Version:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="781"/>
+ <location filename="settingsdialog.ui" line="812"/>
<source>Description:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="819"/>
+ <location filename="settingsdialog.ui" line="850"/>
<source>Key</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="824"/>
+ <location filename="settingsdialog.ui" line="855"/>
<source>Value</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="836"/>
+ <location filename="settingsdialog.ui" line="867"/>
<source>Blacklisted Plugins (use &lt;del&gt; to remove):</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="847"/>
+ <location filename="settingsdialog.ui" line="878"/>
<source>Workarounds</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="855"/>
+ <location filename="settingsdialog.ui" line="886"/>
<source>Steam App ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="875"/>
+ <location filename="settingsdialog.ui" line="906"/>
<source>The Steam AppID for your game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="878"/>
+ <location filename="settingsdialog.ui" line="909"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -6243,17 +6542,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="909"/>
+ <location filename="settingsdialog.ui" line="940"/>
<source>Load Mechanism</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="929"/>
+ <location filename="settingsdialog.ui" line="960"/>
<source>Select loading mechanism. See help for details.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="932"/>
+ <location filename="settingsdialog.ui" line="963"/>
<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.
@@ -6264,83 +6563,79 @@ 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="949"/>
- <source>NMM Version</source>
+ <location filename="settingsdialog.ui" line="983"/>
+ <source>Enforces that inactive ESPs and ESMs are never loaded.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="969"/>
- <source>The Version of Nexus Mod Manager to impersonate.</source>
+ <location filename="settingsdialog.ui" line="986"/>
+ <source>It seems that the Games occasionally load ESP or ESM files even if they haven&apos;t been activated as plugins.
+I don&apos;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="972"/>
- <source>Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in.
-On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn&apos;t need an update. Therefore you can configure the version to identify as here.
-Please note that MO does identify itself as MO to the webserver, it&apos;s not lying about what it is. It is merely adding a &quot;compatible&quot; NMM version to the user agent.
-
-tl;dr-version: If Nexus-features don&apos;t work, insert the current version number of NMM here and try again.</source>
+ <location filename="settingsdialog.ui" line="990"/>
+ <source>Hide inactive ESPs/ESMs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="996"/>
- <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>
+ <location filename="settingsdialog.ui" line="997"/>
+ <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="999"/>
- <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>
+ <location filename="settingsdialog.ui" line="1000"/>
+ <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&apos;t be resolved correctly.
+
+If you disable this feature, MO will only display official DLCs this way. Please note that plugins (esps, esms, and esls) displayed in the right pane are completely unaffected by this feature.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1003"/>
- <source>Force-enable game files</source>
+ <location filename="settingsdialog.ui" line="1006"/>
+ <source>Display mods installed outside MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <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>
+ <location filename="settingsdialog.ui" line="1016"/>
+ <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="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&apos;t be resolved correctly.
-
-If you disable this feature, MO will only display official DLCs this way. Please note that plugins (esps, esms, and esls) displayed in the right pane are completely unaffected by this feature.</source>
+ <location filename="settingsdialog.ui" line="1019"/>
+ <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="1022"/>
- <source>Display mods installed outside MO</source>
+ <location filename="settingsdialog.ui" line="1023"/>
+ <source>Force-enable game files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1035"/>
- <source>Enforces that inactive ESPs and ESMs are never loaded.</source>
+ <location filename="settingsdialog.ui" line="1033"/>
+ <location filename="settingsdialog.ui" line="1036"/>
+ <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="1038"/>
- <source>It seems that the Games occasionally load ESP or ESM files even if they haven&apos;t been activated as plugins.
-I don&apos;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>
+ <location filename="settingsdialog.ui" line="1039"/>
+ <source>Lock GUI when running executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1042"/>
- <source>Hide inactive ESPs/ESMs</source>
+ <location filename="settingsdialog.ui" line="1049"/>
+ <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="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>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;If you disable this feature, MO will only display conflicts between loose files.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="settingsdialog.ui" line="1055"/>
- <source>Lock GUI when running executable</source>
+ <source>Enable parsing of Archives (Experimental Feature)</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -6508,22 +6803,22 @@ programs you are intentionally running.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="83"/>
+ <location filename="settingsdialog.cpp" line="117"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="84"/>
+ <location filename="settingsdialog.cpp" line="118"/>
<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="128"/>
+ <location filename="settingsdialog.cpp" line="154"/>
<source>Executables Blacklist</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="129"/>
+ <location filename="settingsdialog.cpp" line="155"/>
<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.
@@ -6534,37 +6829,37 @@ Example:
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="162"/>
+ <location filename="settingsdialog.cpp" line="188"/>
<source>Select base directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="173"/>
+ <location filename="settingsdialog.cpp" line="199"/>
<source>Select download directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="184"/>
+ <location filename="settingsdialog.cpp" line="210"/>
<source>Select mod directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="195"/>
+ <location filename="settingsdialog.cpp" line="221"/>
<source>Select cache directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="206"/>
+ <location filename="settingsdialog.cpp" line="232"/>
<source>Select profiles directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="217"/>
+ <location filename="settingsdialog.cpp" line="243"/>
<source>Select overwrite directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="227"/>
+ <location filename="settingsdialog.cpp" line="253"/>
<source>Select game executable</source>
<translation type="unfinished"></translation>
</message>
@@ -6778,7 +7073,7 @@ On Windows XP:
<context>
<name>UsvfsConnector</name>
<message>
- <location filename="usvfsconnector.cpp" line="161"/>
+ <location filename="usvfsconnector.cpp" line="163"/>
<source>Preparing vfs</source>
<translation type="unfinished"></translation>
</message>
@@ -6990,13 +7285,16 @@ Please go along with the tutorial because some things can&apos;t be demonstrated
</message>
<message>
<location filename="tutorials/tutorial_firststeps_main.js" line="17"/>
- <source>The highlighted button provides hints on solving problems MO recognized automatically.</source>
+ <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 problem now but you may want to hold off on fixing it until after completing the tutorial.</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>
@@ -7192,7 +7490,8 @@ Please open the &quot;Nexus&quot;-tab</source>
</message>
<message>
<location filename="tutorials/tutorial_primer_main.js" line="84"/>
- <source>Reports potential Problems about the current setup.</source>
+ <source>Notifications about the current setup.</source>
+ <oldsource>Reports potential Problems about the current setup.</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index d52ef40b..8212d248 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -1,2339 +1,2528 @@
-#include "organizercore.h"
-
-#include "delayedfilewriter.h"
-#include "guessedvalue.h"
-#include "imodinterface.h"
-#include "imoinfo.h"
-#include "iplugingame.h"
-#include "iuserinterface.h"
-#include "loadmechanism.h"
-#include "messagedialog.h"
-#include "modlistsortproxy.h"
-#include "modrepositoryfileinfo.h"
-#include "nexusinterface.h"
-#include "plugincontainer.h"
-#include "pluginlistsortproxy.h"
-#include "profile.h"
-#include "logbuffer.h"
-#include "credentialsdialog.h"
-#include "filedialogmemory.h"
-#include "modinfodialog.h"
-#include "spawn.h"
-#include "syncoverwritedialog.h"
-#include "nxmaccessmanager.h"
-#include <ipluginmodpage.h>
-#include <dataarchives.h>
-#include <localsavegames.h>
-#include <directoryentry.h>
-#include <scopeguard.h>
-#include <utility.h>
-#include <usvfs.h>
-#include "appconfig.h"
-#include <report.h>
-#include <questionboxmemory.h>
-#include "lockeddialog.h"
-#include "instancemanager.h"
-#include <scriptextender.h>
-
-#include <QApplication>
-#include <QCoreApplication>
-#include <QDialog>
-#include <QDialogButtonBox>
-#include <QMessageBox>
-#include <QNetworkInterface>
-#include <QProcess>
-#include <QTimer>
-#include <QUrl>
-#include <QWidget>
-
-#include <QtDebug>
-#include <QtGlobal> // for qPrintable, etc
-
-#include <Psapi.h>
-#include <Shlobj.h>
-#include <tlhelp32.h>
-#include <tchar.h> // for _tcsicmp
-
-#include <limits.h>
-#include <stddef.h>
-#include <string.h> // for memset, wcsrchr
-
-#include <exception>
-#include <functional>
-#include <boost/algorithm/string/predicate.hpp>
-#include <memory>
-#include <set>
-#include <string> //for wstring
-#include <tuple>
-#include <utility>
-
-
-using namespace MOShared;
-using namespace MOBase;
-
-//static
-CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None;
-
-static bool isOnline()
-{
- QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
-
- bool connected = false;
- for (auto iter = interfaces.begin(); iter != interfaces.end() && !connected;
- ++iter) {
- if ((iter->flags() & QNetworkInterface::IsUp)
- && (iter->flags() & QNetworkInterface::IsRunning)
- && !(iter->flags() & QNetworkInterface::IsLoopBack)) {
- auto addresses = iter->addressEntries();
- if (addresses.count() == 0) {
- continue;
- }
- qDebug("interface %s seems to be up (address: %s)",
- qPrintable(iter->humanReadableName()),
- qPrintable(addresses[0].ip().toString()));
- connected = true;
- }
- }
-
- return connected;
-}
-
-static bool renameFile(const QString &oldName, const QString &newName,
- bool overwrite = true)
-{
- if (overwrite && QFile::exists(newName)) {
- QFile::remove(newName);
- }
- return QFile::rename(oldName, newName);
-}
-
-static std::wstring getProcessName(HANDLE process)
-{
- wchar_t buffer[MAX_PATH];
- wchar_t *fileName = L"unknown";
-
- if (process == nullptr) return fileName;
-
- if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) {
- fileName = wcsrchr(buffer, L'\\');
- if (fileName == nullptr) {
- fileName = buffer;
- }
- else {
- fileName += 1;
- }
- }
-
- return fileName;
-}
-
-// Get parent PID for the given process, return 0 on failure
-static DWORD getProcessParentID(DWORD pid)
-{
- HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- PROCESSENTRY32 pe = { 0 };
- pe.dwSize = sizeof(PROCESSENTRY32);
-
- DWORD res = 0;
- if (Process32First(th, &pe))
- do {
- if (pe.th32ProcessID == pid) {
- res = pe.th32ParentProcessID;
- break;
- }
- } while (Process32Next(th, &pe));
-
- CloseHandle(th);
-
- return res;
-}
-
-static void startSteam(QWidget *widget)
-{
- QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam",
- QSettings::NativeFormat);
- QString exe = steamSettings.value("SteamExe", "").toString();
- if (!exe.isEmpty()) {
- exe = QString("\"%1\"").arg(exe);
- // See if username and password supplied. If so, pass them into steam.
- QStringList args;
- QString username;
- QString password;
- if (Settings::instance().getSteamLogin(username, password)) {
- args << "-login";
- args << username;
- if (password != "") {
- args << password;
- }
- }
- if (!QProcess::startDetached(exe, args)) {
- reportError(QObject::tr("Failed to start \"%1\"").arg(exe));
- } else {
- QMessageBox::information(
- widget, QObject::tr("Waiting"),
- QObject::tr("Please press OK once you're logged into steam."));
- }
- }
-}
-
-template <typename InputIterator>
-QStringList toStringList(InputIterator current, InputIterator end)
-{
- QStringList result;
- for (; current != end; ++current) {
- result.append(*current);
- }
- return result;
-}
-
-bool checkService()
-{
- SC_HANDLE serviceManagerHandle = NULL;
- SC_HANDLE serviceHandle = NULL;
- LPSERVICE_STATUS_PROCESS serviceStatus = NULL;
- LPQUERY_SERVICE_CONFIG serviceConfig = NULL;
- bool serviceRunning = true;
-
- DWORD bytesNeeded;
-
- try {
- serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
- if (!serviceManagerHandle) {
- qWarning("failed to open service manager (query status) (error %d)", GetLastError());
- throw 1;
- }
-
- serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
- if (!serviceHandle) {
- qWarning("failed to open EventLog service (query status) (error %d)", GetLastError());
- throw 2;
- }
-
- if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded)
- || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
- qWarning("failed to get size of service config (error %d)", GetLastError());
- throw 3;
- }
-
- DWORD serviceConfigSize = bytesNeeded;
- serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize);
- if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) {
- qWarning("failed to query service config (error %d)", GetLastError());
- throw 4;
- }
-
- if (serviceConfig->dwStartType == SERVICE_DISABLED) {
- qCritical("Windows Event Log service is disabled!");
- serviceRunning = false;
- }
-
- if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded)
- || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
- qWarning("failed to get size of service status (error %d)", GetLastError());
- throw 5;
- }
-
- DWORD serviceStatusSize = bytesNeeded;
- serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize);
- if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) {
- qWarning("failed to query service status (error %d)", GetLastError());
- throw 6;
- }
-
- if (serviceStatus->dwCurrentState != SERVICE_RUNNING) {
- qCritical("Windows Event Log service is not running");
- serviceRunning = false;
- }
- }
- catch (int e) {
- UNUSED_VAR(e);
- serviceRunning = false;
- }
-
- if (serviceStatus) {
- LocalFree(serviceStatus);
- }
- if (serviceConfig) {
- LocalFree(serviceConfig);
- }
- if (serviceHandle) {
- CloseServiceHandle(serviceHandle);
- }
- if (serviceManagerHandle) {
- CloseServiceHandle(serviceManagerHandle);
- }
-
- return serviceRunning;
-}
-
-OrganizerCore::OrganizerCore(const QSettings &initSettings)
- : m_UserInterface(nullptr)
- , m_PluginContainer(nullptr)
- , m_GameName()
- , m_CurrentProfile(nullptr)
- , m_Settings(initSettings)
- , m_Updater(NexusInterface::instance(m_PluginContainer))
- , m_AboutToRun()
- , m_FinishedRun()
- , m_ModInstalled()
- , m_ModList(m_PluginContainer, this)
- , m_PluginList(this)
- , m_DirectoryRefresher()
- , m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0))
- , m_DownloadManager(NexusInterface::instance(m_PluginContainer), this)
- , m_InstallationManager()
- , m_RefresherThread()
- , m_AskForNexusPW(false)
- , m_DirectoryUpdate(false)
- , m_ArchivesInit(false)
- , m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this))
-{
- m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory());
- m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers());
-
- NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory());
- NexusInterface::instance(m_PluginContainer)->setNMMVersion(m_Settings.getNMMVersion());
-
- MOBase::QuestionBoxMemory::init(initSettings.fileName());
-
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
- m_InstallationManager.setDownloadDirectory(m_Settings.getDownloadDirectory());
-
- connect(&m_DownloadManager, SIGNAL(downloadSpeed(QString, int)), this,
- SLOT(downloadSpeed(QString, int)));
- connect(&m_DirectoryRefresher, SIGNAL(refreshed()), this,
- SLOT(directory_refreshed()));
-
- connect(&m_ModList, SIGNAL(removeOrigin(QString)), this,
- SLOT(removeOrigin(QString)));
-
- connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(),
- SIGNAL(loginSuccessful(bool)), this, SLOT(loginSuccessful(bool)));
- connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(),
- SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString)));
-
- // This seems awfully imperative
- connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)),
- &m_Settings, SLOT(managedGameChanged(MOBase::IPluginGame const *)));
- connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)),
- &m_DownloadManager,
- SLOT(managedGameChanged(MOBase::IPluginGame const *)));
- connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)),
- &m_PluginList, SLOT(managedGameChanged(MOBase::IPluginGame const *)));
-
- connect(&m_PluginList, &PluginList::writePluginsList, &m_PluginListsWriter,
- &DelayedFileWriterBase::write);
-
- // make directory refresher run in a separate thread
- m_RefresherThread.start();
- m_DirectoryRefresher.moveToThread(&m_RefresherThread);
-
- m_AskForNexusPW = initSettings.value("ask_for_nexuspw", true).toBool();
-}
-
-OrganizerCore::~OrganizerCore()
-{
- m_RefresherThread.exit();
- m_RefresherThread.wait();
-
- prepareStart();
-
- // profile has to be cleaned up before the modinfo-buffer is cleared
- delete m_CurrentProfile;
- m_CurrentProfile = nullptr;
-
- ModInfo::clear();
- LogBuffer::cleanQuit();
- m_ModList.setProfile(nullptr);
- // NexusInterface::instance()->cleanup();
-
- delete m_DirectoryStructure;
-}
-
-QString OrganizerCore::commitSettings(const QString &iniFile)
-{
- if (!shellRename(iniFile + ".new", iniFile, true, qApp->activeWindow())) {
- DWORD err = ::GetLastError();
- // make a second attempt using qt functions but if that fails print the
- // error from the first attempt
- if (!renameFile(iniFile + ".new", iniFile)) {
- return windowsErrorString(err);
- }
- }
- return QString();
-}
-
-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.setValue("ask_for_nexuspw", m_AskForNexusPW);
-
- 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();
-
- FileDialogMemory::save(settings);
-
- settings.sync();
- return settings.status();
-}
-
-void OrganizerCore::storeSettings()
-{
- QString iniFile = qApp->property("dataPath").toString() + "/"
- + QString::fromStdWString(AppConfig::iniFileName());
- if (QFileInfo(iniFile).exists()) {
- 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")
- .arg(iniFile, windowsErrorString(::GetLastError())));
- return;
- }
- }
-
- QString writeTarget = iniFile + ".new";
-
- QSettings::Status result = storeSettings(writeTarget);
-
- if (result == QSettings::NoError) {
- QString errMsg = commitSettings(iniFile);
- if (!errMsg.isEmpty()) {
- qWarning("settings file not writable, may be locked by another "
- "application, trying direct write");
- writeTarget = iniFile;
- result = storeSettings(iniFile);
- }
- }
- if (result != QSettings::NoError) {
- QString reason = result == QSettings::AccessError
- ? tr("File is write protected")
- : result == QSettings::FormatError
- ? tr("Invalid file format (probably a bug)")
- : 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")
- .arg(writeTarget, reason));
- }
-}
-
-bool OrganizerCore::testForSteam()
-{
- size_t currentSize = 1024;
- std::unique_ptr<DWORD[]> processIDs;
- DWORD bytesReturned;
- bool success = false;
- while (!success) {
- processIDs.reset(new DWORD[currentSize]);
- if (!::EnumProcesses(processIDs.get(),
- static_cast<DWORD>(currentSize) * sizeof(DWORD),
- &bytesReturned)) {
- qWarning("failed to determine if steam is running");
- return true;
- }
- if (bytesReturned == (currentSize * sizeof(DWORD))) {
- // maximum size used, list probably truncated
- currentSize *= 2;
- } else {
- success = true;
- }
- }
- TCHAR processName[MAX_PATH];
- for (unsigned int i = 0; i < bytesReturned / sizeof(DWORD); ++i) {
- memset(processName, '\0', sizeof(TCHAR) * MAX_PATH);
- if (processIDs[i] != 0) {
- HANDLE process = ::OpenProcess(
- PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processIDs[i]);
-
- if (process != nullptr) {
-
- ON_BLOCK_EXIT([&]() {
- if (process != INVALID_HANDLE_VALUE)
- ::CloseHandle(process);
- });
-
- HMODULE module;
- DWORD ignore;
-
- // first module in a process is always the binary
- if (EnumProcessModules(process, &module, sizeof(HMODULE) * 1,
- &ignore)) {
- ::GetModuleBaseName(process, module, processName, MAX_PATH);
- if ((_tcsicmp(processName, TEXT("steam.exe")) == 0)
- || (_tcsicmp(processName, TEXT("steamservice.exe")) == 0)) {
- return true;
- }
- }
- }
- }
- }
-
- return false;
-}
-
-void OrganizerCore::updateExecutablesList(QSettings &settings)
-{
- if (m_PluginContainer == nullptr) {
- qCritical("can't update executables list now");
- 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();
-
- // TODO this has nothing to do with executables list move to an appropriate
- // function!
- ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.displayForeign(), managedGame());
-}
-
-void OrganizerCore::setUserInterface(IUserInterface *userInterface,
- QWidget *widget)
-{
- storeSettings();
-
- m_UserInterface = userInterface;
-
- if (widget != nullptr) {
- connect(&m_ModList, SIGNAL(modlist_changed(QModelIndex, int)), widget,
- SLOT(modlistChanged(QModelIndex, int)));
- connect(&m_ModList, SIGNAL(showMessage(QString)), widget,
- SLOT(showMessage(QString)));
- connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), widget,
- SLOT(modRenamed(QString, QString)));
- connect(&m_ModList, SIGNAL(modUninstalled(QString)), widget,
- SLOT(modRemoved(QString)));
- connect(&m_ModList, SIGNAL(removeSelectedMods()), widget,
- SLOT(removeMod_clicked()));
- connect(&m_ModList, SIGNAL(clearOverwrite()), widget,
- SLOT(clearOverwrite()));
- connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget,
- SLOT(displayColumnSelection(QPoint)));
- connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget,
- SLOT(fileMoved(QString, QString, QString)));
- connect(&m_ModList, SIGNAL(modorder_changed()), widget,
- SLOT(modorder_changed()));
- connect(&m_DownloadManager, SIGNAL(showMessage(QString)), widget,
- SLOT(showMessage(QString)));
- }
-
- m_InstallationManager.setParentWidget(widget);
- m_Updater.setUserInterface(widget);
-
- if (userInterface != nullptr) {
- // this currently wouldn't work reliably if the ui isn't initialized yet to
- // display the result
- if (isOnline() && !m_Settings.offlineMode()) {
- m_Updater.testForUpdate();
- } else {
- qDebug("user doesn't seem to be connected to the internet");
- }
- }
-}
-
-void OrganizerCore::connectPlugins(PluginContainer *container)
-{
- m_DownloadManager.setSupportedExtensions(
- m_InstallationManager.getSupportedExtensions());
- m_PluginContainer = container;
- m_Updater.setPluginContainer(m_PluginContainer);
- m_DownloadManager.setPluginContainer(m_PluginContainer);
- m_ModList.setPluginContainer(m_PluginContainer);
-
- if (!m_GameName.isEmpty()) {
- m_GamePlugin = m_PluginContainer->managedGame(m_GameName);
- emit managedGameChanged(m_GamePlugin);
- }
-}
-
-void OrganizerCore::disconnectPlugins()
-{
- m_AboutToRun.disconnect_all_slots();
- m_FinishedRun.disconnect_all_slots();
- m_ModInstalled.disconnect_all_slots();
- m_ModList.disconnectSlots();
- m_PluginList.disconnectSlots();
- m_Updater.setPluginContainer(nullptr);
- m_DownloadManager.setPluginContainer(nullptr);
- m_ModList.setPluginContainer(nullptr);
-
- m_Settings.clearPlugins();
- m_GamePlugin = nullptr;
- m_PluginContainer = nullptr;
-}
-
-void OrganizerCore::setManagedGame(MOBase::IPluginGame *game)
-{
- m_GameName = game->gameName();
- m_GamePlugin = game;
- qApp->setProperty("managed_game", QVariant::fromValue(m_GamePlugin));
- emit managedGameChanged(m_GamePlugin);
-}
-
-Settings &OrganizerCore::settings()
-{
- return m_Settings;
-}
-
-bool OrganizerCore::nexusLogin(bool retry)
-{
- NXMAccessManager *accessManager
- = NexusInterface::instance(m_PluginContainer)->getAccessManager();
-
- if ((accessManager->loginAttempted() || accessManager->loggedIn())
- && !retry) {
- // previous attempt, maybe even successful
- return false;
- } else {
- QString username, password;
- if ((!retry && m_Settings.getNexusLogin(username, password))
- || (m_AskForNexusPW && queryLogin(username, password))) {
- // credentials stored or user entered them manually
- qDebug("attempt login with username %s", qPrintable(username));
- accessManager->login(username, password);
- return true;
- } else {
- // no credentials stored and user didn't enter them
- accessManager->refuseLogin();
- return false;
- }
- }
-}
-
-bool OrganizerCore::queryLogin(QString &username, QString &password)
-{
- CredentialsDialog dialog(qApp->activeWindow());
- int res = dialog.exec();
- if (dialog.neverAsk()) {
- m_AskForNexusPW = false;
- }
- if (res == QDialog::Accepted) {
- username = dialog.username();
- password = dialog.password();
- if (dialog.store()) {
- m_Settings.setNexusLogin(username, password);
- }
- return true;
- } else {
- return false;
- }
-}
-
-void OrganizerCore::startMOUpdate()
-{
- if (nexusLogin()) {
- m_PostLoginTasks.append([&]() { m_Updater.startUpdate(); });
- } else {
- m_Updater.startUpdate();
- }
-}
-
-void OrganizerCore::downloadRequestedNXM(const QString &url)
-{
- qDebug("download requested: %s", qPrintable(url));
- if (nexusLogin()) {
- m_PendingDownloads.append(url);
- } else {
- m_DownloadManager.addNXMDownload(url);
- }
-}
-
-void OrganizerCore::externalMessage(const QString &message)
-{
- if (MOShortcut moshortcut{ message } ) {
- if(moshortcut.hasExecutable())
- runShortcut(moshortcut);
- }
- else if (isNxmLink(message)) {
- MessageDialog::showMessage(tr("Download started"), qApp->activeWindow());
- downloadRequestedNXM(message);
- }
-}
-
-void OrganizerCore::downloadRequested(QNetworkReply *reply, QString gameName, int modID,
- const QString &fileName)
-{
- try {
- if (m_DownloadManager.addDownload(reply, QStringList(), fileName, gameName, modID, 0,
- new ModRepositoryFileInfo(gameName, modID))) {
- MessageDialog::showMessage(tr("Download started"), qApp->activeWindow());
- }
- } catch (const std::exception &e) {
- MessageDialog::showMessage(tr("Download failed"), qApp->activeWindow());
- qCritical("exception starting download: %s", e.what());
- }
-}
-
-void OrganizerCore::removeOrigin(const QString &name)
-{
- FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name));
- origin.enable(false);
- refreshLists();
-}
-
-void OrganizerCore::downloadSpeed(const QString &serverName, int bytesPerSecond)
-{
- m_Settings.setDownloadSpeed(serverName, bytesPerSecond);
-}
-
-InstallationManager *OrganizerCore::installationManager()
-{
- return &m_InstallationManager;
-}
-
-bool OrganizerCore::createDirectory(const QString &path) {
- if (!QDir(path).exists() && !QDir().mkpath(path)) {
- QMessageBox::critical(nullptr, QObject::tr("Error"),
- QObject::tr("Failed to create \"%1\". Your user "
- "account probably lacks permission.")
- .arg(QDir::toNativeSeparators(path)));
- return false;
- } else {
- 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();
-}
-
-void OrganizerCore::createDefaultProfile()
-{
- QString profilesPath = settings().getProfileDirectory();
- if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size()
- == 0) {
- Profile newProf("Default", managedGame(), false);
- }
-}
-
-void OrganizerCore::prepareVFS()
-{
- m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
-}
-
-void OrganizerCore::updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist) {
- setGlobalCrashDumpsType(crashDumpsType);
- m_USVFS.updateParams(logLevel, crashDumpsType, executableBlacklist);
-}
-
-bool OrganizerCore::cycleDiagnostics() {
- if (int maxDumps = settings().crashDumpsMax())
- removeOldFiles(QString::fromStdWString(crashDumpsPath()), "*.dmp", maxDumps, QDir::Time|QDir::Reversed);
- return true;
-}
-
-//static
-void OrganizerCore::setGlobalCrashDumpsType(int crashDumpsType) {
- m_globalCrashDumpsType = ::crashDumpsType(crashDumpsType);
-}
-
-//static
-std::wstring OrganizerCore::crashDumpsPath() {
- return (
- qApp->property("dataPath").toString() + "/"
- + QString::fromStdWString(AppConfig::dumpsDir())
- ).toStdWString();
-}
-
-void OrganizerCore::setCurrentProfile(const QString &profileName)
-{
- if ((m_CurrentProfile != nullptr)
- && (profileName == m_CurrentProfile->name())) {
- return;
- }
-
- QDir profileBaseDir(settings().getProfileDirectory());
- QString profileDir = profileBaseDir.absoluteFilePath(profileName);
-
- if (!QDir(profileDir).exists()) {
- // selected profile doesn't exist. Ensure there is at least one profile,
- // then pick any one
- createDefaultProfile();
-
- profileDir = profileBaseDir.absoluteFilePath(
- profileBaseDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot).at(0));
- }
-
- Profile *newProfile = new Profile(QDir(profileDir), managedGame());
-
- delete m_CurrentProfile;
- m_CurrentProfile = newProfile;
- m_ModList.setProfile(newProfile);
-
- if (m_CurrentProfile->invalidationActive(nullptr)) {
- m_CurrentProfile->activateInvalidation();
- } else {
- m_CurrentProfile->deactivateInvalidation();
- }
-
- connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this,
- SLOT(modStatusChanged(uint)));
- refreshDirectoryStructure();
-}
-
-MOBase::IModRepositoryBridge *OrganizerCore::createNexusBridge() const
-{
- return new NexusBridge(m_PluginContainer);
-}
-
-QString OrganizerCore::profileName() const
-{
- if (m_CurrentProfile != nullptr) {
- return m_CurrentProfile->name();
- } else {
- return "";
- }
-}
-
-QString OrganizerCore::profilePath() const
-{
- if (m_CurrentProfile != nullptr) {
- return m_CurrentProfile->absolutePath();
- } else {
- return "";
- }
-}
-
-QString OrganizerCore::downloadsPath() const
-{
- return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory());
-}
-
-QString OrganizerCore::overwritePath() const
-{
- return QDir::fromNativeSeparators(m_Settings.getOverwriteDirectory());
-}
-
-QString OrganizerCore::basePath() const
-{
- return QDir::fromNativeSeparators(m_Settings.getBaseDirectory());
-}
-
-MOBase::VersionInfo OrganizerCore::appVersion() const
-{
- return m_Updater.getVersion();
-}
-
-MOBase::IModInterface *OrganizerCore::getMod(const QString &name) const
-{
- unsigned int index = ModInfo::getIndex(name);
- return index == UINT_MAX ? nullptr : ModInfo::getByIndex(index).data();
-}
-
-MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const
-{
- for (IPluginGame *game : m_PluginContainer->plugins<IPluginGame>()) {
- if (game->gameShortName().compare(name, Qt::CaseInsensitive) == 0)
- return game;
- }
- return nullptr;
-}
-
-MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &name)
-{
- bool merge = false;
- if (!m_InstallationManager.testOverwrite(name, &merge)) {
- return nullptr;
- }
-
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
-
- QString targetDirectory
- = QDir::fromNativeSeparators(m_Settings.getModDirectory())
- .append("/")
- .append(name);
-
- QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat);
-
- if (!merge) {
- settingsFile.setValue("modid", 0);
- settingsFile.setValue("version", "");
- settingsFile.setValue("newestVersion", "");
- settingsFile.setValue("category", 0);
- settingsFile.setValue("installationFile", "");
-
- settingsFile.beginWriteArray("installedFiles", 0);
- settingsFile.endArray();
- }
-
- return ModInfo::createFrom(m_PluginContainer, m_GamePlugin, QDir(targetDirectory), &m_DirectoryStructure)
- .data();
-}
-
-bool OrganizerCore::removeMod(MOBase::IModInterface *mod)
-{
- unsigned int index = ModInfo::getIndex(mod->name());
- if (index == UINT_MAX) {
- return mod->remove();
- } else {
- return ModInfo::removeMod(index);
- }
-}
-
-void OrganizerCore::modDataChanged(MOBase::IModInterface *)
-{
- refreshModList(false);
-}
-
-QVariant OrganizerCore::pluginSetting(const QString &pluginName,
- const QString &key) const
-{
- return m_Settings.pluginSetting(pluginName, key);
-}
-
-void OrganizerCore::setPluginSetting(const QString &pluginName,
- const QString &key, const QVariant &value)
-{
- m_Settings.setPluginSetting(pluginName, key, value);
-}
-
-QVariant OrganizerCore::persistent(const QString &pluginName,
- const QString &key,
- const QVariant &def) const
-{
- return m_Settings.pluginPersistent(pluginName, key, def);
-}
-
-void OrganizerCore::setPersistent(const QString &pluginName, const QString &key,
- const QVariant &value, bool sync)
-{
- m_Settings.setPluginPersistent(pluginName, key, value, sync);
-}
-
-QString OrganizerCore::pluginDataPath() const
-{
- return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())
- + "/data";
-}
-
-MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName,
- const QString &initModName)
-{
- if (m_CurrentProfile == nullptr) {
- return nullptr;
- }
-
- bool hasIniTweaks = false;
- GuessedValue<QString> modName;
- if (!initModName.isEmpty()) {
- modName.update(initModName, GUESS_USER);
- }
- m_CurrentProfile->writeModlistNow();
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
- if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) {
- MessageDialog::showMessage(tr("Installation successful"),
- qApp->activeWindow());
- refreshModList();
-
- int modIndex = ModInfo::getIndex(modName);
- if (modIndex != UINT_MAX) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- if (hasIniTweaks && (m_UserInterface != nullptr)
- && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"),
- tr("This mod contains ini tweaks. Do you "
- "want to configure them now?"),
- QMessageBox::Yes | QMessageBox::No)
- == QMessageBox::Yes)) {
- m_UserInterface->displayModInformation(modInfo, modIndex,
- ModInfoDialog::TAB_INIFILES);
- }
- m_ModInstalled(modName);
- m_DownloadManager.markInstalled(fileName);
- emit modInstalled(modName);
- return modInfo.data();
- } else {
- reportError(tr("mod \"%1\" not found").arg(modName));
- }
- } else if (m_InstallationManager.wasCancelled()) {
- QMessageBox::information(qApp->activeWindow(), tr("Installation cancelled"),
- tr("The mod was not installed completely."),
- QMessageBox::Ok);
- }
- return nullptr;
-}
-
-void OrganizerCore::installDownload(int index)
-{
- try {
- QString fileName = m_DownloadManager.getFilePath(index);
- QString gameName = m_DownloadManager.getGameName(index);
- int modID = m_DownloadManager.getModID(index);
- int fileID = m_DownloadManager.getFileInfo(index)->fileID;
- GuessedValue<QString> modName;
-
- // see if there already are mods with the specified mod id
- if (modID != 0) {
- std::vector<ModInfo::Ptr> modInfo = ModInfo::getByModID(gameName, modID);
- for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) {
- std::vector<ModInfo::EFlag> flags = (*iter)->getFlags();
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP)
- == flags.end()) {
- modName.update((*iter)->name(), GUESS_PRESET);
- (*iter)->saveMeta();
- }
- }
- }
-
- m_CurrentProfile->writeModlistNow();
-
- bool hasIniTweaks = false;
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
- if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) {
- MessageDialog::showMessage(tr("Installation successful"),
- qApp->activeWindow());
- refreshModList();
-
- int modIndex = ModInfo::getIndex(modName);
- if (modIndex != UINT_MAX) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- modInfo->addInstalledFile(modID, fileID);
-
- if (hasIniTweaks && m_UserInterface != nullptr
- && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"),
- tr("This mod contains ini tweaks. Do you "
- "want to configure them now?"),
- QMessageBox::Yes | QMessageBox::No)
- == QMessageBox::Yes)) {
- m_UserInterface->displayModInformation(modInfo, modIndex,
- ModInfoDialog::TAB_INIFILES);
- }
-
- m_ModInstalled(modName);
- } else {
- reportError(tr("mod \"%1\" not found").arg(modName));
- }
- m_DownloadManager.markInstalled(index);
-
- emit modInstalled(modName);
- } else if (m_InstallationManager.wasCancelled()) {
- QMessageBox::information(
- qApp->activeWindow(), tr("Installation cancelled"),
- tr("The mod was not installed completely."), QMessageBox::Ok);
- }
- } catch (const std::exception &e) {
- reportError(e.what());
- }
-}
-
-QString OrganizerCore::resolvePath(const QString &fileName) const
-{
- if (m_DirectoryStructure == nullptr) {
- return QString();
- }
- const FileEntry::Ptr file
- = m_DirectoryStructure->searchFile(ToWString(fileName), nullptr);
- if (file.get() != nullptr) {
- return ToQString(file->getFullPath());
- } else {
- return QString();
- }
-}
-
-QStringList OrganizerCore::listDirectories(const QString &directoryName) const
-{
- QStringList result;
- DirectoryEntry *dir = m_DirectoryStructure->findSubDirectoryRecursive(
- ToWString(directoryName));
- if (dir != nullptr) {
- std::vector<DirectoryEntry *>::iterator current, end;
- dir->getSubDirectories(current, end);
- for (; current != end; ++current) {
- result.append(ToQString((*current)->getName()));
- }
- }
- return result;
-}
-
-QStringList OrganizerCore::findFiles(
- const QString &path,
- const std::function<bool(const QString &)> &filter) const
-{
- QStringList result;
- DirectoryEntry *dir
- = m_DirectoryStructure->findSubDirectoryRecursive(ToWString(path));
- if (dir != nullptr) {
- std::vector<FileEntry::Ptr> files = dir->getFiles();
- foreach (FileEntry::Ptr file, files) {
- if (filter(ToQString(file->getFullPath()))) {
- result.append(ToQString(file->getFullPath()));
- }
- }
- } else {
- qWarning("directory %s not found", qPrintable(path));
- }
- return result;
-}
-
-QStringList OrganizerCore::getFileOrigins(const QString &fileName) const
-{
- QStringList result;
- const FileEntry::Ptr file = m_DirectoryStructure->searchFile(
- ToWString(QFileInfo(fileName).fileName()), nullptr);
-
- if (file.get() != nullptr) {
- result.append(ToQString(
- m_DirectoryStructure->getOriginByID(file->getOrigin()).getName()));
- foreach (auto i, file->getAlternatives()) {
- result.append(
- ToQString(m_DirectoryStructure->getOriginByID(i.first).getName()));
- }
- } else {
- qDebug("%s not found", qPrintable(fileName));
- }
- return result;
-}
-
-QList<MOBase::IOrganizer::FileInfo> OrganizerCore::findFileInfos(
- const QString &path,
- const std::function<bool(const MOBase::IOrganizer::FileInfo &)> &filter)
- const
-{
- QList<IOrganizer::FileInfo> result;
- DirectoryEntry *dir
- = m_DirectoryStructure->findSubDirectoryRecursive(ToWString(path));
- if (dir != nullptr) {
- std::vector<FileEntry::Ptr> files = dir->getFiles();
- foreach (FileEntry::Ptr file, files) {
- IOrganizer::FileInfo info;
- info.filePath = ToQString(file->getFullPath());
- bool fromArchive = false;
- info.origins.append(ToQString(
- m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive))
- .getName()));
- info.archive = fromArchive ? ToQString(file->getArchive()) : "";
- foreach (auto idx, file->getAlternatives()) {
- info.origins.append(
- ToQString(m_DirectoryStructure->getOriginByID(idx.first).getName()));
- }
-
- if (filter(info)) {
- result.append(info);
- }
- }
- }
- return result;
-}
-
-DownloadManager *OrganizerCore::downloadManager()
-{
- return &m_DownloadManager;
-}
-
-PluginList *OrganizerCore::pluginList()
-{
- return &m_PluginList;
-}
-
-ModList *OrganizerCore::modList()
-{
- return &m_ModList;
-}
-
-QStringList OrganizerCore::modsSortedByProfilePriority() const
-{
- QStringList res;
- for (int i = currentProfile()->getPriorityMinimum();
- i < currentProfile()->getPriorityMinimum() + (int)currentProfile()->numRegularMods();
- ++i) {
- int modIndex = currentProfile()->modIndexByPriority(i);
- res.push_back(ModInfo::getByIndex(modIndex)->name());
- }
- return res;
-}
-
-void OrganizerCore::spawnBinary(const QFileInfo &binary, const QString &arguments, const QDir &currentDirectory, const QString &steamAppID, const QString &customOverwrite)
-{
- DWORD processExitCode = 0;
- HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, &processExitCode);
- if (processHandle != INVALID_HANDLE_VALUE) {
- refreshDirectoryStructure();
- // need to remove our stored load order because it may be outdated if a foreign tool changed the
- // file time. After removing that file, refreshESPList will use the file time as the order
- if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) {
- qDebug("removing loadorder.txt");
- QFile::remove(m_CurrentProfile->getLoadOrderFileName());
- }
- refreshDirectoryStructure();
-
- refreshESPList(true);
- savePluginList();
-
- //These callbacks should not fiddle with directoy structure and ESPs.
- m_FinishedRun(binary.absoluteFilePath(), processExitCode);
- }
-}
-
-HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary,
- const QString &arguments,
- const QString &profileName,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- LPDWORD exitCode)
-{
- HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite);
- if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) {
- std::unique_ptr<LockedDialog> dlg;
- ILockedWaitingForProcess* uilock = nullptr;
-
- if (m_UserInterface != nullptr) {
- uilock = m_UserInterface->lock();
- }
- else {
- // i.e. when running command line shortcuts there is no m_UserInterface
- dlg.reset(new LockedDialog);
- dlg->show();
- dlg->setEnabled(true);
- uilock = dlg.get();
- }
-
- ON_BLOCK_EXIT([&]() {
- if (m_UserInterface != nullptr) {
- m_UserInterface->unlock();
- } });
-
- DWORD ignoreExitCode;
- waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock);
- cycleDiagnostics();
- }
-
- return processHandle;
-}
-
-
-HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary,
- const QString &arguments,
- const QString &profileName,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite)
-{
- prepareStart();
-
- if (!binary.exists()) {
- reportError(
- tr("Executable \"%1\" not found").arg(binary.absoluteFilePath()));
- return INVALID_HANDLE_VALUE;
- }
-
- if (!steamAppID.isEmpty()) {
- ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str());
- } else {
- ::SetEnvironmentVariableW(L"SteamAPPId",
- ToWString(m_Settings.getSteamAppID()).c_str());
- }
-
- QWidget *window = qApp->activeWindow();
- if ((window != nullptr) && (!window->isVisible())) {
- window = nullptr;
- }
-
- // This could possibly be extracted somewhere else but it's probably for when
- // we have more than one provider of game registration.
- if ((QFileInfo(
- managedGame()->gameDirectory().absoluteFilePath("steam_api.dll"))
- .exists()
- || QFileInfo(managedGame()->gameDirectory().absoluteFilePath(
- "steam_api64.dll"))
- .exists())
- && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) {
- if (!testForSteam()) {
- QDialogButtonBox::StandardButton result;
- result = QuestionBoxMemory::query(window, "steamQuery", binary.fileName(),
- tr("Start Steam?"),
- tr("Steam is required to be running already to correctly start the game. "
- "Should MO try to start steam now?"),
- QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
- if (result == QDialogButtonBox::Yes) {
- startSteam(window);
- } else if(result == QDialogButtonBox::Cancel) {
- return INVALID_HANDLE_VALUE;
- }
- }
- }
-
- while (m_DirectoryUpdate) {
- ::Sleep(100);
- QCoreApplication::processEvents();
- }
-
- // need to make sure all data is saved before we start the application
- if (m_CurrentProfile != nullptr) {
- m_CurrentProfile->writeModlistNow(true);
- }
-
- // TODO: should also pass arguments
- if (m_AboutToRun(binary.absoluteFilePath())) {
- try {
- m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
- } catch (const UsvfsConnectorException &e) {
- qDebug(e.what());
- return INVALID_HANDLE_VALUE;
- } catch (const std::exception &e) {
- QMessageBox::warning(window, tr("Error"), e.what());
- return INVALID_HANDLE_VALUE;
- }
-
- // Check if the Windows Event Logging service is running. For some reason, this seems to be
- // critical to the successful running of usvfs.
- if (!checkService()) {
- if (QuestionBoxMemory::query(window, QString("eventLogService"), binary.fileName(),
- tr("Windows Event Log Error"),
- tr("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.\n\nContinue launching %1?").arg(binary.fileName()),
- QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) {
- return INVALID_HANDLE_VALUE;
- }
- }
-
- for (auto exec : settings().executablesBlacklist().split(";")) {
- if (exec.compare(binary.fileName(), Qt::CaseInsensitive) == 0) {
- if (QuestionBoxMemory::query(window, QString("blacklistedExecutable"), binary.fileName(),
- tr("Blacklisted Executable"),
- tr("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.\n\nContinue launching %1?").arg(binary.fileName()),
- QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) {
- return INVALID_HANDLE_VALUE;
- }
- }
- }
-
- QString modsPath = settings().getModDirectory();
-
- // Check if this a request with either an executable or a working directory under our mods folder
- // then will start the process in a virtualized "environment" with the appropriate paths fixed:
- // (i.e. mods\FNIS\path\exe => game\data\path\exe)
- QString cwdPath = currentDirectory.absolutePath();
- bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive);
- QString binPath = binary.absoluteFilePath();
- bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive);
- if (virtualizedCwd || virtualizedBin) {
- if (virtualizedCwd) {
- int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1);
- QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
- cwdPath = m_GamePlugin->dataDirectory().absolutePath();
- if (cwdOffset >= 0)
- cwdPath += adjustedCwd;
-
- }
-
- if (virtualizedBin) {
- int binOffset = binPath.indexOf('/', modsPath.length() + 1);
- QString adjustedBin = binPath.mid(binOffset, -1);
- binPath = m_GamePlugin->dataDirectory().absolutePath();
- if (binOffset >= 0)
- binPath += adjustedBin;
- }
-
- QString cmdline
- = QString("launch \"%1\" \"%2\" %3")
- .arg(QDir::toNativeSeparators(cwdPath),
- QDir::toNativeSeparators(binPath), arguments);
-
- qDebug() << "Spawning proxyed process <" << cmdline << ">";
-
- return startBinary(QFileInfo(QCoreApplication::applicationFilePath()),
- cmdline, QCoreApplication::applicationDirPath(), true);
- } else {
- qDebug() << "Spawning direct process <" << binPath << "," << arguments << "," << cwdPath << ">";
- return startBinary(binary, arguments, currentDirectory, true);
- }
- } else {
- qDebug("start of \"%s\" canceled by plugin",
- qPrintable(binary.absoluteFilePath()));
- return INVALID_HANDLE_VALUE;
- }
-}
-
-HANDLE OrganizerCore::runShortcut(const MOShortcut& shortcut)
-{
- if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance())
- throw std::runtime_error(
- QString("Refusing to run executable from different instance %1:%2")
- .arg(shortcut.instance(),shortcut.executable())
- .toLocal8Bit().constData());
-
- Executable& exe = m_ExecutablesList.find(shortcut.executable());
-
- return spawnBinaryDirect(
- exe.m_BinaryInfo, exe.m_Arguments,
- m_CurrentProfile->name(),
- exe.m_WorkingDirectory.length() != 0
- ? exe.m_WorkingDirectory
- : exe.m_BinaryInfo.absolutePath(),
- exe.m_SteamAppID, "");
-}
-
-HANDLE OrganizerCore::startApplication(const QString &executable,
- const QStringList &args,
- const QString &cwd,
- const QString &profile)
-{
- QFileInfo binary;
- QString arguments = args.join(" ");
- QString currentDirectory = cwd;
- QString profileName = profile;
- if (profile.length() == 0) {
- if (m_CurrentProfile != nullptr) {
- profileName = m_CurrentProfile->name();
- } else {
- throw MyException(tr("No profile set"));
- }
- }
- QString steamAppID;
- QString customOverwrite;
- if (executable.contains('\\') || executable.contains('/')) {
- // file path
-
- binary = QFileInfo(executable);
- if (binary.isRelative()) {
- // relative path, should be relative to game directory
- binary = QFileInfo(
- managedGame()->gameDirectory().absoluteFilePath(executable));
- }
- if (cwd.length() == 0) {
- currentDirectory = binary.absolutePath();
- }
- try {
- const Executable &exe = m_ExecutablesList.findByBinary(binary);
- steamAppID = exe.m_SteamAppID;
- customOverwrite
- = m_CurrentProfile->setting("custom_overwrites", exe.m_Title)
- .toString();
- } catch (const std::runtime_error &) {
- // nop
- }
- } else {
- // only a file name, search executables list
- try {
- const Executable &exe = m_ExecutablesList.find(executable);
- steamAppID = exe.m_SteamAppID;
- customOverwrite
- = m_CurrentProfile->setting("custom_overwrites", exe.m_Title)
- .toString();
- if (arguments == "") {
- arguments = exe.m_Arguments;
- }
- binary = exe.m_BinaryInfo;
- if (cwd.length() == 0) {
- currentDirectory = exe.m_WorkingDirectory;
- }
- } catch (const std::runtime_error &) {
- qWarning("\"%s\" not set up as executable",
- executable.toUtf8().constData());
- binary = QFileInfo(executable);
- }
- }
-
- return spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite);
-}
-
-bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode)
-{
- if (!Settings::instance().lockGUI())
- return true;
-
- ILockedWaitingForProcess* uilock = nullptr;
- if (m_UserInterface != nullptr) {
- uilock = m_UserInterface->lock();
- }
-
- ON_BLOCK_EXIT([&] () {
- if (m_UserInterface != nullptr) {
- m_UserInterface->unlock();
- } });
- return waitForProcessCompletion(handle, exitCode, uilock);
-}
-
-bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock)
-{
- bool originalHandle = true;
- bool newHandle = true;
- bool uiunlocked = false;
-
- DWORD currentPID = 0;
- QString processName;
- auto waitForChildUntil = GetTickCount64();
- if (handle != INVALID_HANDLE_VALUE) {
- currentPID = GetProcessId(handle);
- processName = QString::fromStdWString(getProcessName(handle));
- }
-
- // Certain process names we wish to "hide" for aesthetic reason:
- bool waitingOnHidden = false;
- std::vector<QString> hiddenList;
- hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName());
- for (QString hide : hiddenList)
- if (processName.contains(hide, Qt::CaseInsensitive))
- waitingOnHidden = true;
- // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes.
- // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want
- // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden"
- // process. For this reason we use exponential backoff and also start with a delibrately low value to improve
- // the responsiveness of the initial update
- DWORD64 nextHiddenCheck = GetTickCount64();
- DWORD64 nextHiddenCheckDelay = 50;
-
- constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1;
- DWORD res = WAIT_TIMEOUT;
- while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT))
- {
- if (newHandle) {
- processName += QString(" (%1)").arg(currentPID);
- if (uilock)
- uilock->setProcessName(processName);
- qDebug() << "Waiting for"
- << (originalHandle ? "spawned" : "usvfs")
- << "process completion :" << processName.toUtf8().constData();
- newHandle = false;
- }
-
- // Wait for a an event on the handle, a key press, mouse click or timeout
- res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON);
- if (res == WAIT_FAILED) {
- qWarning() << "Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED" << GetLastError();
- break;
- }
-
- // keep processing events so the app doesn't appear dead
- QCoreApplication::sendPostedEvents();
- QCoreApplication::processEvents();
-
- if (uilock && uilock->unlockForced()) {
- uiunlocked = true;
- break;
- }
-
- if (res == WAIT_OBJECT_0) {
- // process we were waiting on has completed
- if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode))
- qWarning() << "Failed getting exit code of complete process :" << GetLastError();
- CloseHandle(handle);
- handle = INVALID_HANDLE_VALUE;
- originalHandle = false;
- // if the previous process spawned a child process and immediately exits we may miss it if we check immediately
- waitForChildUntil = GetTickCount64() + 800;
- }
-
- // search for another process to wait on if either:
- // 1. we just completed waiting for a process and need to find/wait for an inject child
- // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on
- bool firstIteration = true;
- while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil)
- || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck))
- {
- if (firstIteration)
- firstIteration = false;
- else {
- QThread::msleep(200);
- QCoreApplication::sendPostedEvents();
- QCoreApplication::processEvents();
- }
-
- // search if there is another usvfs process active
- handle = findAndOpenAUSVFSProcess(hiddenList, currentPID);
- waitingOnHidden = false;
- newHandle = handle != INVALID_HANDLE_VALUE;
- if (newHandle) {
- currentPID = GetProcessId(handle);
- processName = QString::fromStdWString(getProcessName(handle));
- for (QString hide : hiddenList)
- if (processName.contains(hide, Qt::CaseInsensitive))
- waitingOnHidden = true;
- }
- if (waitingOnHidden) {
- nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay;
- nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000);
- }
- else {
- nextHiddenCheck = GetTickCount64();
- nextHiddenCheckDelay = 200;
- }
- }
- }
-
- if (res == WAIT_OBJECT_0)
- qDebug() << "Waiting for process completion successfull";
- else if (uiunlocked)
- qDebug() << "Waiting for process completion aborted by UI";
- else
- qDebug() << "Waiting for process completion not successfull :" << res;
-
- if (handle != INVALID_HANDLE_VALUE)
- ::CloseHandle(handle);
-
- return res == WAIT_OBJECT_0;
-}
-
-HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid) {
- // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics
- // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid)
- constexpr size_t querySize = 100;
- DWORD pids[querySize];
- size_t found = querySize;
- if (!::GetVFSProcessList(&found, pids)) {
- qWarning() << "Failed seeking USVFS processes : GetVFSProcessList failed?!";
- return INVALID_HANDLE_VALUE;
- }
-
- HANDLE best_match = INVALID_HANDLE_VALUE;
- bool best_match_hidden = true;
- for (size_t i = 0; i < found; ++i) {
- if (pids[i] == GetCurrentProcessId())
- continue; // obviously don't wait for MO process
-
- HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]);
- if (handle == INVALID_HANDLE_VALUE) {
- qWarning() << "Failed openning USVFS process " << pids[i] << " : OpenProcess failed" << GetLastError();
- continue;
- }
-
- QString pname = QString::fromStdWString(getProcessName(handle));
- bool phidden = false;
- for (auto hide : hiddenList)
- if (pname.contains(hide, Qt::CaseInsensitive))
- phidden = true;
-
- bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid;
-
- if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) {
- if (best_match != INVALID_HANDLE_VALUE)
- CloseHandle(best_match);
- best_match = handle;
- best_match_hidden = phidden;
- }
- else
- CloseHandle(handle);
-
- if (!phidden && pprefered)
- return best_match;
- }
-
- return best_match;
-}
-
-bool OrganizerCore::onAboutToRun(
- const std::function<bool(const QString &)> &func)
-{
- auto conn = m_AboutToRun.connect(func);
- return conn.connected();
-}
-
-bool OrganizerCore::onFinishedRun(
- const std::function<void(const QString &, unsigned int)> &func)
-{
- auto conn = m_FinishedRun.connect(func);
- return conn.connected();
-}
-
-bool OrganizerCore::onModInstalled(
- const std::function<void(const QString &)> &func)
-{
- auto conn = m_ModInstalled.connect(func);
- return conn.connected();
-}
-
-void OrganizerCore::refreshModList(bool saveChanges)
-{
- // don't lose changes!
- if (saveChanges) {
- m_CurrentProfile->writeModlistNow(true);
- }
- ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.displayForeign(), managedGame());
-
- m_CurrentProfile->refreshModStatus();
-
- m_ModList.notifyChange(-1);
-
- refreshDirectoryStructure();
-}
-
-void OrganizerCore::refreshESPList(bool force)
-{
- if (m_DirectoryUpdate) {
- // don't mess up the esp list if we're currently updating the directory
- // structure
- m_PostRefreshTasks.append([=]() {
- this->refreshESPList(force);
- });
- return;
- }
- m_CurrentProfile->writeModlist();
-
- // clear list
- try {
- m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure,
- m_CurrentProfile->getLockedOrderFileName(), force);
- } catch (const std::exception &e) {
- reportError(tr("Failed to refresh list of esps: %1").arg(e.what()));
- }
-}
-
-void OrganizerCore::refreshBSAList()
-{
- DataArchives *archives = m_GamePlugin->feature<DataArchives>();
-
- if (archives != nullptr) {
- m_ArchivesInit = false;
-
- // default archives are the ones enabled outside MO. if the list can't be
- // found (which might
- // happen if ini files are missing) use hard-coded defaults (preferrably the
- // same the game would use)
- m_DefaultArchives = archives->archives(m_CurrentProfile);
- if (m_DefaultArchives.length() == 0) {
- m_DefaultArchives = archives->vanillaArchives();
- }
-
- m_ActiveArchives.clear();
-
- auto iter = enabledArchives();
- m_ActiveArchives = toStringList(iter.begin(), iter.end());
- if (m_ActiveArchives.isEmpty()) {
- m_ActiveArchives = m_DefaultArchives;
- }
-
- if (m_UserInterface != nullptr) {
- m_UserInterface->updateBSAList(m_DefaultArchives, m_ActiveArchives);
- }
-
- m_ArchivesInit = true;
- }
-}
-
-void OrganizerCore::refreshLists()
-{
- if ((m_CurrentProfile != nullptr) && m_DirectoryStructure->isPopulated()) {
- refreshESPList(true);
- refreshBSAList();
- } // no point in refreshing lists if no files have been added to the directory
- // tree
-}
-
-void OrganizerCore::updateModActiveState(int index, bool active)
-{
- ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
- QDir dir(modInfo->absolutePath());
- for (const QString &esm :
- dir.entryList(QStringList() << "*.esm", QDir::Files)) {
- const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esm));
- if (file.get() == nullptr) {
- qWarning("failed to activate %s", qPrintable(esm));
- continue;
- }
-
- if (active != m_PluginList.isEnabled(esm)
- && file->getAlternatives().empty()) {
- m_PluginList.enableESP(esm, active);
- }
- }
- int enabled = 0;
- for (const QString &esl :
- dir.entryList(QStringList() << "*.esl", QDir::Files)) {
- const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esl));
- if (file.get() == nullptr) {
- qWarning("failed to activate %s", qPrintable(esl));
- continue;
- }
-
- if (active != m_PluginList.isEnabled(esl)
- && file->getAlternatives().empty()) {
- m_PluginList.enableESP(esl, active);
- ++enabled;
- }
- }
- QStringList esps = dir.entryList(QStringList() << "*.esp", QDir::Files);
- for (const QString &esp : esps) {
- const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esp));
- if (file.get() == nullptr) {
- qWarning("failed to activate %s", qPrintable(esp));
- continue;
- }
-
- if (active != m_PluginList.isEnabled(esp)
- && file->getAlternatives().empty()) {
- m_PluginList.enableESP(esp, active);
- ++enabled;
- }
- }
- if (active && (enabled > 1)) {
- MessageDialog::showMessage(
- tr("Multiple esps/esls activated, please check that they don't conflict."),
- qApp->activeWindow());
- }
- m_PluginList.refreshLoadOrder();
- // immediately save affected lists
- m_PluginListsWriter.writeImmediately(false);
-}
-
-void OrganizerCore::updateModInDirectoryStructure(unsigned int index,
- ModInfo::Ptr modInfo)
-{
- // add files of the bsa to the directory structure
- m_DirectoryRefresher.addModFilesToStructure(
- m_DirectoryStructure, modInfo->name(),
- m_CurrentProfile->getModPriority(index), modInfo->absolutePath(),
- modInfo->stealFiles());
- DirectoryRefresher::cleanStructure(m_DirectoryStructure);
- // need to refresh plugin list now so we can activate esps
- refreshESPList(true);
- // activate all esps of the specified mod so the bsas get activated along with
- // it
- updateModActiveState(index, true);
- // now we need to refresh the bsa list and save it so there is no confusion
- // about what archives are avaiable and active
- refreshBSAList();
- if (m_UserInterface != nullptr) {
- m_UserInterface->archivesWriter().writeImmediately(false);
- }
-
- std::vector<QString> archives = enabledArchives();
- m_DirectoryRefresher.setMods(
- m_CurrentProfile->getActiveMods(),
- std::set<QString>(archives.begin(), archives.end()));
-
- // finally also add files from bsas to the directory structure
- m_DirectoryRefresher.addModBSAToStructure(
- m_DirectoryStructure, modInfo->name(),
- m_CurrentProfile->getModPriority(index), modInfo->absolutePath(),
- modInfo->archives());
-}
-
-void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply)
-{
- if (m_PluginContainer != nullptr) {
- for (IPluginModPage *modPage :
- m_PluginContainer->plugins<MOBase::IPluginModPage>()) {
- ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo();
- if (modPage->handlesDownload(url, reply->url(), *fileInfo)) {
- fileInfo->repository = modPage->name();
- m_DownloadManager.addDownload(reply, fileInfo);
- return;
- }
- }
- }
-
- // no mod found that could handle the download. Is it a nexus mod?
- if (url.host() == "www.nexusmods.com") {
- QString gameName = "";
- int modID = 0;
- int fileID = 0;
- QRegExp nameExp("www\.nexusmods\.com/(\\a+)/");
- if (nameExp.indexIn(url.toString()) != -1) {
- gameName = nameExp.cap(1);
- }
- QRegExp modExp("mods/(\\d+)");
- if (modExp.indexIn(url.toString()) != -1) {
- modID = modExp.cap(1).toInt();
- }
- QRegExp fileExp("fid=(\\d+)");
- if (fileExp.indexIn(reply->url().toString()) != -1) {
- fileID = fileExp.cap(1).toInt();
- }
- m_DownloadManager.addDownload(reply,
- new ModRepositoryFileInfo(gameName, modID, fileID));
- } else {
- if (QMessageBox::question(qApp->activeWindow(), tr("Download?"),
- tr("A download has been started but no installed "
- "page plugin recognizes it.\n"
- "If you download anyway no information (i.e. "
- "version) will be associated with the "
- "download.\n"
- "Continue?"),
- QMessageBox::Yes | QMessageBox::No)
- == QMessageBox::Yes) {
- m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo());
- }
- }
-}
-
-ModListSortProxy *OrganizerCore::createModListProxyModel()
-{
- ModListSortProxy *result = new ModListSortProxy(m_CurrentProfile, this);
- result->setSourceModel(&m_ModList);
- return result;
-}
-
-PluginListSortProxy *OrganizerCore::createPluginListProxyModel()
-{
- PluginListSortProxy *result = new PluginListSortProxy(this);
- result->setSourceModel(&m_PluginList);
- return result;
-}
-
-IPluginGame const *OrganizerCore::managedGame() const
-{
- return m_GamePlugin;
-}
-
-std::vector<QString> OrganizerCore::enabledArchives()
-{
- std::vector<QString> result;
- QFile archiveFile(m_CurrentProfile->getArchivesFileName());
- if (archiveFile.open(QIODevice::ReadOnly)) {
- while (!archiveFile.atEnd()) {
- result.push_back(QString::fromUtf8(archiveFile.readLine()).trimmed());
- }
- archiveFile.close();
- }
- return result;
-}
-
-void OrganizerCore::refreshDirectoryStructure()
-{
- if (!m_DirectoryUpdate) {
- m_CurrentProfile->writeModlistNow(true);
-
- m_DirectoryUpdate = true;
- std::vector<std::tuple<QString, QString, int>> activeModList
- = m_CurrentProfile->getActiveMods();
- auto archives = enabledArchives();
- m_DirectoryRefresher.setMods(
- activeModList, std::set<QString>(archives.begin(), archives.end()));
-
- QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh()));
- }
-}
-
-void OrganizerCore::directory_refreshed()
-{
- DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure();
- Q_ASSERT(newStructure != m_DirectoryStructure);
- if (newStructure != nullptr) {
- std::swap(m_DirectoryStructure, newStructure);
- delete newStructure;
- } else {
- // TODO: don't know why this happens, this slot seems to get called twice
- // with only one emit
- return;
- }
- m_DirectoryUpdate = false;
-
- for (int i = 0; i < m_ModList.rowCount(); ++i) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
- modInfo->clearCaches();
- }
- for (auto task : m_PostRefreshTasks) {
- task();
- }
- m_PostRefreshTasks.clear();
-
- if (m_CurrentProfile != nullptr) {
- refreshLists();
- }
-}
-
-void OrganizerCore::profileRefresh()
-{
- // have to refresh mods twice (again in refreshModList), otherwise the refresh
- // isn't complete. Not sure why
- ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.displayForeign(), managedGame());
- m_CurrentProfile->refreshModStatus();
-
- refreshModList();
-}
-
-void OrganizerCore::modStatusChanged(unsigned int index)
-{
- try {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
- if (m_CurrentProfile->modEnabled(index)) {
- updateModInDirectoryStructure(index, modInfo);
- } else {
- updateModActiveState(index, false);
- refreshESPList(true);
- if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) {
- FilesOrigin &origin
- = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name()));
- origin.enable(false);
- }
- if (m_UserInterface != nullptr) {
- m_UserInterface->archivesWriter().write();
- }
- }
- modInfo->clearCaches();
-
- for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
- int priority = m_CurrentProfile->getModPriority(i);
- if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) {
- // priorities in the directory structure are one higher because data is
- // 0
- m_DirectoryStructure->getOriginByName(ToWString(modInfo->name()))
- .setPriority(priority + 1);
- }
- }
- m_DirectoryStructure->getFileRegister()->sortOrigins();
-
- refreshLists();
- } catch (const std::exception &e) {
- reportError(tr("failed to update mod list: %1").arg(e.what()));
- }
-}
-
-void OrganizerCore::loginSuccessful(bool necessary)
-{
- if (necessary) {
- MessageDialog::showMessage(tr("login successful"), qApp->activeWindow());
- }
- for (QString url : m_PendingDownloads) {
- downloadRequestedNXM(url);
- }
- m_PendingDownloads.clear();
- for (auto task : m_PostLoginTasks) {
- task();
- }
-
- m_PostLoginTasks.clear();
- NexusInterface::instance(m_PluginContainer)->loginCompleted();
-}
-
-void OrganizerCore::loginSuccessfulUpdate(bool necessary)
-{
- if (necessary) {
- MessageDialog::showMessage(tr("login successful"), qApp->activeWindow());
- }
- m_Updater.startUpdate();
-}
-
-void OrganizerCore::loginFailed(const QString &message)
-{
- if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"),
- tr("Login failed, try again?"))
- == QMessageBox::Yes) {
- if (nexusLogin(true)) {
- return;
- }
- }
-
- if (!m_PendingDownloads.isEmpty()) {
- MessageDialog::showMessage(
- tr("login failed: %1. Download will not be associated with an account")
- .arg(message),
- qApp->activeWindow());
- for (QString url : m_PendingDownloads) {
- downloadRequestedNXM(url);
- }
- m_PendingDownloads.clear();
- } else {
- MessageDialog::showMessage(tr("login failed: %1").arg(message),
- qApp->activeWindow());
- m_PostLoginTasks.clear();
- }
- NexusInterface::instance(m_PluginContainer)->loginCompleted();
-}
-
-void OrganizerCore::loginFailedUpdate(const QString &message)
-{
- MessageDialog::showMessage(
- tr("login failed: %1. You need to log-in with Nexus to update MO.")
- .arg(message),
- qApp->activeWindow());
-}
-
-void OrganizerCore::syncOverwrite()
-{
- unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
- std::vector<ModInfo::EFlag> flags = mod->getFlags();
- return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE)
- != flags.end();
- });
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex);
- SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure,
- qApp->activeWindow());
- if (syncDialog.exec() == QDialog::Accepted) {
- syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory()));
- modInfo->testValid();
- refreshDirectoryStructure();
- }
-}
-
-QString OrganizerCore::oldMO1HookDll() const
-{
- if (auto extender = managedGame()->feature<ScriptExtender>()) {
- QString hookdll = QDir::toNativeSeparators(
- managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll"));
- if (QFile(hookdll).exists())
- return hookdll;
- }
- return QString();
-}
-
-std::vector<unsigned int> OrganizerCore::activeProblems() const
-{
- std::vector<unsigned int> problems;
- const auto& hookdll = oldMO1HookDll();
- if (!hookdll.isEmpty()) {
- // This warning will now be shown every time the problems are checked, which is a bit
- // of a "log spam". But since this is a sevre error which will most likely make the
- // game crash/freeze/etc. and is very hard to diagnose, this "log spam" will make it
- // easier for the user to notice the warning.
- qWarning("hook.dll found in game folder: %s", qPrintable(hookdll));
- problems.push_back(PROBLEM_MO1SCRIPTEXTENDERWORKAROUND);
- }
- return problems;
-}
-
-QString OrganizerCore::shortDescription(unsigned int key) const
-{
- switch (key) {
- case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: {
- return tr("MO1 \"Script Extender\" load mechanism has left hook.dll in your game folder");
- } break;
- default: {
- return tr("Description missing");
- } break;
- }
-}
-
-QString OrganizerCore::fullDescription(unsigned int key) const
-{
- switch (key) {
- case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: {
- return tr("<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.").arg(oldMO1HookDll());
- break;
- }
- default: {
- return tr("Description missing");
- } break;
- }
-}
-
-bool OrganizerCore::hasGuidedFix(unsigned int) const
-{
- return false;
-}
-
-void OrganizerCore::startGuidedFix(unsigned int) const
-{
-}
-
-bool OrganizerCore::saveCurrentLists()
-{
- if (m_DirectoryUpdate) {
- qWarning("not saving lists during directory update");
- return false;
- }
-
- try {
- savePluginList();
- if (m_UserInterface != nullptr) {
- m_UserInterface->archivesWriter().write();
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to save load order: %1").arg(e.what()));
- }
-
- return true;
-}
-
-void OrganizerCore::savePluginList()
-{
- if (m_DirectoryUpdate) {
- // delay save till after directory update
- m_PostRefreshTasks.append([this]() {
- this->savePluginList();
- });
- return;
- }
- m_PluginList.saveTo(m_CurrentProfile->getLockedOrderFileName(),
- m_CurrentProfile->getDeleterFileName(),
- m_Settings.hideUncheckedPlugins());
- m_PluginList.saveLoadOrder(*m_DirectoryStructure);
-}
-
-void OrganizerCore::prepareStart()
-{
- if (m_CurrentProfile == nullptr) {
- return;
- }
- m_CurrentProfile->writeModlist();
- m_CurrentProfile->createTweakedIniFile();
- saveCurrentLists();
- m_Settings.setupLoadMechanism();
- storeSettings();
-}
-
-std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName,
- const QString &customOverwrite)
-{
- // need to wait until directory structure
- while (m_DirectoryUpdate) {
- ::Sleep(100);
- QCoreApplication::processEvents();
- }
-
- IPluginGame *game = qApp->property("managed_game").value<IPluginGame *>();
- Profile profile(QDir(m_Settings.getProfileDirectory() + "/" + profileName),
- game);
-
- MappingType result;
-
- QString dataPath
- = QDir::toNativeSeparators(game->dataDirectory().absolutePath());
-
- bool overwriteActive = false;
-
- for (auto mod : profile.getActiveMods()) {
- if (std::get<0>(mod).compare("overwrite", Qt::CaseInsensitive) == 0) {
- continue;
- }
-
- unsigned int modIndex = ModInfo::getIndex(std::get<0>(mod));
- ModInfo::Ptr modPtr = ModInfo::getByIndex(modIndex);
-
- bool createTarget = customOverwrite == std::get<0>(mod);
-
- overwriteActive |= createTarget;
-
- if (modPtr->isRegular()) {
- result.insert(result.end(), {QDir::toNativeSeparators(std::get<1>(mod)),
- dataPath, true, createTarget});
- }
- }
-
- if (!overwriteActive && !customOverwrite.isEmpty()) {
- throw MyException(tr("The designated write target \"%1\" is not enabled.")
- .arg(customOverwrite));
- }
-
- if (m_CurrentProfile->localSavesEnabled()) {
- LocalSavegames *localSaves = game->feature<LocalSavegames>();
- if (localSaves != nullptr) {
- MappingType saveMap
- = localSaves->mappings(currentProfile()->absolutePath() + "/saves");
- result.reserve(result.size() + saveMap.size());
- result.insert(result.end(), saveMap.begin(), saveMap.end());
- } else {
- qWarning("local save games not supported by this game plugin");
- }
- }
-
- result.insert(result.end(), {
- QDir::toNativeSeparators(m_Settings.getOverwriteDirectory()),
- dataPath,
- true,
- customOverwrite.isEmpty()
- });
-
- for (MOBase::IPluginFileMapper *mapper :
- m_PluginContainer->plugins<MOBase::IPluginFileMapper>()) {
- IPlugin *plugin = dynamic_cast<IPlugin *>(mapper);
- if (plugin->isActive()) {
- MappingType pluginMap = mapper->mappings();
- result.reserve(result.size() + pluginMap.size());
- result.insert(result.end(), pluginMap.begin(), pluginMap.end());
- }
- }
-
- return result;
-}
-
-
-std::vector<Mapping> OrganizerCore::fileMapping(
- const QString &dataPath, const QString &relPath, const DirectoryEntry *base,
- const DirectoryEntry *directoryEntry, int createDestination)
-{
- std::vector<Mapping> result;
-
- for (FileEntry::Ptr current : directoryEntry->getFiles()) {
- bool isArchive = false;
- int origin = current->getOrigin(isArchive);
- if (isArchive || (origin == 0)) {
- continue;
- }
-
- QString originPath
- = QString::fromStdWString(base->getOriginByID(origin).getPath());
- QString fileName = QString::fromStdWString(current->getName());
-// QString fileName = ToQString(current->getName());
- QString source = originPath + relPath + fileName;
- QString target = dataPath + relPath + fileName;
- if (source != target) {
- result.push_back({source, target, false, false});
- }
- }
-
- // recurse into subdirectories
- std::vector<DirectoryEntry *>::const_iterator current, end;
- directoryEntry->getSubDirectories(current, end);
- for (; current != end; ++current) {
- int origin = (*current)->anyOrigin();
-
- QString originPath
- = QString::fromStdWString(base->getOriginByID(origin).getPath());
- QString dirName = QString::fromStdWString((*current)->getName());
- QString source = originPath + relPath + dirName;
- QString target = dataPath + relPath + dirName;
-
- bool writeDestination
- = (base == directoryEntry) && (origin == createDestination);
-
- result.push_back({source, target, true, writeDestination});
- std::vector<Mapping> subRes = fileMapping(
- dataPath, relPath + dirName + "\\", base, *current, createDestination);
- result.insert(result.end(), subRes.begin(), subRes.end());
- }
- return result;
-}
+#include "organizercore.h"
+
+#include "delayedfilewriter.h"
+#include "guessedvalue.h"
+#include "imodinterface.h"
+#include "imoinfo.h"
+#include "iplugingame.h"
+#include "iuserinterface.h"
+#include "loadmechanism.h"
+#include "messagedialog.h"
+#include "modlistsortproxy.h"
+#include "modrepositoryfileinfo.h"
+#include "nexusinterface.h"
+#include "plugincontainer.h"
+#include "pluginlistsortproxy.h"
+#include "profile.h"
+#include "logbuffer.h"
+#include "credentialsdialog.h"
+#include "filedialogmemory.h"
+#include "modinfodialog.h"
+#include "spawn.h"
+#include "syncoverwritedialog.h"
+#include "nxmaccessmanager.h"
+#include <ipluginmodpage.h>
+#include <dataarchives.h>
+#include <localsavegames.h>
+#include <directoryentry.h>
+#include <scopeguard.h>
+#include <utility.h>
+#include <usvfs.h>
+#include "appconfig.h"
+#include <report.h>
+#include <questionboxmemory.h>
+#include "lockeddialog.h"
+#include "instancemanager.h"
+#include <scriptextender.h>
+#include "helper.h"
+
+#include <QApplication>
+#include <QCoreApplication>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QMessageBox>
+#include <QNetworkInterface>
+#include <QProcess>
+#include <QTimer>
+#include <QUrl>
+#include <QWidget>
+
+#include <QtDebug>
+#include <QtGlobal> // for qUtf8Printable, etc
+
+#include <Psapi.h>
+#include <Shlobj.h>
+#include <tlhelp32.h>
+#include <tchar.h> // for _tcsicmp
+
+#include <limits.h>
+#include <stddef.h>
+#include <string.h> // for memset, wcsrchr
+
+#include <exception>
+#include <functional>
+#include <boost/algorithm/string/predicate.hpp>
+#include <memory>
+#include <set>
+#include <string> //for wstring
+#include <tuple>
+#include <utility>
+
+
+using namespace MOShared;
+using namespace MOBase;
+
+//static
+CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None;
+
+static bool isOnline()
+{
+ QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
+
+ bool connected = false;
+ for (auto iter = interfaces.begin(); iter != interfaces.end() && !connected;
+ ++iter) {
+ if ((iter->flags() & QNetworkInterface::IsUp)
+ && (iter->flags() & QNetworkInterface::IsRunning)
+ && !(iter->flags() & QNetworkInterface::IsLoopBack)) {
+ auto addresses = iter->addressEntries();
+ if (addresses.count() == 0) {
+ continue;
+ }
+ qDebug("interface %s seems to be up (address: %s)",
+ qUtf8Printable(iter->humanReadableName()),
+ qUtf8Printable(addresses[0].ip().toString()));
+ connected = true;
+ }
+ }
+
+ return connected;
+}
+
+static bool renameFile(const QString &oldName, const QString &newName,
+ bool overwrite = true)
+{
+ if (overwrite && QFile::exists(newName)) {
+ QFile::remove(newName);
+ }
+ return QFile::rename(oldName, newName);
+}
+
+static std::wstring getProcessName(HANDLE process)
+{
+ wchar_t buffer[MAX_PATH];
+ wchar_t *fileName = L"unknown";
+
+ if (process == nullptr) return fileName;
+
+ if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) {
+ fileName = wcsrchr(buffer, L'\\');
+ if (fileName == nullptr) {
+ fileName = buffer;
+ }
+ else {
+ fileName += 1;
+ }
+ }
+
+ return fileName;
+}
+
+// Get parent PID for the given process, return 0 on failure
+static DWORD getProcessParentID(DWORD pid)
+{
+ HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ PROCESSENTRY32 pe = { 0 };
+ pe.dwSize = sizeof(PROCESSENTRY32);
+
+ DWORD res = 0;
+ if (Process32First(th, &pe))
+ do {
+ if (pe.th32ProcessID == pid) {
+ res = pe.th32ParentProcessID;
+ break;
+ }
+ } while (Process32Next(th, &pe));
+
+ CloseHandle(th);
+
+ return res;
+}
+
+static void startSteam(QWidget *widget)
+{
+ QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam",
+ QSettings::NativeFormat);
+ QString exe = steamSettings.value("SteamExe", "").toString();
+ if (!exe.isEmpty()) {
+ exe = QString("\"%1\"").arg(exe);
+ // See if username and password supplied. If so, pass them into steam.
+ QStringList args;
+ QString username;
+ QString password;
+ if (Settings::instance().getSteamLogin(username, password)) {
+ args << "-login";
+ args << username;
+ if (password != "") {
+ args << password;
+ }
+ }
+ if (!QProcess::startDetached(exe, args)) {
+ reportError(QObject::tr("Failed to start \"%1\"").arg(exe));
+ } else {
+ QMessageBox::information(
+ widget, QObject::tr("Waiting"),
+ QObject::tr("Please press OK once you're logged into steam."));
+ }
+ }
+}
+
+template <typename InputIterator>
+QStringList toStringList(InputIterator current, InputIterator end)
+{
+ QStringList result;
+ for (; current != end; ++current) {
+ result.append(*current);
+ }
+ return result;
+}
+
+bool checkService()
+{
+ SC_HANDLE serviceManagerHandle = NULL;
+ SC_HANDLE serviceHandle = NULL;
+ LPSERVICE_STATUS_PROCESS serviceStatus = NULL;
+ LPQUERY_SERVICE_CONFIG serviceConfig = NULL;
+ bool serviceRunning = true;
+
+ DWORD bytesNeeded;
+
+ try {
+ serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
+ if (!serviceManagerHandle) {
+ qWarning("failed to open service manager (query status) (error %d)", GetLastError());
+ throw 1;
+ }
+
+ serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
+ if (!serviceHandle) {
+ qWarning("failed to open EventLog service (query status) (error %d)", GetLastError());
+ throw 2;
+ }
+
+ if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded)
+ || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
+ qWarning("failed to get size of service config (error %d)", GetLastError());
+ throw 3;
+ }
+
+ DWORD serviceConfigSize = bytesNeeded;
+ serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize);
+ if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) {
+ qWarning("failed to query service config (error %d)", GetLastError());
+ throw 4;
+ }
+
+ if (serviceConfig->dwStartType == SERVICE_DISABLED) {
+ qCritical("Windows Event Log service is disabled!");
+ serviceRunning = false;
+ }
+
+ if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded)
+ || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
+ qWarning("failed to get size of service status (error %d)", GetLastError());
+ throw 5;
+ }
+
+ DWORD serviceStatusSize = bytesNeeded;
+ serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize);
+ if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) {
+ qWarning("failed to query service status (error %d)", GetLastError());
+ throw 6;
+ }
+
+ if (serviceStatus->dwCurrentState != SERVICE_RUNNING) {
+ qCritical("Windows Event Log service is not running");
+ serviceRunning = false;
+ }
+ }
+ catch (int e) {
+ UNUSED_VAR(e);
+ serviceRunning = false;
+ }
+
+ if (serviceStatus) {
+ LocalFree(serviceStatus);
+ }
+ if (serviceConfig) {
+ LocalFree(serviceConfig);
+ }
+ if (serviceHandle) {
+ CloseServiceHandle(serviceHandle);
+ }
+ if (serviceManagerHandle) {
+ CloseServiceHandle(serviceManagerHandle);
+ }
+
+ return serviceRunning;
+}
+
+OrganizerCore::OrganizerCore(const QSettings &initSettings)
+ : m_UserInterface(nullptr)
+ , m_PluginContainer(nullptr)
+ , m_GameName()
+ , m_CurrentProfile(nullptr)
+ , m_Settings(initSettings)
+ , m_Updater(NexusInterface::instance(m_PluginContainer))
+ , m_AboutToRun()
+ , m_FinishedRun()
+ , m_ModInstalled()
+ , m_ModList(m_PluginContainer, this)
+ , m_PluginList(this)
+ , m_DirectoryRefresher()
+ , m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0))
+ , m_DownloadManager(NexusInterface::instance(m_PluginContainer), this)
+ , m_InstallationManager()
+ , m_RefresherThread()
+ , m_DirectoryUpdate(false)
+ , m_ArchivesInit(false)
+ , m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this))
+{
+ m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory());
+ m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers());
+
+ NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory());
+
+ MOBase::QuestionBoxMemory::init(initSettings.fileName());
+
+ m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
+ m_InstallationManager.setDownloadDirectory(m_Settings.getDownloadDirectory());
+
+ connect(&m_DownloadManager, SIGNAL(downloadSpeed(QString, int)), this,
+ SLOT(downloadSpeed(QString, int)));
+ connect(&m_DirectoryRefresher, SIGNAL(refreshed()), this,
+ SLOT(directory_refreshed()));
+
+ connect(&m_ModList, SIGNAL(removeOrigin(QString)), this,
+ SLOT(removeOrigin(QString)));
+
+ connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(),
+ SIGNAL(validateSuccessful(bool)), this, SLOT(loginSuccessful(bool)));
+ connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(),
+ SIGNAL(validateFailed(QString)), this, SLOT(loginFailed(QString)));
+
+ // This seems awfully imperative
+ connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)),
+ &m_Settings, SLOT(managedGameChanged(MOBase::IPluginGame const *)));
+ connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)),
+ &m_DownloadManager,
+ SLOT(managedGameChanged(MOBase::IPluginGame const *)));
+ connect(this, SIGNAL(managedGameChanged(MOBase::IPluginGame const *)),
+ &m_PluginList, SLOT(managedGameChanged(MOBase::IPluginGame const *)));
+
+ connect(&m_PluginList, &PluginList::writePluginsList, &m_PluginListsWriter,
+ &DelayedFileWriterBase::write);
+
+ // make directory refresher run in a separate thread
+ m_RefresherThread.start();
+ m_DirectoryRefresher.moveToThread(&m_RefresherThread);
+}
+
+OrganizerCore::~OrganizerCore()
+{
+ m_RefresherThread.exit();
+ m_RefresherThread.wait();
+
+ prepareStart();
+
+ // profile has to be cleaned up before the modinfo-buffer is cleared
+ delete m_CurrentProfile;
+ m_CurrentProfile = nullptr;
+
+ ModInfo::clear();
+ LogBuffer::cleanQuit();
+ m_ModList.setProfile(nullptr);
+ // NexusInterface::instance()->cleanup();
+
+ delete m_DirectoryStructure;
+}
+
+QString OrganizerCore::commitSettings(const QString &iniFile)
+{
+ if (!shellRename(iniFile + ".new", iniFile, true, qApp->activeWindow())) {
+ DWORD err = ::GetLastError();
+ // make a second attempt using qt functions but if that fails print the
+ // error from the first attempt
+ if (!renameFile(iniFile + ".new", iniFile)) {
+ return windowsErrorString(err);
+ }
+ }
+ return QString();
+}
+
+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();
+
+ FileDialogMemory::save(settings);
+
+ settings.sync();
+ return settings.status();
+}
+
+void OrganizerCore::storeSettings()
+{
+ QString iniFile = qApp->property("dataPath").toString() + "/"
+ + QString::fromStdWString(AppConfig::iniFileName());
+ if (QFileInfo(iniFile).exists()) {
+ 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")
+ .arg(iniFile, windowsErrorString(::GetLastError())));
+ return;
+ }
+ }
+
+ QString writeTarget = iniFile + ".new";
+
+ QSettings::Status result = storeSettings(writeTarget);
+
+ if (result == QSettings::NoError) {
+ QString errMsg = commitSettings(iniFile);
+ if (!errMsg.isEmpty()) {
+ qWarning("settings file not writable, may be locked by another "
+ "application, trying direct write");
+ writeTarget = iniFile;
+ result = storeSettings(iniFile);
+ }
+ }
+ if (result != QSettings::NoError) {
+ QString reason = result == QSettings::AccessError
+ ? tr("File is write protected")
+ : result == QSettings::FormatError
+ ? tr("Invalid file format (probably a bug)")
+ : 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")
+ .arg(writeTarget, reason));
+ }
+}
+
+bool OrganizerCore::testForSteam(bool *found, bool *access)
+{
+ HANDLE hProcessSnap;
+ HANDLE hProcess;
+ PROCESSENTRY32 pe32;
+ DWORD lastError;
+
+ if (found == nullptr || access == nullptr) {
+ return false;
+ }
+
+ // Take a snapshot of all processes in the system.
+ hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (hProcessSnap == INVALID_HANDLE_VALUE) {
+ lastError = GetLastError();
+ qCritical("unable to get snapshot of processes (error %d)", lastError);
+ return false;
+ }
+
+ // Retrieve information about the first process,
+ // and exit if unsuccessful
+ pe32.dwSize = sizeof(PROCESSENTRY32);
+ if (!Process32First(hProcessSnap, &pe32)) {
+ lastError = GetLastError();
+ qCritical("unable to get first process (error %d)", lastError);
+ CloseHandle(hProcessSnap);
+ return false;
+ }
+
+ *found = false;
+ *access = true;
+
+ // Now walk the snapshot of processes, and
+ // display information about each process in turn
+ do {
+ if ((_tcsicmp(pe32.szExeFile, L"Steam.exe") == 0) ||
+ (_tcsicmp(pe32.szExeFile, L"SteamService.exe") == 0)) {
+
+ *found = true;
+
+ // Try to open the process to determine if MO has the proper access
+ hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
+ FALSE, pe32.th32ProcessID);
+ if (hProcess == NULL) {
+ lastError = GetLastError();
+ if (lastError == ERROR_ACCESS_DENIED) {
+ *access = false;
+ }
+ } else {
+ CloseHandle(hProcess);
+ }
+ break;
+ }
+
+} while(Process32Next(hProcessSnap, &pe32));
+
+CloseHandle(hProcessSnap);
+return true;
+
+}
+
+void OrganizerCore::updateExecutablesList(QSettings &settings)
+{
+ if (m_PluginContainer == nullptr) {
+ qCritical("can't update executables list now");
+ 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();
+
+ // TODO this has nothing to do with executables list move to an appropriate
+ // function!
+ ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
+ m_PluginContainer, m_Settings.displayForeign(), managedGame());
+}
+
+void OrganizerCore::setUserInterface(IUserInterface *userInterface,
+ QWidget *widget)
+{
+ storeSettings();
+
+ m_UserInterface = userInterface;
+
+ if (widget != nullptr) {
+ connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), widget,
+ SLOT(modlistChanged(QModelIndex, int)));
+ connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), widget,
+ SLOT(modlistChanged(QModelIndexList, int)));
+ connect(&m_ModList, SIGNAL(showMessage(QString)), widget,
+ SLOT(showMessage(QString)));
+ connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), widget,
+ SLOT(modRenamed(QString, QString)));
+ connect(&m_ModList, SIGNAL(modUninstalled(QString)), widget,
+ SLOT(modRemoved(QString)));
+ connect(&m_ModList, SIGNAL(removeSelectedMods()), widget,
+ SLOT(removeMod_clicked()));
+ connect(&m_ModList, SIGNAL(clearOverwrite()), widget,
+ SLOT(clearOverwrite()));
+ connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget,
+ SLOT(displayColumnSelection(QPoint)));
+ connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget,
+ SLOT(fileMoved(QString, QString, QString)));
+ connect(&m_ModList, SIGNAL(modorder_changed()), widget,
+ SLOT(modorder_changed()));
+ connect(&m_PluginList, SIGNAL(writePluginsList()), widget,
+ SLOT(esplist_changed()));
+ connect(&m_PluginList, SIGNAL(esplist_changed()), widget,
+ SLOT(esplist_changed()));
+ connect(&m_DownloadManager, SIGNAL(showMessage(QString)), widget,
+ SLOT(showMessage(QString)));
+ }
+
+ m_InstallationManager.setParentWidget(widget);
+ m_Updater.setUserInterface(widget);
+
+ if (userInterface != nullptr) {
+ // this currently wouldn't work reliably if the ui isn't initialized yet to
+ // display the result
+ if (isOnline() && !m_Settings.offlineMode()) {
+ m_Updater.testForUpdate();
+ } else {
+ qDebug("user doesn't seem to be connected to the internet");
+ }
+ }
+}
+
+void OrganizerCore::connectPlugins(PluginContainer *container)
+{
+ m_DownloadManager.setSupportedExtensions(
+ m_InstallationManager.getSupportedExtensions());
+ m_PluginContainer = container;
+ m_Updater.setPluginContainer(m_PluginContainer);
+ m_DownloadManager.setPluginContainer(m_PluginContainer);
+ m_ModList.setPluginContainer(m_PluginContainer);
+
+ if (!m_GameName.isEmpty()) {
+ m_GamePlugin = m_PluginContainer->managedGame(m_GameName);
+ emit managedGameChanged(m_GamePlugin);
+ }
+}
+
+void OrganizerCore::disconnectPlugins()
+{
+ m_AboutToRun.disconnect_all_slots();
+ m_FinishedRun.disconnect_all_slots();
+ m_ModInstalled.disconnect_all_slots();
+ m_ModList.disconnectSlots();
+ m_PluginList.disconnectSlots();
+ m_Updater.setPluginContainer(nullptr);
+ m_DownloadManager.setPluginContainer(nullptr);
+ m_ModList.setPluginContainer(nullptr);
+
+ m_Settings.clearPlugins();
+ m_GamePlugin = nullptr;
+ m_PluginContainer = nullptr;
+}
+
+void OrganizerCore::setManagedGame(MOBase::IPluginGame *game)
+{
+ m_GameName = game->gameName();
+ m_GamePlugin = game;
+ qApp->setProperty("managed_game", QVariant::fromValue(m_GamePlugin));
+ emit managedGameChanged(m_GamePlugin);
+}
+
+Settings &OrganizerCore::settings()
+{
+ return m_Settings;
+}
+
+bool OrganizerCore::nexusApi(bool retry)
+{
+ NXMAccessManager *accessManager
+ = NexusInterface::instance(m_PluginContainer)->getAccessManager();
+
+ if ((accessManager->validateAttempted() || accessManager->validated())
+ && !retry) {
+ // previous attempt, maybe even successful
+ return false;
+ } else {
+ QString apiKey;
+ if (m_Settings.getNexusApiKey(apiKey)) {
+ // credentials stored or user entered them manually
+ qDebug("attempt to verify nexus api key");
+ accessManager->apiCheck(apiKey);
+ return true;
+ } else {
+ // no credentials stored and user didn't enter them
+ accessManager->refuseValidation();
+ return false;
+ }
+ }
+}
+
+void OrganizerCore::startMOUpdate()
+{
+ if (nexusApi()) {
+ m_PostLoginTasks.append([&]() { m_Updater.startUpdate(); });
+ } else {
+ m_Updater.startUpdate();
+ }
+}
+
+void OrganizerCore::downloadRequestedNXM(const QString &url)
+{
+ qDebug("download requested: %s", qUtf8Printable(url));
+ if (nexusApi()) {
+ m_PendingDownloads.append(url);
+ } else {
+ m_DownloadManager.addNXMDownload(url);
+ }
+}
+
+void OrganizerCore::externalMessage(const QString &message)
+{
+ if (MOShortcut moshortcut{ message } ) {
+ if(moshortcut.hasExecutable())
+ runShortcut(moshortcut);
+ }
+ else if (isNxmLink(message)) {
+ MessageDialog::showMessage(tr("Download started"), qApp->activeWindow());
+ downloadRequestedNXM(message);
+ }
+}
+
+void OrganizerCore::downloadRequested(QNetworkReply *reply, QString gameName, int modID,
+ const QString &fileName)
+{
+ try {
+ if (m_DownloadManager.addDownload(reply, QStringList(), fileName, gameName, modID, 0,
+ new ModRepositoryFileInfo(gameName, modID))) {
+ MessageDialog::showMessage(tr("Download started"), qApp->activeWindow());
+ }
+ } catch (const std::exception &e) {
+ MessageDialog::showMessage(tr("Download failed"), qApp->activeWindow());
+ qCritical("exception starting download: %s", e.what());
+ }
+}
+
+void OrganizerCore::removeOrigin(const QString &name)
+{
+ FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name));
+ origin.enable(false);
+ refreshLists();
+}
+
+void OrganizerCore::downloadSpeed(const QString &serverName, int bytesPerSecond)
+{
+ m_Settings.setDownloadSpeed(serverName, bytesPerSecond);
+}
+
+InstallationManager *OrganizerCore::installationManager()
+{
+ return &m_InstallationManager;
+}
+
+bool OrganizerCore::createDirectory(const QString &path) {
+ if (!QDir(path).exists() && !QDir().mkpath(path)) {
+ QMessageBox::critical(nullptr, QObject::tr("Error"),
+ QObject::tr("Failed to create \"%1\". Your user "
+ "account probably lacks permission.")
+ .arg(QDir::toNativeSeparators(path)));
+ return false;
+ } else {
+ 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();
+}
+
+void OrganizerCore::createDefaultProfile()
+{
+ QString profilesPath = settings().getProfileDirectory();
+ if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size()
+ == 0) {
+ Profile newProf("Default", managedGame(), false);
+ }
+}
+
+void OrganizerCore::prepareVFS()
+{
+ m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
+}
+
+void OrganizerCore::updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist) {
+ setGlobalCrashDumpsType(crashDumpsType);
+ m_USVFS.updateParams(logLevel, crashDumpsType, executableBlacklist);
+}
+
+bool OrganizerCore::cycleDiagnostics() {
+ if (int maxDumps = settings().crashDumpsMax())
+ removeOldFiles(QString::fromStdWString(crashDumpsPath()), "*.dmp", maxDumps, QDir::Time|QDir::Reversed);
+ return true;
+}
+
+//static
+void OrganizerCore::setGlobalCrashDumpsType(int crashDumpsType) {
+ m_globalCrashDumpsType = ::crashDumpsType(crashDumpsType);
+}
+
+//static
+std::wstring OrganizerCore::crashDumpsPath() {
+ return (
+ qApp->property("dataPath").toString() + "/"
+ + QString::fromStdWString(AppConfig::dumpsDir())
+ ).toStdWString();
+}
+
+bool OrganizerCore::getArchiveParsing() const
+{
+ return m_ArchiveParsing;
+}
+
+void OrganizerCore::setArchiveParsing(const bool archiveParsing)
+{
+ m_ArchiveParsing = archiveParsing;
+}
+
+void OrganizerCore::setCurrentProfile(const QString &profileName)
+{
+ if ((m_CurrentProfile != nullptr)
+ && (profileName == m_CurrentProfile->name())) {
+ return;
+ }
+
+ QDir profileBaseDir(settings().getProfileDirectory());
+ QString profileDir = profileBaseDir.absoluteFilePath(profileName);
+
+ if (!QDir(profileDir).exists()) {
+ // selected profile doesn't exist. Ensure there is at least one profile,
+ // then pick any one
+ createDefaultProfile();
+
+ profileDir = profileBaseDir.absoluteFilePath(
+ profileBaseDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot).at(0));
+ }
+
+ Profile *newProfile = new Profile(QDir(profileDir), managedGame());
+
+ delete m_CurrentProfile;
+ m_CurrentProfile = newProfile;
+ m_ModList.setProfile(newProfile);
+
+ if (m_CurrentProfile->invalidationActive(nullptr)) {
+ m_CurrentProfile->activateInvalidation();
+ } else {
+ m_CurrentProfile->deactivateInvalidation();
+ }
+
+ connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint)));
+ connect(m_CurrentProfile, SIGNAL(modStatusChanged(QList<uint>)), this, SLOT(modStatusChanged(QList<uint>)));
+ refreshDirectoryStructure();
+
+ //This line is not actually needed and was only added to allow some
+ //outside detection of Mo2 profile change. (like BaobobMiller utility)
+ if (m_CurrentProfile != nullptr) {
+ settings().directInterface().setValue("selected_profile",
+ m_CurrentProfile->name().toUtf8().constData());
+ }
+}
+
+MOBase::IModRepositoryBridge *OrganizerCore::createNexusBridge() const
+{
+ return new NexusBridge(m_PluginContainer);
+}
+
+QString OrganizerCore::profileName() const
+{
+ if (m_CurrentProfile != nullptr) {
+ return m_CurrentProfile->name();
+ } else {
+ return "";
+ }
+}
+
+QString OrganizerCore::profilePath() const
+{
+ if (m_CurrentProfile != nullptr) {
+ return m_CurrentProfile->absolutePath();
+ } else {
+ return "";
+ }
+}
+
+QString OrganizerCore::downloadsPath() const
+{
+ return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory());
+}
+
+QString OrganizerCore::overwritePath() const
+{
+ return QDir::fromNativeSeparators(m_Settings.getOverwriteDirectory());
+}
+
+QString OrganizerCore::basePath() const
+{
+ return QDir::fromNativeSeparators(m_Settings.getBaseDirectory());
+}
+
+QString OrganizerCore::modsPath() const
+{
+ return QDir::fromNativeSeparators(m_Settings.getModDirectory());
+}
+
+MOBase::VersionInfo OrganizerCore::appVersion() const
+{
+ return m_Updater.getVersion();
+}
+
+MOBase::IModInterface *OrganizerCore::getMod(const QString &name) const
+{
+ unsigned int index = ModInfo::getIndex(name);
+ return index == UINT_MAX ? nullptr : ModInfo::getByIndex(index).data();
+}
+
+MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const
+{
+ for (IPluginGame *game : m_PluginContainer->plugins<IPluginGame>()) {
+ if (game != nullptr && game->gameShortName().compare(name, Qt::CaseInsensitive) == 0)
+ return game;
+ }
+ return nullptr;
+}
+
+MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &name)
+{
+ bool merge = false;
+ if (!m_InstallationManager.testOverwrite(name, &merge)) {
+ return nullptr;
+ }
+
+ m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
+
+ QString targetDirectory
+ = QDir::fromNativeSeparators(m_Settings.getModDirectory())
+ .append("/")
+ .append(name);
+
+ QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat);
+
+ if (!merge) {
+ settingsFile.setValue("modid", 0);
+ settingsFile.setValue("version", "");
+ settingsFile.setValue("newestVersion", "");
+ settingsFile.setValue("category", 0);
+ settingsFile.setValue("installationFile", "");
+
+ settingsFile.remove("installedFiles");
+ settingsFile.beginWriteArray("installedFiles", 0);
+ settingsFile.endArray();
+ }
+
+ return ModInfo::createFrom(m_PluginContainer, m_GamePlugin, QDir(targetDirectory), &m_DirectoryStructure)
+ .data();
+}
+
+bool OrganizerCore::removeMod(MOBase::IModInterface *mod)
+{
+ unsigned int index = ModInfo::getIndex(mod->name());
+ if (index == UINT_MAX) {
+ return mod->remove();
+ } else {
+ return ModInfo::removeMod(index);
+ }
+}
+
+void OrganizerCore::modDataChanged(MOBase::IModInterface *)
+{
+ refreshModList(false);
+}
+
+QVariant OrganizerCore::pluginSetting(const QString &pluginName,
+ const QString &key) const
+{
+ return m_Settings.pluginSetting(pluginName, key);
+}
+
+void OrganizerCore::setPluginSetting(const QString &pluginName,
+ const QString &key, const QVariant &value)
+{
+ m_Settings.setPluginSetting(pluginName, key, value);
+}
+
+QVariant OrganizerCore::persistent(const QString &pluginName,
+ const QString &key,
+ const QVariant &def) const
+{
+ return m_Settings.pluginPersistent(pluginName, key, def);
+}
+
+void OrganizerCore::setPersistent(const QString &pluginName, const QString &key,
+ const QVariant &value, bool sync)
+{
+ m_Settings.setPluginPersistent(pluginName, key, value, sync);
+}
+
+QString OrganizerCore::pluginDataPath() const
+{
+ return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())
+ + "/data";
+}
+
+MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName,
+ const QString &initModName)
+{
+ if (m_CurrentProfile == nullptr) {
+ return nullptr;
+ }
+
+ if (m_InstallationManager.isRunning()) {
+ QMessageBox::information(
+ qApp->activeWindow(), tr("Installation cancelled"),
+ tr("Another installation is currently in progress."), QMessageBox::Ok);
+ return nullptr;
+ }
+
+ bool hasIniTweaks = false;
+ GuessedValue<QString> modName;
+ if (!initModName.isEmpty()) {
+ modName.update(initModName, GUESS_USER);
+ }
+ m_CurrentProfile->writeModlistNow();
+ m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
+ if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) {
+ MessageDialog::showMessage(tr("Installation successful"),
+ qApp->activeWindow());
+ refreshModList();
+
+ int modIndex = ModInfo::getIndex(modName);
+ if (modIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ if (hasIniTweaks && (m_UserInterface != nullptr)
+ && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"),
+ tr("This mod contains ini tweaks. Do you "
+ "want to configure them now?"),
+ QMessageBox::Yes | QMessageBox::No)
+ == QMessageBox::Yes)) {
+ m_UserInterface->displayModInformation(modInfo, modIndex,
+ ModInfoDialog::TAB_INIFILES);
+ }
+ m_ModInstalled(modName);
+ m_DownloadManager.markInstalled(fileName);
+ emit modInstalled(modName);
+ return modInfo.data();
+ } else {
+ reportError(tr("mod not found: %1").arg(qUtf8Printable(modName)));
+ }
+ } else if (m_InstallationManager.wasCancelled()) {
+ QMessageBox::information(qApp->activeWindow(), tr("Installation cancelled"),
+ tr("The mod was not installed completely."),
+ QMessageBox::Ok);
+ }
+ return nullptr;
+}
+
+void OrganizerCore::installDownload(int index)
+{
+ if (m_InstallationManager.isRunning()) {
+ QMessageBox::information(
+ qApp->activeWindow(), tr("Installation cancelled"),
+ tr("Another installation is currently in progress."), QMessageBox::Ok);
+ return;
+ }
+
+ try {
+ QString fileName = m_DownloadManager.getFilePath(index);
+ QString gameName = m_DownloadManager.getGameName(index);
+ int modID = m_DownloadManager.getModID(index);
+ int fileID = m_DownloadManager.getFileInfo(index)->fileID;
+ GuessedValue<QString> modName;
+
+ // see if there already are mods with the specified mod id
+ if (modID != 0) {
+ std::vector<ModInfo::Ptr> modInfo = ModInfo::getByModID(gameName, modID);
+ for (auto iter = modInfo.begin(); iter != modInfo.end(); ++iter) {
+ std::vector<ModInfo::EFlag> flags = (*iter)->getFlags();
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP)
+ == flags.end()) {
+ modName.update((*iter)->name(), GUESS_PRESET);
+ (*iter)->saveMeta();
+ }
+ }
+ }
+
+ m_CurrentProfile->writeModlistNow();
+
+ bool hasIniTweaks = false;
+ m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
+ if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) {
+ MessageDialog::showMessage(tr("Installation successful"),
+ qApp->activeWindow());
+ refreshModList();
+
+ int modIndex = ModInfo::getIndex(modName);
+ if (modIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ modInfo->addInstalledFile(modID, fileID);
+
+ if (hasIniTweaks && m_UserInterface != nullptr
+ && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"),
+ tr("This mod contains ini tweaks. Do you "
+ "want to configure them now?"),
+ QMessageBox::Yes | QMessageBox::No)
+ == QMessageBox::Yes)) {
+ m_UserInterface->displayModInformation(modInfo, modIndex,
+ ModInfoDialog::TAB_INIFILES);
+ }
+
+ m_ModInstalled(modName);
+ } else {
+ reportError(tr("mod not found: %1").arg(qUtf8Printable(modName)));
+ }
+ m_DownloadManager.markInstalled(index);
+
+ emit modInstalled(modName);
+ } else if (m_InstallationManager.wasCancelled()) {
+ QMessageBox::information(
+ qApp->activeWindow(), tr("Installation cancelled"),
+ tr("The mod was not installed completely."), QMessageBox::Ok);
+ }
+ } catch (const std::exception &e) {
+ reportError(e.what());
+ }
+}
+
+QString OrganizerCore::resolvePath(const QString &fileName) const
+{
+ if (m_DirectoryStructure == nullptr) {
+ return QString();
+ }
+ const FileEntry::Ptr file
+ = m_DirectoryStructure->searchFile(ToWString(fileName), nullptr);
+ if (file.get() != nullptr) {
+ return ToQString(file->getFullPath());
+ } else {
+ return QString();
+ }
+}
+
+QStringList OrganizerCore::listDirectories(const QString &directoryName) const
+{
+ QStringList result;
+ DirectoryEntry *dir = m_DirectoryStructure;
+ if (!directoryName.isEmpty())
+ dir = dir->findSubDirectoryRecursive(ToWString(directoryName));
+ if (dir != nullptr) {
+ std::vector<DirectoryEntry *>::iterator current, end;
+ dir->getSubDirectories(current, end);
+ for (; current != end; ++current) {
+ result.append(ToQString((*current)->getName()));
+ }
+ }
+ return result;
+}
+
+QStringList OrganizerCore::findFiles(
+ const QString &path,
+ const std::function<bool(const QString &)> &filter) const
+{
+ QStringList result;
+ DirectoryEntry *dir = m_DirectoryStructure;
+ if (!path.isEmpty())
+ dir = dir->findSubDirectoryRecursive(ToWString(path));
+ if (dir != nullptr) {
+ std::vector<FileEntry::Ptr> files = dir->getFiles();
+ foreach (FileEntry::Ptr file, files) {
+ if (filter(ToQString(file->getFullPath()))) {
+ result.append(ToQString(file->getFullPath()));
+ }
+ }
+ }
+ return result;
+}
+
+QStringList OrganizerCore::getFileOrigins(const QString &fileName) const
+{
+ QStringList result;
+ const FileEntry::Ptr file = m_DirectoryStructure->searchFile(ToWString(fileName), nullptr);
+
+ if (file.get() != nullptr) {
+ result.append(ToQString(
+ m_DirectoryStructure->getOriginByID(file->getOrigin()).getName()));
+ foreach (auto i, file->getAlternatives()) {
+ result.append(
+ ToQString(m_DirectoryStructure->getOriginByID(i.first).getName()));
+ }
+ }
+ return result;
+}
+
+QList<MOBase::IOrganizer::FileInfo> OrganizerCore::findFileInfos(
+ const QString &path,
+ const std::function<bool(const MOBase::IOrganizer::FileInfo &)> &filter)
+ const
+{
+ QList<IOrganizer::FileInfo> result;
+ DirectoryEntry *dir = m_DirectoryStructure;
+ if (!path.isEmpty())
+ dir = dir->findSubDirectoryRecursive(ToWString(path));
+ if (dir != nullptr) {
+ std::vector<FileEntry::Ptr> files = dir->getFiles();
+ foreach (FileEntry::Ptr file, files) {
+ IOrganizer::FileInfo info;
+ info.filePath = ToQString(file->getFullPath());
+ bool fromArchive = false;
+ info.origins.append(ToQString(
+ m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive))
+ .getName()));
+ info.archive = fromArchive ? ToQString(file->getArchive().first) : "";
+ foreach (auto idx, file->getAlternatives()) {
+ info.origins.append(
+ ToQString(m_DirectoryStructure->getOriginByID(idx.first).getName()));
+ }
+
+ if (filter(info)) {
+ result.append(info);
+ }
+ }
+ }
+ return result;
+}
+
+DownloadManager *OrganizerCore::downloadManager()
+{
+ return &m_DownloadManager;
+}
+
+PluginList *OrganizerCore::pluginList()
+{
+ return &m_PluginList;
+}
+
+ModList *OrganizerCore::modList()
+{
+ return &m_ModList;
+}
+
+QStringList OrganizerCore::modsSortedByProfilePriority() const
+{
+ QStringList res;
+ for (int i = currentProfile()->getPriorityMinimum();
+ i < currentProfile()->getPriorityMinimum() + (int)currentProfile()->numRegularMods();
+ ++i) {
+ int modIndex = currentProfile()->modIndexByPriority(i);
+ auto modInfo = ModInfo::getByIndex(modIndex);
+ if (!modInfo->hasFlag(ModInfo::FLAG_OVERWRITE) &&
+ !modInfo->hasFlag(ModInfo::FLAG_BACKUP)) {
+ res.push_back(ModInfo::getByIndex(modIndex)->name());
+ }
+ }
+ return res;
+}
+
+void OrganizerCore::spawnBinary(const QFileInfo &binary,
+ const QString &arguments,
+ const QDir &currentDirectory,
+ const QString &steamAppID,
+ const QString &customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries)
+{
+ DWORD processExitCode = 0;
+ HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, forcedLibraries, &processExitCode);
+ if (processHandle != INVALID_HANDLE_VALUE) {
+ refreshDirectoryStructure();
+ // need to remove our stored load order because it may be outdated if a foreign tool changed the
+ // file time. After removing that file, refreshESPList will use the file time as the order
+ if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) {
+ qDebug("removing loadorder.txt");
+ QFile::remove(m_CurrentProfile->getLoadOrderFileName());
+ }
+ refreshDirectoryStructure();
+
+ refreshESPList(true);
+ savePluginList();
+
+ //These callbacks should not fiddle with directoy structure and ESPs.
+ m_FinishedRun(binary.absoluteFilePath(), processExitCode);
+ }
+}
+
+HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary,
+ const QString &arguments,
+ const QString &profileName,
+ const QDir &currentDirectory,
+ const QString &steamAppID,
+ const QString &customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries,
+ LPDWORD exitCode)
+{
+ HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries);
+ if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) {
+ std::unique_ptr<LockedDialog> dlg;
+ ILockedWaitingForProcess* uilock = nullptr;
+
+ if (m_UserInterface != nullptr) {
+ uilock = m_UserInterface->lock();
+ }
+ else {
+ // i.e. when running command line shortcuts there is no m_UserInterface
+ dlg.reset(new LockedDialog);
+ dlg->show();
+ dlg->setEnabled(true);
+ uilock = dlg.get();
+ }
+
+ ON_BLOCK_EXIT([&]() {
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->unlock();
+ } });
+
+ DWORD ignoreExitCode;
+ waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock);
+ cycleDiagnostics();
+ }
+
+ return processHandle;
+}
+
+
+HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary,
+ const QString &arguments,
+ const QString &profileName,
+ const QDir &currentDirectory,
+ const QString &steamAppID,
+ const QString &customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries)
+{
+ prepareStart();
+
+ if (!binary.exists()) {
+ reportError(
+ tr("Executable not found: %1").arg(qUtf8Printable(binary.absoluteFilePath())));
+ return INVALID_HANDLE_VALUE;
+ }
+
+ if (!steamAppID.isEmpty()) {
+ ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str());
+ } else {
+ ::SetEnvironmentVariableW(L"SteamAPPId",
+ ToWString(m_Settings.getSteamAppID()).c_str());
+ }
+
+ QWidget *window = qApp->activeWindow();
+ if ((window != nullptr) && (!window->isVisible())) {
+ window = nullptr;
+ }
+
+ // This could possibly be extracted somewhere else but it's probably for when
+ // we have more than one provider of game registration.
+ if ((QFileInfo(
+ managedGame()->gameDirectory().absoluteFilePath("steam_api.dll"))
+ .exists()
+ || QFileInfo(managedGame()->gameDirectory().absoluteFilePath(
+ "steam_api64.dll"))
+ .exists())
+ && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) {
+
+ bool steamFound = true;
+ bool steamAccess = true;
+ if (!testForSteam(&steamFound, &steamAccess)) {
+ qCritical("unable to determine state of Steam");
+ }
+
+ if (!steamFound) {
+ QDialogButtonBox::StandardButton result;
+ result = QuestionBoxMemory::query(window, "steamQuery", binary.fileName(),
+ tr("Start Steam?"),
+ tr("Steam is required to be running already to correctly start the game. "
+ "Should MO try to start steam now?"),
+ QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
+ if (result == QDialogButtonBox::Yes) {
+ startSteam(window);
+
+ // double-check that Steam is started and MO has access
+ steamFound = true;
+ steamAccess = true;
+ if (!testForSteam(&steamFound, &steamAccess)) {
+ qCritical("unable to determine state of Steam");
+ } else if (!steamFound) {
+ qCritical("could not find Steam");
+ }
+
+ } else if (result == QDialogButtonBox::Cancel) {
+ return INVALID_HANDLE_VALUE;
+ }
+ }
+
+ if (!steamAccess) {
+ QDialogButtonBox::StandardButton result;
+ result = QuestionBoxMemory::query(window, "steamAdminQuery", binary.fileName(),
+ tr("Steam: Access Denied"),
+ 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"
+ "Restart MO as administrator?"),
+ QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
+ if (result == QDialogButtonBox::Yes) {
+ WCHAR cwd[MAX_PATH];
+ if (!GetCurrentDirectory(MAX_PATH, cwd)) {
+ qCritical("unable to get current directory (error %d)", GetLastError());
+ cwd[0] = L'\0';
+ }
+ if (!Helper::adminLaunch(
+ qApp->applicationDirPath().toStdWString(),
+ qApp->applicationFilePath().toStdWString(),
+ std::wstring(cwd))) {
+ qCritical("unable to relaunch MO as admin");
+ return INVALID_HANDLE_VALUE;
+ }
+ qApp->exit(0);
+ return INVALID_HANDLE_VALUE;
+ } else if (result == QDialogButtonBox::Cancel) {
+ return INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+
+ while (m_DirectoryUpdate) {
+ ::Sleep(100);
+ QCoreApplication::processEvents();
+ }
+
+ // need to make sure all data is saved before we start the application
+ if (m_CurrentProfile != nullptr) {
+ m_CurrentProfile->writeModlistNow(true);
+ }
+
+ // TODO: should also pass arguments
+ if (m_AboutToRun(binary.absoluteFilePath())) {
+ try {
+ m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
+ m_USVFS.updateForcedLibraries(forcedLibraries);
+
+ } catch (const UsvfsConnectorException &e) {
+ qDebug(e.what());
+ return INVALID_HANDLE_VALUE;
+ } catch (const std::exception &e) {
+ QMessageBox::warning(window, tr("Error"), e.what());
+ return INVALID_HANDLE_VALUE;
+ }
+
+ // Check if the Windows Event Logging service is running. For some reason, this seems to be
+ // critical to the successful running of usvfs.
+ if (!checkService()) {
+ if (QuestionBoxMemory::query(window, QString("eventLogService"), binary.fileName(),
+ tr("Windows Event Log Error"),
+ tr("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.\n\nContinue launching %1?").arg(binary.fileName()),
+ QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) {
+ return INVALID_HANDLE_VALUE;
+ }
+ }
+
+ for (auto exec : settings().executablesBlacklist().split(";")) {
+ if (exec.compare(binary.fileName(), Qt::CaseInsensitive) == 0) {
+ if (QuestionBoxMemory::query(window, QString("blacklistedExecutable"), binary.fileName(),
+ tr("Blacklisted Executable"),
+ tr("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.\n\nContinue launching %1?").arg(binary.fileName()),
+ QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) {
+ return INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+
+ QString modsPath = settings().getModDirectory();
+
+ // Check if this a request with either an executable or a working directory under our mods folder
+ // then will start the process in a virtualized "environment" with the appropriate paths fixed:
+ // (i.e. mods\FNIS\path\exe => game\data\path\exe)
+ QString cwdPath = currentDirectory.absolutePath();
+ bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive);
+ QString binPath = binary.absoluteFilePath();
+ bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive);
+ if (virtualizedCwd || virtualizedBin) {
+ if (virtualizedCwd) {
+ int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1);
+ QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
+ cwdPath = m_GamePlugin->dataDirectory().absolutePath();
+ if (cwdOffset >= 0)
+ cwdPath += adjustedCwd;
+
+ }
+
+ if (virtualizedBin) {
+ int binOffset = binPath.indexOf('/', modsPath.length() + 1);
+ QString adjustedBin = binPath.mid(binOffset, -1);
+ binPath = m_GamePlugin->dataDirectory().absolutePath();
+ if (binOffset >= 0)
+ binPath += adjustedBin;
+ }
+
+ QString cmdline
+ = QString("launch \"%1\" \"%2\" %3")
+ .arg(QDir::toNativeSeparators(cwdPath),
+ QDir::toNativeSeparators(binPath), arguments);
+
+ qDebug() << "Spawning proxyed process <" << cmdline << ">";
+
+ return startBinary(QFileInfo(QCoreApplication::applicationFilePath()),
+ cmdline, QCoreApplication::applicationDirPath(), true);
+ } else {
+ qDebug() << "Spawning direct process <" << binPath << "," << arguments << "," << cwdPath << ">";
+ return startBinary(binary, arguments, currentDirectory, true);
+ }
+ } else {
+ qDebug("start of \"%s\" canceled by plugin",
+ qUtf8Printable(binary.absoluteFilePath()));
+ return INVALID_HANDLE_VALUE;
+ }
+}
+
+HANDLE OrganizerCore::runShortcut(const MOShortcut& shortcut)
+{
+ if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance())
+ throw std::runtime_error(
+ QString("Refusing to run executable from different instance %1:%2")
+ .arg(shortcut.instance(),shortcut.executable())
+ .toLocal8Bit().constData());
+
+ Executable& exe = m_ExecutablesList.find(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,
+ m_CurrentProfile->name(),
+ exe.m_WorkingDirectory.length() != 0
+ ? exe.m_WorkingDirectory
+ : exe.m_BinaryInfo.absolutePath(),
+ exe.m_SteamAppID,
+ "",
+ forcedLibaries);
+}
+
+HANDLE OrganizerCore::startApplication(const QString &executable,
+ const QStringList &args,
+ const QString &cwd,
+ const QString &profile,
+ const QString &forcedCustomOverwrite,
+ bool ignoreCustomOverwrite)
+{
+ QFileInfo binary;
+ QString arguments = args.join(" ");
+ QString currentDirectory = cwd;
+ QString profileName = profile;
+ if (profile.length() == 0) {
+ if (m_CurrentProfile != nullptr) {
+ profileName = m_CurrentProfile->name();
+ } else {
+ throw MyException(tr("No profile set"));
+ }
+ }
+ QString steamAppID;
+ QString customOverwrite;
+ QList<ExecutableForcedLoadSetting> forcedLibraries;
+ if (executable.contains('\\') || executable.contains('/')) {
+ // file path
+
+ binary = QFileInfo(executable);
+ if (binary.isRelative()) {
+ // relative path, should be relative to game directory
+ binary = QFileInfo(
+ managedGame()->gameDirectory().absoluteFilePath(executable));
+ }
+ if (cwd.length() == 0) {
+ currentDirectory = binary.absolutePath();
+ }
+ try {
+ const Executable &exe = m_ExecutablesList.findByBinary(binary);
+ steamAppID = exe.m_SteamAppID;
+ customOverwrite
+ = m_CurrentProfile->setting("custom_overwrites", exe.m_Title)
+ .toString();
+ if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) {
+ forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title);
+ }
+ } catch (const std::runtime_error &) {
+ // nop
+ }
+ } else {
+ // only a file name, search executables list
+ try {
+ const Executable &exe = m_ExecutablesList.find(executable);
+ steamAppID = exe.m_SteamAppID;
+ customOverwrite
+ = m_CurrentProfile->setting("custom_overwrites", exe.m_Title)
+ .toString();
+ if (m_CurrentProfile->forcedLibrariesEnabled(exe.m_Title)) {
+ forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.m_Title);
+ }
+ if (arguments == "") {
+ arguments = exe.m_Arguments;
+ }
+ binary = exe.m_BinaryInfo;
+ if (cwd.length() == 0) {
+ currentDirectory = exe.m_WorkingDirectory;
+ }
+ } catch (const std::runtime_error &) {
+ qWarning("\"%s\" not set up as executable",
+ qUtf8Printable(executable));
+ binary = QFileInfo(executable);
+ }
+ }
+
+ if (!forcedCustomOverwrite.isEmpty())
+ customOverwrite = forcedCustomOverwrite;
+ if (ignoreCustomOverwrite)
+ customOverwrite.clear();
+
+ return spawnBinaryDirect(binary,
+ arguments,
+ profileName,
+ currentDirectory,
+ steamAppID,
+ customOverwrite,
+ forcedLibraries);
+}
+
+bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode)
+{
+ if (!Settings::instance().lockGUI())
+ return true;
+
+ ILockedWaitingForProcess* uilock = nullptr;
+ if (m_UserInterface != nullptr) {
+ uilock = m_UserInterface->lock();
+ }
+
+ ON_BLOCK_EXIT([&] () {
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->unlock();
+ } });
+ return waitForProcessCompletion(handle, exitCode, uilock);
+}
+
+bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock)
+{
+ bool originalHandle = true;
+ bool newHandle = true;
+ bool uiunlocked = false;
+
+ DWORD currentPID = 0;
+ QString processName;
+ auto waitForChildUntil = GetTickCount64();
+ if (handle != INVALID_HANDLE_VALUE) {
+ currentPID = GetProcessId(handle);
+ processName = QString::fromStdWString(getProcessName(handle));
+ }
+
+ // Certain process names we wish to "hide" for aesthetic reason:
+ bool waitingOnHidden = false;
+ std::vector<QString> hiddenList;
+ hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName());
+ for (QString hide : hiddenList)
+ if (processName.contains(hide, Qt::CaseInsensitive))
+ waitingOnHidden = true;
+ // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes.
+ // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want
+ // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden"
+ // process. For this reason we use exponential backoff and also start with a delibrately low value to improve
+ // the responsiveness of the initial update
+ DWORD64 nextHiddenCheck = GetTickCount64();
+ DWORD64 nextHiddenCheckDelay = 50;
+
+ constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1;
+ DWORD res = WAIT_TIMEOUT;
+ while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT))
+ {
+ if (newHandle) {
+ processName += QString(" (%1)").arg(currentPID);
+ if (uilock)
+ uilock->setProcessName(processName);
+ qDebug() << "Waiting for"
+ << (originalHandle ? "spawned" : "usvfs")
+ << "process completion :" << qUtf8Printable(processName);
+ newHandle = false;
+ }
+
+ // Wait for a an event on the handle, a key press, mouse click or timeout
+ res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON);
+ if (res == WAIT_FAILED) {
+ qWarning() << "Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED" << GetLastError();
+ break;
+ }
+
+ // keep processing events so the app doesn't appear dead
+ QCoreApplication::sendPostedEvents();
+ QCoreApplication::processEvents();
+
+ if (uilock && uilock->unlockForced()) {
+ uiunlocked = true;
+ break;
+ }
+
+ if (res == WAIT_OBJECT_0) {
+ // process we were waiting on has completed
+ if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode))
+ qWarning() << "Failed getting exit code of complete process :" << GetLastError();
+ CloseHandle(handle);
+ handle = INVALID_HANDLE_VALUE;
+ originalHandle = false;
+ // if the previous process spawned a child process and immediately exits we may miss it if we check immediately
+ waitForChildUntil = GetTickCount64() + 800;
+ }
+
+ // search for another process to wait on if either:
+ // 1. we just completed waiting for a process and need to find/wait for an inject child
+ // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on
+ bool firstIteration = true;
+ while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil)
+ || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck))
+ {
+ if (firstIteration)
+ firstIteration = false;
+ else {
+ QThread::msleep(200);
+ QCoreApplication::sendPostedEvents();
+ QCoreApplication::processEvents();
+ }
+
+ // search if there is another usvfs process active
+ handle = findAndOpenAUSVFSProcess(hiddenList, currentPID);
+ waitingOnHidden = false;
+ newHandle = handle != INVALID_HANDLE_VALUE;
+ if (newHandle) {
+ currentPID = GetProcessId(handle);
+ processName = QString::fromStdWString(getProcessName(handle));
+ for (QString hide : hiddenList)
+ if (processName.contains(hide, Qt::CaseInsensitive))
+ waitingOnHidden = true;
+ }
+ if (waitingOnHidden) {
+ nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay;
+ nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000);
+ }
+ else {
+ nextHiddenCheck = GetTickCount64();
+ nextHiddenCheckDelay = 200;
+ }
+ }
+ }
+
+ if (res == WAIT_OBJECT_0)
+ qDebug() << "Waiting for process completion successfull";
+ else if (uiunlocked)
+ qDebug() << "Waiting for process completion aborted by UI";
+ else
+ qDebug() << "Waiting for process completion not successfull :" << res;
+
+ if (handle != INVALID_HANDLE_VALUE)
+ ::CloseHandle(handle);
+
+ return res == WAIT_OBJECT_0;
+}
+
+HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid) {
+ // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics
+ // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid)
+ constexpr size_t querySize = 100;
+ DWORD pids[querySize];
+ size_t found = querySize;
+ if (!::GetVFSProcessList(&found, pids)) {
+ qWarning() << "Failed seeking USVFS processes : GetVFSProcessList failed?!";
+ return INVALID_HANDLE_VALUE;
+ }
+
+ HANDLE best_match = INVALID_HANDLE_VALUE;
+ bool best_match_hidden = true;
+ for (size_t i = 0; i < found; ++i) {
+ if (pids[i] == GetCurrentProcessId())
+ continue; // obviously don't wait for MO process
+
+ HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]);
+ if (handle == INVALID_HANDLE_VALUE) {
+ qWarning() << "Failed openning USVFS process " << pids[i] << " : OpenProcess failed" << GetLastError();
+ continue;
+ }
+
+ QString pname = QString::fromStdWString(getProcessName(handle));
+ bool phidden = false;
+ for (auto hide : hiddenList)
+ if (pname.contains(hide, Qt::CaseInsensitive))
+ phidden = true;
+
+ bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid;
+
+ if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) {
+ if (best_match != INVALID_HANDLE_VALUE)
+ CloseHandle(best_match);
+ best_match = handle;
+ best_match_hidden = phidden;
+ }
+ else
+ CloseHandle(handle);
+
+ if (!phidden && pprefered)
+ return best_match;
+ }
+
+ return best_match;
+}
+
+bool OrganizerCore::onAboutToRun(
+ const std::function<bool(const QString &)> &func)
+{
+ auto conn = m_AboutToRun.connect(func);
+ return conn.connected();
+}
+
+bool OrganizerCore::onFinishedRun(
+ const std::function<void(const QString &, unsigned int)> &func)
+{
+ auto conn = m_FinishedRun.connect(func);
+ return conn.connected();
+}
+
+bool OrganizerCore::onModInstalled(
+ const std::function<void(const QString &)> &func)
+{
+ auto conn = m_ModInstalled.connect(func);
+ return conn.connected();
+}
+
+void OrganizerCore::refreshModList(bool saveChanges)
+{
+ // don't lose changes!
+ if (saveChanges) {
+ m_CurrentProfile->writeModlistNow(true);
+ }
+ ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
+ m_PluginContainer, m_Settings.displayForeign(), managedGame());
+
+ m_CurrentProfile->refreshModStatus();
+
+ m_ModList.notifyChange(-1);
+
+ refreshDirectoryStructure();
+}
+
+void OrganizerCore::refreshESPList(bool force)
+{
+ if (m_DirectoryUpdate) {
+ // don't mess up the esp list if we're currently updating the directory
+ // structure
+ m_PostRefreshTasks.append([=]() {
+ this->refreshESPList(force);
+ });
+ return;
+ }
+ m_CurrentProfile->writeModlist();
+
+ // clear list
+ try {
+ m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure,
+ m_CurrentProfile->getLockedOrderFileName(), force);
+ } catch (const std::exception &e) {
+ reportError(tr("Failed to refresh list of esps: %1").arg(e.what()));
+ }
+}
+
+void OrganizerCore::refreshBSAList()
+{
+ DataArchives *archives = m_GamePlugin->feature<DataArchives>();
+
+ if (archives != nullptr) {
+ m_ArchivesInit = false;
+
+ // default archives are the ones enabled outside MO. if the list can't be
+ // found (which might
+ // happen if ini files are missing) use hard-coded defaults (preferrably the
+ // same the game would use)
+ m_DefaultArchives = archives->archives(m_CurrentProfile);
+ if (m_DefaultArchives.length() == 0) {
+ m_DefaultArchives = archives->vanillaArchives();
+ }
+
+ m_ActiveArchives.clear();
+
+ auto iter = enabledArchives();
+ m_ActiveArchives = toStringList(iter.begin(), iter.end());
+ if (m_ActiveArchives.isEmpty()) {
+ m_ActiveArchives = m_DefaultArchives;
+ }
+
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->updateBSAList(m_DefaultArchives, m_ActiveArchives);
+ }
+
+ m_ArchivesInit = true;
+ }
+}
+
+void OrganizerCore::refreshLists()
+{
+ if ((m_CurrentProfile != nullptr) && m_DirectoryStructure->isPopulated()) {
+ refreshESPList(true);
+ refreshBSAList();
+ } // no point in refreshing lists if no files have been added to the directory
+ // tree
+}
+
+void OrganizerCore::updateModActiveState(int index, bool active)
+{
+ QList<unsigned int> modsToUpdate;
+ modsToUpdate.append(index);
+ updateModsActiveState(modsToUpdate, active);
+}
+
+void OrganizerCore::updateModsActiveState(const QList<unsigned int> &modIndices, bool active)
+{
+ int enabled = 0;
+ for (auto index : modIndices) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ QDir dir(modInfo->absolutePath());
+ for (const QString &esm :
+ dir.entryList(QStringList() << "*.esm", QDir::Files)) {
+ const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esm));
+ if (file.get() == nullptr) {
+ qWarning("failed to activate %s", qUtf8Printable(esm));
+ continue;
+ }
+
+ if (active != m_PluginList.isEnabled(esm)
+ && file->getAlternatives().empty()) {
+ m_PluginList.blockSignals(true);
+ m_PluginList.enableESP(esm, active);
+ m_PluginList.blockSignals(false);
+ }
+ }
+
+ for (const QString &esl :
+ dir.entryList(QStringList() << "*.esl", QDir::Files)) {
+ const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esl));
+ if (file.get() == nullptr) {
+ qWarning("failed to activate %s", qUtf8Printable(esl));
+ continue;
+ }
+
+ if (active != m_PluginList.isEnabled(esl)
+ && file->getAlternatives().empty()) {
+ m_PluginList.blockSignals(true);
+ m_PluginList.enableESP(esl, active);
+ m_PluginList.blockSignals(false);
+ ++enabled;
+ }
+ }
+ QStringList esps = dir.entryList(QStringList() << "*.esp", QDir::Files);
+ for (const QString &esp : esps) {
+ const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esp));
+ if (file.get() == nullptr) {
+ qWarning("failed to activate %s", qUtf8Printable(esp));
+ continue;
+ }
+
+ if (active != m_PluginList.isEnabled(esp)
+ && file->getAlternatives().empty()) {
+ m_PluginList.blockSignals(true);
+ m_PluginList.enableESP(esp, active);
+ m_PluginList.blockSignals(false);
+ ++enabled;
+ }
+ }
+ }
+ if (active && (enabled > 1)) {
+ MessageDialog::showMessage(
+ tr("Multiple esps/esls activated, please check that they don't conflict."),
+ qApp->activeWindow());
+ }
+ m_PluginList.refreshLoadOrder();
+ // immediately save affected lists
+ m_PluginListsWriter.writeImmediately(false);
+}
+
+void OrganizerCore::updateModInDirectoryStructure(unsigned int index,
+ ModInfo::Ptr modInfo)
+{
+ QMap<unsigned int, ModInfo::Ptr> allModInfo;
+ allModInfo[index] = modInfo;
+ updateModsInDirectoryStructure(allModInfo);
+}
+
+void OrganizerCore::updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> modInfo)
+{
+ for (auto idx : modInfo.keys()) {
+ // add files of the bsa to the directory structure
+ m_DirectoryRefresher.addModFilesToStructure(
+ m_DirectoryStructure, modInfo[idx]->name(),
+ m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(),
+ modInfo[idx]->stealFiles());
+ }
+ DirectoryRefresher::cleanStructure(m_DirectoryStructure);
+ // need to refresh plugin list now so we can activate esps
+ refreshESPList(true);
+ // activate all esps of the specified mod so the bsas get activated along with
+ // it
+ m_PluginList.blockSignals(true);
+ updateModsActiveState(modInfo.keys(), true);
+ m_PluginList.blockSignals(false);
+ // now we need to refresh the bsa list and save it so there is no confusion
+ // about what archives are avaiable and active
+ refreshBSAList();
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->archivesWriter().writeImmediately(false);
+ }
+
+ std::vector<QString> archives = enabledArchives();
+ m_DirectoryRefresher.setMods(
+ m_CurrentProfile->getActiveMods(),
+ std::set<QString>(archives.begin(), archives.end()));
+
+ // finally also add files from bsas to the directory structure
+ for (auto idx : modInfo.keys()) {
+ m_DirectoryRefresher.addModBSAToStructure(
+ m_DirectoryStructure, modInfo[idx]->name(),
+ m_CurrentProfile->getModPriority(idx), modInfo[idx]->absolutePath(),
+ modInfo[idx]->archives());
+ }
+}
+
+void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply)
+{
+ if (m_PluginContainer != nullptr) {
+ for (IPluginModPage *modPage :
+ m_PluginContainer->plugins<MOBase::IPluginModPage>()) {
+ ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo();
+ if (modPage->handlesDownload(url, reply->url(), *fileInfo)) {
+ fileInfo->repository = modPage->name();
+ m_DownloadManager.addDownload(reply, fileInfo);
+ return;
+ }
+ }
+ }
+
+ // no mod found that could handle the download. Is it a nexus mod?
+ if (url.host() == "www.nexusmods.com") {
+ QString gameName = "";
+ int modID = 0;
+ int fileID = 0;
+ QRegExp nameExp("www\\.nexusmods\\.com/(\\a+)/");
+ if (nameExp.indexIn(url.toString()) != -1) {
+ gameName = nameExp.cap(1);
+ }
+ QRegExp modExp("mods/(\\d+)");
+ if (modExp.indexIn(url.toString()) != -1) {
+ modID = modExp.cap(1).toInt();
+ }
+ QRegExp fileExp("fid=(\\d+)");
+ if (fileExp.indexIn(reply->url().toString()) != -1) {
+ fileID = fileExp.cap(1).toInt();
+ }
+ m_DownloadManager.addDownload(reply,
+ new ModRepositoryFileInfo(gameName, modID, fileID));
+ } else {
+ if (QMessageBox::question(qApp->activeWindow(), tr("Download?"),
+ tr("A download has been started but no installed "
+ "page plugin recognizes it.\n"
+ "If you download anyway no information (i.e. "
+ "version) will be associated with the "
+ "download.\n"
+ "Continue?"),
+ QMessageBox::Yes | QMessageBox::No)
+ == QMessageBox::Yes) {
+ m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo());
+ }
+ }
+}
+
+ModListSortProxy *OrganizerCore::createModListProxyModel()
+{
+ ModListSortProxy *result = new ModListSortProxy(m_CurrentProfile, this);
+ result->setSourceModel(&m_ModList);
+ return result;
+}
+
+PluginListSortProxy *OrganizerCore::createPluginListProxyModel()
+{
+ PluginListSortProxy *result = new PluginListSortProxy(this);
+ result->setSourceModel(&m_PluginList);
+ return result;
+}
+
+IPluginGame const *OrganizerCore::managedGame() const
+{
+ return m_GamePlugin;
+}
+
+std::vector<QString> OrganizerCore::enabledArchives()
+{
+ std::vector<QString> result;
+ if (m_ArchiveParsing) {
+ QFile archiveFile(m_CurrentProfile->getArchivesFileName());
+ if (archiveFile.open(QIODevice::ReadOnly)) {
+ while (!archiveFile.atEnd()) {
+ result.push_back(QString::fromUtf8(archiveFile.readLine()).trimmed());
+ }
+ archiveFile.close();
+ }
+ }
+ return result;
+}
+
+void OrganizerCore::refreshDirectoryStructure()
+{
+ if (!m_DirectoryUpdate) {
+ m_CurrentProfile->writeModlistNow(true);
+
+ m_DirectoryUpdate = true;
+ std::vector<std::tuple<QString, QString, int>> activeModList
+ = m_CurrentProfile->getActiveMods();
+ auto archives = enabledArchives();
+ m_DirectoryRefresher.setMods(
+ activeModList, std::set<QString>(archives.begin(), archives.end()));
+
+ QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh()));
+ }
+}
+
+void OrganizerCore::directory_refreshed()
+{
+ DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure();
+ Q_ASSERT(newStructure != m_DirectoryStructure);
+ if (newStructure != nullptr) {
+ std::swap(m_DirectoryStructure, newStructure);
+ delete newStructure;
+ } else {
+ // TODO: don't know why this happens, this slot seems to get called twice
+ // with only one emit
+ return;
+ }
+ m_DirectoryUpdate = false;
+
+ for (int i = 0; i < m_ModList.rowCount(); ++i) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ modInfo->clearCaches();
+ }
+ for (auto task : m_PostRefreshTasks) {
+ task();
+ }
+ m_PostRefreshTasks.clear();
+
+ if (m_CurrentProfile != nullptr) {
+ refreshLists();
+ }
+}
+
+void OrganizerCore::profileRefresh()
+{
+ // have to refresh mods twice (again in refreshModList), otherwise the refresh
+ // isn't complete. Not sure why
+ ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
+ m_PluginContainer, m_Settings.displayForeign(), managedGame());
+ m_CurrentProfile->refreshModStatus();
+
+ refreshModList();
+}
+
+void OrganizerCore::modStatusChanged(unsigned int index)
+{
+ try {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ if (m_CurrentProfile->modEnabled(index)) {
+ updateModInDirectoryStructure(index, modInfo);
+ } else {
+ updateModActiveState(index, false);
+ if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) {
+ FilesOrigin &origin
+ = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name()));
+ origin.enable(false);
+ }
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->archivesWriter().write();
+ }
+ }
+ modInfo->clearCaches();
+
+ for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ int priority = m_CurrentProfile->getModPriority(i);
+ if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) {
+ // priorities in the directory structure are one higher because data is
+ // 0
+ m_DirectoryStructure->getOriginByName(ToWString(modInfo->name()))
+ .setPriority(priority + 1);
+ }
+ }
+ m_DirectoryStructure->getFileRegister()->sortOrigins();
+
+ refreshLists();
+ } catch (const std::exception &e) {
+ reportError(tr("failed to update mod list: %1").arg(e.what()));
+ }
+}
+
+void OrganizerCore::modStatusChanged(QList<unsigned int> index) {
+ try {
+ QMap<unsigned int, ModInfo::Ptr> modsToEnable;
+ QMap<unsigned int, ModInfo::Ptr> modsToDisable;
+ for (auto idx : index) {
+ if (m_CurrentProfile->modEnabled(idx)) {
+ modsToEnable[idx] = ModInfo::getByIndex(idx);
+ } else {
+ modsToDisable[idx] = ModInfo::getByIndex(idx);
+ }
+ }
+ if (!modsToEnable.isEmpty()) {
+ updateModsInDirectoryStructure(modsToEnable);
+ for (auto modInfo : modsToEnable.values()) {
+ modInfo->clearCaches();
+ }
+ }
+ if (!modsToDisable.isEmpty()) {
+ updateModsActiveState(modsToDisable.keys(), false);
+ for (auto idx : modsToDisable.keys()) {
+ if (m_DirectoryStructure->originExists(ToWString(modsToDisable[idx]->name()))) {
+ FilesOrigin &origin
+ = m_DirectoryStructure->getOriginByName(ToWString(modsToDisable[idx]->name()));
+ origin.enable(false);
+ }
+ }
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->archivesWriter().write();
+ }
+ }
+
+ for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ int priority = m_CurrentProfile->getModPriority(i);
+ if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) {
+ // priorities in the directory structure are one higher because data is
+ // 0
+ m_DirectoryStructure->getOriginByName(ToWString(modInfo->name()))
+ .setPriority(priority + 1);
+ }
+ }
+ m_DirectoryStructure->getFileRegister()->sortOrigins();
+
+ refreshLists();
+ } catch (const std::exception &e) {
+ reportError(tr("failed to update mod list: %1").arg(e.what()));
+ }
+}
+
+void OrganizerCore::loginSuccessful(bool necessary)
+{
+ if (necessary) {
+ MessageDialog::showMessage(tr("login successful"), qApp->activeWindow());
+ }
+ for (QString url : m_PendingDownloads) {
+ downloadRequestedNXM(url);
+ }
+ m_PendingDownloads.clear();
+ for (auto task : m_PostLoginTasks) {
+ task();
+ }
+
+ m_PostLoginTasks.clear();
+ NexusInterface::instance(m_PluginContainer)->loginCompleted();
+}
+
+void OrganizerCore::loginSuccessfulUpdate(bool necessary)
+{
+ if (necessary) {
+ MessageDialog::showMessage(tr("login successful"), qApp->activeWindow());
+ }
+ m_Updater.startUpdate();
+}
+
+void OrganizerCore::loginFailed(const QString &message)
+{
+ if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"),
+ tr("Login failed, try again?"))
+ == QMessageBox::Yes) {
+ if (nexusApi(true)) {
+ return;
+ }
+ }
+
+ if (!m_PendingDownloads.isEmpty()) {
+ MessageDialog::showMessage(
+ tr("login failed: %1. Download will not be associated with an account")
+ .arg(message),
+ qApp->activeWindow());
+ for (QString url : m_PendingDownloads) {
+ downloadRequestedNXM(url);
+ }
+ m_PendingDownloads.clear();
+ } else {
+ MessageDialog::showMessage(tr("login failed: %1").arg(message),
+ qApp->activeWindow());
+ m_PostLoginTasks.clear();
+ }
+ NexusInterface::instance(m_PluginContainer)->loginCompleted();
+}
+
+void OrganizerCore::loginFailedUpdate(const QString &message)
+{
+ MessageDialog::showMessage(
+ tr("login failed: %1. You need to log-in with Nexus to update MO.")
+ .arg(message),
+ qApp->activeWindow());
+}
+
+void OrganizerCore::syncOverwrite()
+{
+ unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
+ std::vector<ModInfo::EFlag> flags = mod->getFlags();
+ return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE)
+ != flags.end();
+ });
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex);
+ SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure,
+ qApp->activeWindow());
+ if (syncDialog.exec() == QDialog::Accepted) {
+ syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory()));
+ modInfo->testValid();
+ refreshDirectoryStructure();
+ }
+}
+
+QString OrganizerCore::oldMO1HookDll() const
+{
+ if (auto extender = managedGame()->feature<ScriptExtender>()) {
+ QString hookdll = QDir::toNativeSeparators(
+ managedGame()->dataDirectory().absoluteFilePath(extender->PluginPath() + "/hook.dll"));
+ if (QFile(hookdll).exists())
+ return hookdll;
+ }
+ return QString();
+}
+
+std::vector<unsigned int> OrganizerCore::activeProblems() const
+{
+ std::vector<unsigned int> problems;
+ const auto& hookdll = oldMO1HookDll();
+ if (!hookdll.isEmpty()) {
+ // This warning will now be shown every time the problems are checked, which is a bit
+ // of a "log spam". But since this is a sevre error which will most likely make the
+ // game crash/freeze/etc. and is very hard to diagnose, this "log spam" will make it
+ // easier for the user to notice the warning.
+ qWarning("hook.dll found in game folder: %s", qUtf8Printable(hookdll));
+ problems.push_back(PROBLEM_MO1SCRIPTEXTENDERWORKAROUND);
+ }
+ return problems;
+}
+
+QString OrganizerCore::shortDescription(unsigned int key) const
+{
+ switch (key) {
+ case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: {
+ return tr("MO1 \"Script Extender\" load mechanism has left hook.dll in your game folder");
+ } break;
+ default: {
+ return tr("Description missing");
+ } break;
+ }
+}
+
+QString OrganizerCore::fullDescription(unsigned int key) const
+{
+ switch (key) {
+ case PROBLEM_MO1SCRIPTEXTENDERWORKAROUND: {
+ return tr("<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.").arg(oldMO1HookDll());
+ break;
+ }
+ default: {
+ return tr("Description missing");
+ } break;
+ }
+}
+
+bool OrganizerCore::hasGuidedFix(unsigned int) const
+{
+ return false;
+}
+
+void OrganizerCore::startGuidedFix(unsigned int) const
+{
+}
+
+bool OrganizerCore::saveCurrentLists()
+{
+ if (m_DirectoryUpdate) {
+ qWarning("not saving lists during directory update");
+ return false;
+ }
+
+ try {
+ savePluginList();
+ if (m_UserInterface != nullptr) {
+ m_UserInterface->archivesWriter().write();
+ }
+ } catch (const std::exception &e) {
+ reportError(tr("failed to save load order: %1").arg(e.what()));
+ }
+
+ return true;
+}
+
+void OrganizerCore::savePluginList()
+{
+ if (m_DirectoryUpdate) {
+ // delay save till after directory update
+ m_PostRefreshTasks.append([this]() {
+ this->savePluginList();
+ });
+ return;
+ }
+ m_PluginList.saveTo(m_CurrentProfile->getLockedOrderFileName(),
+ m_CurrentProfile->getDeleterFileName(),
+ m_Settings.hideUncheckedPlugins());
+ m_PluginList.saveLoadOrder(*m_DirectoryStructure);
+}
+
+void OrganizerCore::prepareStart()
+{
+ if (m_CurrentProfile == nullptr) {
+ return;
+ }
+ m_CurrentProfile->writeModlist();
+ m_CurrentProfile->createTweakedIniFile();
+ saveCurrentLists();
+ m_Settings.setupLoadMechanism();
+ storeSettings();
+}
+
+std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName,
+ const QString &customOverwrite)
+{
+ // need to wait until directory structure
+ while (m_DirectoryUpdate) {
+ ::Sleep(100);
+ QCoreApplication::processEvents();
+ }
+
+ IPluginGame *game = qApp->property("managed_game").value<IPluginGame *>();
+ Profile profile(QDir(m_Settings.getProfileDirectory() + "/" + profileName),
+ game);
+
+ MappingType result;
+
+ QString dataPath
+ = QDir::toNativeSeparators(game->dataDirectory().absolutePath());
+
+ bool overwriteActive = false;
+
+ for (auto mod : profile.getActiveMods()) {
+ if (std::get<0>(mod).compare("overwrite", Qt::CaseInsensitive) == 0) {
+ continue;
+ }
+
+ unsigned int modIndex = ModInfo::getIndex(std::get<0>(mod));
+ ModInfo::Ptr modPtr = ModInfo::getByIndex(modIndex);
+
+ bool createTarget = customOverwrite == std::get<0>(mod);
+
+ overwriteActive |= createTarget;
+
+ if (modPtr->isRegular()) {
+ result.insert(result.end(), {QDir::toNativeSeparators(std::get<1>(mod)),
+ dataPath, true, createTarget});
+ }
+ }
+
+ if (!overwriteActive && !customOverwrite.isEmpty()) {
+ throw MyException(tr("The designated write target \"%1\" is not enabled.")
+ .arg(customOverwrite));
+ }
+
+ if (m_CurrentProfile->localSavesEnabled()) {
+ LocalSavegames *localSaves = game->feature<LocalSavegames>();
+ if (localSaves != nullptr) {
+ MappingType saveMap
+ = localSaves->mappings(currentProfile()->absolutePath() + "/saves");
+ result.reserve(result.size() + saveMap.size());
+ result.insert(result.end(), saveMap.begin(), saveMap.end());
+ } else {
+ qWarning("local save games not supported by this game plugin");
+ }
+ }
+
+ result.insert(result.end(), {
+ QDir::toNativeSeparators(m_Settings.getOverwriteDirectory()),
+ dataPath,
+ true,
+ customOverwrite.isEmpty()
+ });
+
+ for (MOBase::IPluginFileMapper *mapper :
+ m_PluginContainer->plugins<MOBase::IPluginFileMapper>()) {
+ IPlugin *plugin = dynamic_cast<IPlugin *>(mapper);
+ if (plugin->isActive()) {
+ MappingType pluginMap = mapper->mappings();
+ result.reserve(result.size() + pluginMap.size());
+ result.insert(result.end(), pluginMap.begin(), pluginMap.end());
+ }
+ }
+
+ return result;
+}
+
+
+std::vector<Mapping> OrganizerCore::fileMapping(
+ const QString &dataPath, const QString &relPath, const DirectoryEntry *base,
+ const DirectoryEntry *directoryEntry, int createDestination)
+{
+ std::vector<Mapping> result;
+
+ for (FileEntry::Ptr current : directoryEntry->getFiles()) {
+ bool isArchive = false;
+ int origin = current->getOrigin(isArchive);
+ if (isArchive || (origin == 0)) {
+ continue;
+ }
+
+ QString originPath
+ = QString::fromStdWString(base->getOriginByID(origin).getPath());
+ QString fileName = QString::fromStdWString(current->getName());
+// QString fileName = ToQString(current->getName());
+ QString source = originPath + relPath + fileName;
+ QString target = dataPath + relPath + fileName;
+ if (source != target) {
+ result.push_back({source, target, false, false});
+ }
+ }
+
+ // recurse into subdirectories
+ std::vector<DirectoryEntry *>::const_iterator current, end;
+ directoryEntry->getSubDirectories(current, end);
+ for (; current != end; ++current) {
+ int origin = (*current)->anyOrigin();
+
+ QString originPath
+ = QString::fromStdWString(base->getOriginByID(origin).getPath());
+ QString dirName = QString::fromStdWString((*current)->getName());
+ QString source = originPath + relPath + dirName;
+ QString target = dataPath + relPath + dirName;
+
+ bool writeDestination
+ = (base == directoryEntry) && (origin == createDestination);
+
+ result.push_back({source, target, true, writeDestination});
+ std::vector<Mapping> subRes = fileMapping(
+ dataPath, relPath + dirName + "\\", base, *current, createDestination);
+ result.insert(result.end(), subRes.begin(), subRes.end());
+ }
+ return result;
+}
diff --git a/src/organizercore.h b/src/organizercore.h
index 086fa11d..bfb72529 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -20,6 +20,7 @@
#include <versioninfo.h>
#include <delayedfilewriter.h>
#include <boost/signals2.hpp>
+#include "executableinfo.h"
class ModListSortProxy;
class PluginListSortProxy;
@@ -135,26 +136,30 @@ public:
void refreshDirectoryStructure();
void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
+ void updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> modInfos);
void doAfterLogin(const std::function<void()> &function) { m_PostLoginTasks.append(function); }
void spawnBinary(const QFileInfo &binary, const QString &arguments = "",
const QDir &currentDirectory = QDir(),
const QString &steamAppID = "",
- const QString &customOverwrite = "");
+ const QString &customOverwrite = "",
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>());
HANDLE spawnBinaryDirect(const QFileInfo &binary, const QString &arguments,
const QString &profileName,
const QDir &currentDirectory,
const QString &steamAppID,
const QString &customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>(),
LPDWORD exitCode = nullptr);
HANDLE spawnBinaryProcess(const QFileInfo &binary, const QString &arguments,
const QString &profileName,
const QDir &currentDirectory,
const QString &steamAppID,
- const QString &customOverwrite);
+ const QString &customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>());
void loginSuccessfulUpdate(bool necessary);
void loginFailedUpdate(const QString &message);
@@ -182,6 +187,7 @@ public:
QString downloadsPath() const;
QString overwritePath() const;
QString basePath() const;
+ QString modsPath() const;
MOBase::VersionInfo appVersion() const;
MOBase::IModInterface *getMod(const QString &name) const;
MOBase::IPluginGame *getGame(const QString &gameName) const;
@@ -203,7 +209,7 @@ public:
PluginList *pluginList();
ModList *modList();
HANDLE runShortcut(const MOShortcut& shortcut);
- HANDLE startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile);
+ HANDLE startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile, const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false);
bool waitForApplication(HANDLE processHandle, LPDWORD exitCode = nullptr);
HANDLE findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid);
bool onModInstalled(const std::function<void (const QString &)> &func);
@@ -211,6 +217,8 @@ public:
bool onFinishedRun(const std::function<void (const QString &, unsigned int)> &func);
void refreshModList(bool saveChanges = true);
QStringList modsSortedByProfilePriority() const;
+ bool getArchiveParsing() const;
+ void setArchiveParsing(bool archiveParsing);
public: // IPluginDiagnose interface
@@ -234,10 +242,11 @@ public slots:
void installDownload(int downloadIndex);
void modStatusChanged(unsigned int index);
+ void modStatusChanged(QList<unsigned int> index);
void requestDownload(const QUrl &url, QNetworkReply *reply);
void downloadRequestedNXM(const QString &url);
- bool nexusLogin(bool retry = false);
+ bool nexusApi(bool retry = false);
signals:
@@ -259,11 +268,12 @@ private:
QString commitSettings(const QString &iniFile);
- bool queryLogin(QString &username, QString &password);
+ bool queryApi(QString &apiKey);
void updateModActiveState(int index, bool active);
+ void updateModsActiveState(const QList<unsigned int> &modIndices, bool active);
- bool testForSteam();
+ bool testForSteam(bool *found, bool *access);
bool createDirectory(const QString &path);
@@ -332,9 +342,9 @@ private:
QThread m_RefresherThread;
- bool m_AskForNexusPW;
bool m_DirectoryUpdate;
bool m_ArchivesInit;
+ bool m_ArchiveParsing{ m_Settings.archiveParsing() };
MOBase::DelayedFileWriter m_PluginListsWriter;
UsvfsConnector m_USVFS;
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp
index 8e0bc95b..d6996b3a 100644
--- a/src/organizerproxy.cpp
+++ b/src/organizerproxy.cpp
@@ -39,9 +39,6 @@ QString OrganizerProxy::downloadsPath() const
QString OrganizerProxy::overwritePath() const
{
- /*return QDir::fromNativeSeparators(qApp->property("dataPath").toString())
- + "/"
- + ToQString(AppConfig::overwritePath());*/
return m_Proxied->overwritePath();
}
@@ -50,6 +47,11 @@ QString OrganizerProxy::basePath() const
return m_Proxied->basePath();
}
+QString OrganizerProxy::modsPath() const
+{
+ return m_Proxied->modsPath();
+}
+
VersionInfo OrganizerProxy::appVersion() const
{
return m_Proxied->appVersion();
@@ -105,9 +107,10 @@ QString OrganizerProxy::pluginDataPath() const
return m_Proxied->pluginDataPath();
}
-HANDLE OrganizerProxy::startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile)
+HANDLE OrganizerProxy::startApplication(const QString &executable, const QStringList &args, const QString &cwd,
+ const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite)
{
- return m_Proxied->startApplication(executable, args, cwd, profile);
+ return m_Proxied->startApplication(executable, args, cwd, profile, forcedCustomOverwrite, ignoreCustomOverwrite);
}
bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const
diff --git a/src/organizerproxy.h b/src/organizerproxy.h
index b8eb9b82..c8a96ed3 100644
--- a/src/organizerproxy.h
+++ b/src/organizerproxy.h
@@ -20,6 +20,7 @@ public:
virtual QString downloadsPath() const;
virtual QString overwritePath() const;
virtual QString basePath() const;
+ virtual QString modsPath() const;
virtual MOBase::VersionInfo appVersion() const;
virtual MOBase::IModInterface *getMod(const QString &name) const;
virtual MOBase::IPluginGame *getGame(const QString &gameName) const;
@@ -42,7 +43,8 @@ public:
virtual MOBase::IPluginList *pluginList() const;
virtual MOBase::IModList *modList() const;
virtual MOBase::IProfile *profile() const override;
- virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "", const QString &profile = "");
+ virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "",
+ const QString &profile = "", const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false);
virtual bool waitForApplication(HANDLE handle, LPDWORD exitCode = nullptr) const;
virtual void refreshModList(bool saveChanges);
diff --git a/src/overwriteinfodialog.cpp b/src/overwriteinfodialog.cpp
index 1fe3f645..3d82cf17 100644
--- a/src/overwriteinfodialog.cpp
+++ b/src/overwriteinfodialog.cpp
@@ -110,7 +110,7 @@ void OverwriteInfoDialog::setModInfo(ModInfo::Ptr modInfo)
if (QDir(modInfo->absolutePath()).exists()) {
m_FileSystemModel->setRootPath(modInfo->absolutePath());
} else {
- throw MyException(tr("%1 not found").arg(modInfo->absolutePath()));
+ throw MyException(tr("mod not found: %1").arg(qUtf8Printable(modInfo->absolutePath())));
}
}
@@ -220,8 +220,8 @@ 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)res <= 32) {
- qCritical("failed to invoke %s: %d", fileName.toUtf8().constData(), res);
+ if ((INT_PTR)res <= 32) {
+ qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res);
}
}
diff --git a/src/persistentcookiejar.cpp b/src/persistentcookiejar.cpp
index a6eb78fa..1ed463c6 100644
--- a/src/persistentcookiejar.cpp
+++ b/src/persistentcookiejar.cpp
@@ -11,7 +11,7 @@ PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *paren
}
PersistentCookieJar::~PersistentCookieJar() {
- qDebug("save %s", qPrintable(m_FileName));
+ qDebug("save %s", qUtf8Printable(m_FileName));
save();
}
@@ -40,14 +40,14 @@ void PersistentCookieJar::save() {
QFile oldCookies(m_FileName);
if (oldCookies.exists()) {
if (!oldCookies.remove()) {
- qCritical("failed to save cookies: failed to remove %s", qPrintable(m_FileName));
+ qCritical("failed to save cookies: failed to remove %s", qUtf8Printable(m_FileName));
return;
}
} // if it doesn't exists that's fine
}
if (!file.copy(m_FileName)) {
- qCritical("failed to save cookies: failed to write %s", qPrintable(m_FileName));
+ qCritical("failed to save cookies: failed to write %s", qUtf8Printable(m_FileName));
}
}
diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp
index 8935c472..2126c5ef 100644
--- a/src/plugincontainer.cpp
+++ b/src/plugincontainer.cpp
@@ -84,6 +84,10 @@ void PluginContainer::registerGame(IPluginGame *game)
bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
{
+ // Storing the original QObject* is a bit of a hack as I couldn't figure out any
+ // way to cast directly between IPlugin* and IPluginDiagnose*
+ bf::at_key<QObject>(m_Plugins).push_back(plugin);
+
{ // generic treatment for all plugins
IPlugin *pluginObj = qobject_cast<IPlugin*>(plugin);
if (pluginObj == nullptr) {
@@ -160,12 +164,12 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
for (QObject *proxiedPlugin : matchingPlugins) {
if (proxiedPlugin != nullptr) {
if (registerPlugin(proxiedPlugin, pluginName)) {
- qDebug("loaded plugin \"%s\"", qPrintable(QFileInfo(pluginName).fileName()));
+ qDebug("loaded plugin \"%s\"", qUtf8Printable(QFileInfo(pluginName).fileName()));
}
else {
qWarning("plugin \"%s\" failed to load. If this plugin is for an older version of MO "
"you have to update it or delete it if no update exists.",
- qPrintable(pluginName));
+ qUtf8Printable(pluginName));
}
}
}
@@ -220,7 +224,7 @@ void PluginContainer::unloadPlugins()
QPluginLoader *loader = m_PluginLoaders.back();
m_PluginLoaders.pop_back();
if ((loader != nullptr) && !loader->unload()) {
- qDebug("failed to unload %s: %s", qPrintable(loader->fileName()), qPrintable(loader->errorString()));
+ qDebug("failed to unload %s: %s", qUtf8Printable(loader->fileName()), qUtf8Printable(loader->errorString()));
}
delete loader;
}
@@ -275,7 +279,7 @@ void PluginContainer::loadPlugins()
while (iter.hasNext()) {
iter.next();
if (m_Organizer->settings().pluginBlacklisted(iter.fileName())) {
- qDebug("plugin \"%s\" blacklisted", qPrintable(iter.fileName()));
+ qDebug("plugin \"%s\" blacklisted", qUtf8Printable(iter.fileName()));
continue;
}
loadCheck.write(iter.fileName().toUtf8());
@@ -287,14 +291,14 @@ void PluginContainer::loadPlugins()
if (pluginLoader->instance() == nullptr) {
m_FailedPlugins.push_back(pluginName);
qCritical("failed to load plugin %s: %s",
- qPrintable(pluginName), qPrintable(pluginLoader->errorString()));
+ qUtf8Printable(pluginName), qUtf8Printable(pluginLoader->errorString()));
} else {
if (registerPlugin(pluginLoader->instance(), pluginName)) {
- qDebug("loaded plugin \"%s\"", qPrintable(QFileInfo(pluginName).fileName()));
+ qDebug("loaded plugin \"%s\"", qUtf8Printable(QFileInfo(pluginName).fileName()));
m_PluginLoaders.push_back(pluginLoader.release());
} else {
m_FailedPlugins.push_back(pluginName);
- qWarning("plugin \"%s\" failed to load (may be outdated)", qPrintable(pluginName));
+ qWarning("plugin \"%s\" failed to load (may be outdated)", qUtf8Printable(pluginName));
}
}
}
diff --git a/src/plugincontainer.h b/src/plugincontainer.h
index 172dbd09..21ad4241 100644
--- a/src/plugincontainer.h
+++ b/src/plugincontainer.h
@@ -32,6 +32,7 @@ class PluginContainer : public QObject, public MOBase::IPluginDiagnose
private:
typedef boost::fusion::map<
+ boost::fusion::pair<QObject, std::vector<QObject*>>,
boost::fusion::pair<MOBase::IPlugin, std::vector<MOBase::IPlugin*>>,
boost::fusion::pair<MOBase::IPluginDiagnose, std::vector<MOBase::IPluginDiagnose*>>,
boost::fusion::pair<MOBase::IPluginGame, std::vector<MOBase::IPluginGame*>>,
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp
index b0f59e1e..ec9bdac3 100644
--- a/src/pluginlist.cpp
+++ b/src/pluginlist.cpp
@@ -41,6 +41,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QTextCodec>
#include <QFileInfo>
#include <QListWidgetItem>
+#include <QRegularExpression>
#include <QString>
#include <QApplication>
#include <QKeyEvent>
@@ -126,11 +127,10 @@ void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MO
esp.m_ModSelected = false;
}
for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) {
- int modPriority = idx.data(Qt::UserRole).toInt();
- if (modPriority < 0 || modPriority == INT_MAX)
+ int modIndex = idx.data(Qt::UserRole + 1).toInt();
+ if (modIndex == UINT_MAX)
continue;
- int modIndex = profile.modIndexByPriority(modPriority);
ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex);
if (!selectedMod.isNull() && profile.modEnabled(modIndex)) {
QDir dir(selectedMod->absolutePath());
@@ -140,8 +140,8 @@ void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MO
for (auto plugin : plugins) {
MOShared::FileEntry::Ptr file = directoryEntry.findFile(plugin.toStdWString());
if (file->getOrigin() != origin.getID()) {
- const std::vector<std::pair<int, std::wstring>> alternatives = file->getAlternatives();
- if (std::find_if(alternatives.begin(), alternatives.end(), [&](const std::pair<int, std::wstring>& element) { return element.first == origin.getID(); }) == alternatives.end())
+ 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;
}
std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin.toLower());
@@ -152,7 +152,7 @@ void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MO
}
}
}
- emit dataChanged(this->index(0, 0), this->index(m_ESPs.size() - 1, this->columnCount() - 1));
+ emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
}
void PluginList::refresh(const QString &profileName
@@ -181,31 +181,38 @@ void PluginList::refresh(const QString &profileName
}
QString filename = ToQString(current->getName());
- availablePlugins.append(filename.toLower());
-
- if (m_ESPsByName.find(filename.toLower()) != m_ESPsByName.end()) {
- continue;
- }
-
QString extension = filename.right(3).toLower();
if ((extension == "esp") || (extension == "esm") || (extension == "esl")) {
+
+ availablePlugins.append(filename.toLower());
+
+ if (m_ESPsByName.find(filename.toLower()) != m_ESPsByName.end()) {
+ continue;
+ }
+
bool forceEnabled = Settings::instance().forceEnableCoreFiles() &&
- std::find(primaryPlugins.begin(), primaryPlugins.end(), filename.toLower()) != primaryPlugins.end();
+ primaryPlugins.contains(filename, Qt::CaseInsensitive);
+ //(std::find(primaryPlugins.begin(), primaryPlugins.end(), filename.toLower()) != primaryPlugins.end());
bool archive = false;
try {
FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive));
+ //name without extension
+ QString baseName = QFileInfo(filename).baseName();
- QString iniPath = QFileInfo(filename).baseName() + ".ini";
+ QString iniPath = baseName + ".ini";
bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr;
-
std::set<QString> loadedArchives;
- QString originPath = QString::fromWCharArray(origin.getPath().c_str());
- QDir dir(QDir::toNativeSeparators(originPath));
- for (QString filename : dir.entryList(QStringList() << QFileInfo(filename).baseName() + "*.bsa" << QFileInfo(filename).baseName() + "*.ba2")) {
- loadedArchives.insert(filename);
+ QString candidateName;
+ for (FileEntry::Ptr archiveCandidate : files) {
+ candidateName = ToQString(archiveCandidate->getName());
+ if (candidateName.startsWith(baseName, Qt::CaseInsensitive) &&
+ (candidateName.endsWith(".bsa", Qt::CaseInsensitive) ||
+ candidateName.endsWith(".ba2", Qt::CaseInsensitive))) {
+ loadedArchives.insert(candidateName);
+ }
}
QString originName = ToQString(origin.getName());
@@ -291,7 +298,7 @@ void PluginList::enableESP(const QString &name, bool enable)
emit writePluginsList();
} else {
- reportError(tr("esp not found: %1").arg(name));
+ reportError(tr("Plugin not found: %1").arg(qUtf8Printable(name)));
}
}
@@ -410,7 +417,7 @@ void PluginList::addInformation(const QString &name, const QString &message)
if (iter != m_ESPsByName.end()) {
m_AdditionalInfo[name.toLower()].m_Messages.append(message);
} else {
- qWarning("failed to associate message for \"%s\"", qPrintable(name));
+ qWarning("failed to associate message for \"%s\"", qUtf8Printable(name));
}
}
@@ -474,7 +481,7 @@ void PluginList::writeLockedOrder(const QString &fileName) const
file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8());
}
file.commit();
- qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData());
+ qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(fileName)));
}
@@ -499,7 +506,7 @@ void PluginList::saveTo(const QString &lockedOrderFileName
}
}
if (deleterFile.commitIfDifferent(m_LastSaveHash[deleterFileName])) {
- qDebug("%s saved", qPrintable(QDir::toNativeSeparators(deleterFileName)));
+ qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(deleterFileName)));
}
} else if (QFile::exists(deleterFileName)) {
shellDelete(QStringList() << deleterFileName);
@@ -687,7 +694,7 @@ void PluginList::setState(const QString &name, PluginStates state) {
m_ESPs[iter->second].m_Enabled = (state == IPluginList::STATE_ACTIVE) ||
m_ESPs[iter->second].m_ForceEnabled;
} else {
- qWarning("plugin %s not found", qPrintable(name));
+ qWarning("Plugin not found: %s", qUtf8Printable(name));
}
}
@@ -817,7 +824,7 @@ void PluginList::updateIndices()
continue;
}
if (m_ESPs[i].m_Priority >= static_cast<int>(m_ESPs.size())) {
- qCritical("invalid priority %d", m_ESPs[i].m_Priority);
+ qCritical("invalid plugin priority: %d", m_ESPs[i].m_Priority);
continue;
}
m_ESPsByName[m_ESPs[i].m_Name.toLower()] = i;
@@ -846,6 +853,7 @@ void PluginList::generatePluginIndexes()
m_ESPs[i].m_Index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
}
}
+ emit esplist_changed();
}
@@ -1360,7 +1368,7 @@ PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
m_Masters.insert(QString(iter->c_str()));
}
} catch (const std::exception &e) {
- qCritical("failed to parse plugin file %s: %s", qPrintable(fullPath), e.what());
+ qCritical("failed to parse plugin file %s: %s", qUtf8Printable(fullPath), e.what());
m_IsMaster = false;
m_IsLight = false;
m_IsLightFlagged = false;
diff --git a/src/pluginlistsortproxy.cpp b/src/pluginlistsortproxy.cpp
index e09e64a5..3d13798e 100644
--- a/src/pluginlistsortproxy.cpp
+++ b/src/pluginlistsortproxy.cpp
@@ -72,8 +72,7 @@ void PluginListSortProxy::updateFilter(const QString &filter)
bool PluginListSortProxy::filterAcceptsRow(int row, const QModelIndex&) const
{
- return m_CurrentFilter.isEmpty() ||
- sourceModel()->data(sourceModel()->index(row, 0)).toString().contains(m_CurrentFilter, Qt::CaseInsensitive);
+ return filterMatchesPlugin(sourceModel()->data(sourceModel()->index(row, 0)).toString());
}
@@ -137,3 +136,41 @@ bool PluginListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction act
sourceIndex.parent());
}
+
+bool PluginListSortProxy::filterMatchesPlugin(const QString &plugin) const
+{
+ if (!m_CurrentFilter.isEmpty()) {
+
+ bool display = false;
+ QString filterCopy = QString(m_CurrentFilter);
+ filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
+ QStringList ORList = filterCopy.split(";", QString::SkipEmptyParts);
+
+ bool segmentGood = true;
+
+ //split in ORSegments that internally use AND logic
+ for (auto& ORSegment : ORList) {
+ QStringList ANDKeywords = ORSegment.split(" ", QString::SkipEmptyParts);
+ 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 (!plugin.contains(currentKeyword, Qt::CaseInsensitive)) {
+ segmentGood = false;
+ break;
+ }
+ }
+
+ if (segmentGood) {
+ //the last AND loop didn't break so the ORSegments is true so mod matches filter
+ return true;
+ }
+
+ }//for ORList loop
+
+ return false;
+ }
+ else
+ return true;
+}
+
diff --git a/src/pluginlistsortproxy.h b/src/pluginlistsortproxy.h
index 89f60e70..03b079e2 100644
--- a/src/pluginlistsortproxy.h
+++ b/src/pluginlistsortproxy.h
@@ -46,6 +46,8 @@ public:
virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+ bool filterMatchesPlugin(const QString &plugin) const;
+
public slots:
void updateFilter(const QString &filter);
diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp
index 60a0b1df..795baab0 100644
--- a/src/problemsdialog.cpp
+++ b/src/problemsdialog.cpp
@@ -1,6 +1,7 @@
#include "problemsdialog.h"
#include "ui_problemsdialog.h"
#include <utility.h>
+#include <iplugin.h>
#include <iplugindiagnose.h>
#include <QPushButton>
#include <Shellapi.h>
@@ -9,8 +10,8 @@
using namespace MOBase;
-ProblemsDialog::ProblemsDialog(std::vector<MOBase::IPluginDiagnose *> diagnosePlugins, QWidget *parent)
- : QDialog(parent), ui(new Ui::ProblemsDialog), m_DiagnosePlugins(diagnosePlugins)
+ProblemsDialog::ProblemsDialog(std::vector<QObject *> pluginObjects, QWidget *parent)
+ : QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginObjects(pluginObjects)
{
ui->setupUi(this);
@@ -29,7 +30,15 @@ ProblemsDialog::~ProblemsDialog()
void ProblemsDialog::runDiagnosis()
{
ui->problemsWidget->clear();
- foreach (IPluginDiagnose *diagnose, m_DiagnosePlugins) {
+ for(QObject *pluginObj : m_PluginObjects) {
+ IPlugin *plugin = qobject_cast<IPlugin*>(pluginObj);
+ if (plugin != nullptr && !plugin->isActive())
+ continue;
+
+ IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(pluginObj);
+ if (diagnose == nullptr)
+ continue;
+
std::vector<unsigned int> activeProblems = diagnose->activeProblems();
foreach (unsigned int key, activeProblems) {
QTreeWidgetItem *newItem = new QTreeWidgetItem();
diff --git a/src/problemsdialog.h b/src/problemsdialog.h
index 24a69cdf..a48a5de1 100644
--- a/src/problemsdialog.h
+++ b/src/problemsdialog.h
@@ -15,9 +15,9 @@ class ProblemsDialog;
class ProblemsDialog : public QDialog
{
Q_OBJECT
-
+
public:
- explicit ProblemsDialog(std::vector<MOBase::IPluginDiagnose*> diagnosePlugins, QWidget *parent = 0);
+ explicit ProblemsDialog(std::vector<QObject*> pluginObjects, QWidget *parent = 0);
~ProblemsDialog();
bool hasProblems() const;
@@ -34,7 +34,7 @@ private slots:
private:
Ui::ProblemsDialog *ui;
- std::vector<MOBase::IPluginDiagnose *> m_DiagnosePlugins;
+ std::vector<QObject *> m_PluginObjects;
};
#endif // PROBLEMSDIALOG_H
diff --git a/src/problemsdialog.ui b/src/problemsdialog.ui
index 313a3c74..a1712d4a 100644
--- a/src/problemsdialog.ui
+++ b/src/problemsdialog.ui
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
- <string>Problems</string>
+ <string>Notifications</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
<item>
diff --git a/src/profile.cpp b/src/profile.cpp
index 8eb1c49a..4ccaa641 100644
--- a/src/profile.cpp
+++ b/src/profile.cpp
@@ -29,6 +29,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <safewritefile.h>
#include <bsainvalidation.h>
#include <dataarchives.h>
+#include "util.h"
+#include "registry.h"
+#include <questionboxmemory.h>
#include <QApplication>
#include <QFile> // for QFile
@@ -38,7 +41,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QScopedArrayPointer>
#include <QStringList> // for QStringList
#include <QtDebug> // for qDebug, qWarning, etc
-#include <QtGlobal> // for qPrintable
+#include <QtGlobal> // for qUtf8Printable
#include <QBuffer>
#include <QDirIterator>
@@ -75,7 +78,7 @@ Profile::Profile(const QString &name, IPluginGame const *gamePlugin, bool useDef
QDir profileBase(profilesDir);
QString fixedName = name;
if (!fixDirectoryName(fixedName)) {
- throw MyException(tr("invalid profile name %1").arg(name));
+ throw MyException(tr("invalid profile name: %1").arg(qUtf8Printable(name)));
}
if (!profileBase.exists() || !profileBase.mkdir(fixedName)) {
@@ -85,7 +88,6 @@ Profile::Profile(const QString &name, IPluginGame const *gamePlugin, bool useDef
m_Directory = QDir(fullPath);
m_Settings = new QSettings(m_Directory.absoluteFilePath("settings.ini"),
QSettings::IniFormat);
- findProfileSettings();
try {
// create files. Needs to happen after m_Directory was set!
@@ -101,6 +103,7 @@ Profile::Profile(const QString &name, IPluginGame const *gamePlugin, bool useDef
}
gamePlugin->initializeProfile(fullPath, settings);
+ findProfileSettings();
} catch (...) {
// clean up in case of an error
shellDelete(QStringList(profileBase.absoluteFilePath(fixedName)));
@@ -122,7 +125,7 @@ Profile::Profile(const QDir &directory, IPluginGame const *gamePlugin)
findProfileSettings();
if (!QFile::exists(m_Directory.filePath("modlist.txt"))) {
- qWarning("missing modlist.txt in %s", qPrintable(directory.path()));
+ qWarning("missing modlist.txt in %s", qUtf8Printable(directory.path()));
touchFile(m_Directory.filePath("modlist.txt"));
}
@@ -250,7 +253,7 @@ void Profile::doWriteModlist()
}
if (file.commitIfDifferent(m_LastModlistHash)) {
- qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData());
+ qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(fileName)));
}
} catch (const std::exception &e) {
reportError(tr("failed to write mod list: %1").arg(e.what()));
@@ -279,14 +282,14 @@ void Profile::createTweakedIniFile()
mergeTweak(getProfileTweaks(), tweakedIni);
bool error = false;
- if (!::WritePrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"1", ToWString(tweakedIni).c_str())) {
+ if (!MOBase::WriteRegistryValue(L"Archive", L"bInvalidateOlderFiles", L"1", ToWString(tweakedIni).c_str())) {
error = true;
}
if (error) {
- reportError(tr("failed to create tweaked ini: %1").arg(getCurrentErrorStringA().c_str()));
+ reportError(tr("failed to create tweaked ini: %1").arg(getCurrentErrorString().c_str()));
}
- qDebug("%s saved", qPrintable(QDir::toNativeSeparators(tweakedIni)));
+ qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(tweakedIni)));
}
// static
@@ -301,7 +304,7 @@ void Profile::renameModInAllProfiles(const QString& oldName, const QString& newN
if (modList.exists())
renameModInList(modList, oldName, newName);
else
- qWarning("Profile has no modlist.txt : %s", qPrintable(profileIter.filePath()));
+ qWarning("Profile has no modlist.txt : %s", qUtf8Printable(profileIter.filePath()));
}
}
@@ -346,7 +349,7 @@ void Profile::renameModInList(QFile &modList, const QString &oldName, const QStr
modName = newName;
++renamed;
}
- outBuffer.write(modName.toUtf8().constData());
+ outBuffer.write(qUtf8Printable(modName));
outBuffer.write("\r\n");
}
modList.close();
@@ -359,7 +362,7 @@ void Profile::renameModInList(QFile &modList, const QString &oldName, const QStr
if (renamed)
qDebug("Renamed %d \"%s\" mod to \"%s\" in %s",
- renamed, qPrintable(oldName), qPrintable(newName), qPrintable(modList.fileName()));
+ renamed, qUtf8Printable(oldName), qUtf8Printable(newName), qUtf8Printable(modList.fileName()));
}
void Profile::refreshModStatus()
@@ -413,19 +416,19 @@ void Profile::refreshModStatus()
m_ModStatus[modIndex].m_Enabled = enabled;
if (m_ModStatus[modIndex].m_Priority == -1) {
if (static_cast<size_t>(index) >= m_ModStatus.size()) {
- throw MyException(tr("invalid index %1").arg(index));
+ throw MyException(tr("invalid mod index: %1").arg(index));
}
m_ModStatus[modIndex].m_Priority = index++;
}
} else {
qWarning("no mod state for \"%s\" (profile \"%s\")",
- qPrintable(modName), m_Directory.path().toUtf8().constData());
+ qUtf8Printable(modName), qUtf8Printable(m_Directory.path()));
// need to rewrite the modlist to fix this
modStatusModified = true;
}
} else {
- qDebug("mod \"%s\" (profile \"%s\") not found",
- qPrintable(modName), m_Directory.path().toUtf8().constData());
+ qDebug("mod not found: \"%s\" (profile \"%s\")",
+ qUtf8Printable(modName), qUtf8Printable(m_Directory.path()));
// need to rewrite the modlist to fix this
modStatusModified = true;
}
@@ -452,7 +455,7 @@ void Profile::refreshModStatus()
m_ModStatus[i].m_Priority = numKnownMods - m_ModStatus[i].m_Priority - 1;
} else {
if (static_cast<size_t>(index) >= m_ModStatus.size()) {
- throw MyException(tr("invalid index %1").arg(index));
+ throw MyException(tr("invalid mod index: %1").arg(index));
}
if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) {
m_ModStatus[i].m_Priority = --topInsert;
@@ -518,20 +521,13 @@ std::vector<std::tuple<QString, QString, int> > Profile::getActiveMods()
for (std::map<int, unsigned int>::const_iterator iter = m_ModIndexByPriority.begin(); iter != m_ModIndexByPriority.end(); iter++ ) {
if ((iter->second != UINT_MAX) && m_ModStatus[iter->second].m_Enabled) {
ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second);
- result.push_back(std::make_tuple(modInfo->internalName(), modInfo->absolutePath(), m_ModStatus[iter->second].m_Priority));
+ if (modInfo->hasFlag(ModInfo::FLAG_OVERWRITE))
+ result.push_back(std::make_tuple(modInfo->internalName(), modInfo->absolutePath(), INT_MAX));
+ else
+ result.push_back(std::make_tuple(modInfo->internalName(), modInfo->absolutePath(), m_ModStatus[iter->second].m_Priority));
}
}
- unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool {
- std::vector<ModInfo::EFlag> flags = mod->getFlags();
- return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); });
-
- if (overwriteIndex != UINT_MAX) {
- ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex);
- result.push_back(std::make_tuple(overwriteInfo->name(), overwriteInfo->absolutePath(), UINT_MAX));
- } else {
- reportError(tr("Overwrite directory couldn't be parsed"));
- }
return result;
}
@@ -549,7 +545,7 @@ unsigned int Profile::modIndexByPriority(int priority) const
void Profile::setModEnabled(unsigned int index, bool enabled)
{
if (index >= m_ModStatus.size()) {
- throw MyException(tr("invalid index %1").arg(index));
+ throw MyException(tr("invalid mod index: %1").arg(index));
}
ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
@@ -565,10 +561,41 @@ void Profile::setModEnabled(unsigned int index, bool enabled)
}
}
+void Profile::setModsEnabled(const QList<unsigned int> &modsToEnable, const QList<unsigned int> &modsToDisable)
+{
+ QList<unsigned int> dirtyMods;
+ for (auto idx : modsToEnable) {
+ if (idx >= m_ModStatus.size()) {
+ qCritical() << tr("invalid mod index: %1").arg(idx);
+ continue;
+ }
+ if (!m_ModStatus[idx].m_Enabled) {
+ m_ModStatus[idx].m_Enabled = true;
+ dirtyMods.append(idx);
+ }
+ }
+ for (auto idx : modsToDisable) {
+ if (idx >= m_ModStatus.size()) {
+ qCritical() << tr("invalid mod index: %1").arg(idx);
+ continue;
+ }
+ if (ModInfo::getByIndex(idx)->alwaysEnabled()) {
+ continue;
+ }
+ if (m_ModStatus[idx].m_Enabled) {
+ m_ModStatus[idx].m_Enabled = false;
+ dirtyMods.append(idx);
+ }
+ }
+ if (!dirtyMods.isEmpty()) {
+ emit modStatusChanged(dirtyMods);
+ }
+}
+
bool Profile::modEnabled(unsigned int index) const
{
if (index >= m_ModStatus.size()) {
- throw MyException(tr("invalid index %1").arg(index));
+ throw MyException(tr("invalid mod index: %1").arg(index));
}
return m_ModStatus[index].m_Enabled;
@@ -578,7 +605,7 @@ bool Profile::modEnabled(unsigned int index) const
int Profile::getModPriority(unsigned int index) const
{
if (index >= m_ModStatus.size()) {
- throw MyException(tr("invalid index %1").arg(index));
+ throw MyException(tr("invalid mod index: %1").arg(index));
}
return m_ModStatus[index].m_Priority;
@@ -681,7 +708,7 @@ void Profile::mergeTweak(const QString &tweakName, const QString &tweakedIni) co
//TODO this treats everything as strings but how could I differentiate the type?
::GetPrivateProfileStringW(iter->c_str(), keyIter->c_str(),
nullptr, buffer.data(), bufferSize, ToWString(tweakName).c_str());
- ::WritePrivateProfileStringW(iter->c_str(), keyIter->c_str(),
+ MOBase::WriteRegistryValue(iter->c_str(), keyIter->c_str(),
buffer.data(), tweakedIniW.c_str());
}
}
@@ -747,12 +774,12 @@ bool Profile::enableLocalSaves(bool enable)
m_Directory.mkdir("saves");
}
} else {
- QMessageBox::StandardButton res = QMessageBox::question(
- QApplication::activeModalWidget(), tr("Delete profile-specific save games?"),
+ QDialogButtonBox::StandardButton res;
+ res = QuestionBoxMemory::query(QApplication::activeModalWidget(), "deleteSavesQuery",
+ tr("Delete profile-specific save games?"),
tr("Do you want to delete the profile-specific save games? (If you select \"No\", the "
"save games will show up again if you re-enable profile-specific save games)"),
- QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
- QMessageBox::Cancel);
+ QDialogButtonBox::No | QDialogButtonBox::Yes | QDialogButtonBox::Cancel, QDialogButtonBox::No);
if (res == QMessageBox::Yes) {
shellDelete(QStringList(m_Directory.absoluteFilePath("saves")));
} else if (res == QMessageBox::No) {
@@ -770,20 +797,21 @@ bool Profile::localSettingsEnabled() const
{
bool enabled = setting("LocalSettings", false).toBool();
if (enabled) {
- bool reinitConfig = false;
+ QStringList missingFiles;
for (QString file : m_GamePlugin->iniFiles()) {
if (!QFile::exists(m_Directory.filePath(file))) {
- qWarning("missing %s in %s", qPrintable(file), qPrintable(m_Directory.path()));
- reinitConfig = true;
+ qWarning("missing %s in %s", qUtf8Printable(file), qUtf8Printable(m_Directory.path()));
+ missingFiles << file;
}
}
- if (reinitConfig) {
+ if (!missingFiles.empty()) {
+ m_GamePlugin->initializeProfile(m_Directory, IPluginGame::CONFIGURATION);
QMessageBox::StandardButton res = QMessageBox::warning(
QApplication::activeModalWidget(), tr("Missing profile-specific game INI files!"),
tr("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.")
+ "from the vanilla game folder. You might want to double-check your settings.\n\n"
+ "Missing files:\n") + missingFiles.join("\n")
);
- m_GamePlugin->initializeProfile(m_Directory, IPluginGame::CONFIGURATION);
}
}
return enabled;
@@ -794,12 +822,12 @@ bool Profile::enableLocalSettings(bool enable)
if (enable) {
m_GamePlugin->initializeProfile(m_Directory.absolutePath(), IPluginGame::CONFIGURATION);
} else {
- QMessageBox::StandardButton res = QMessageBox::question(
- QApplication::activeModalWidget(), tr("Delete profile-specific game INI files?"),
+ QDialogButtonBox::StandardButton res;
+ res = QuestionBoxMemory::query(QApplication::activeModalWidget(), "deleteINIQuery",
+ tr("Delete profile-specific game INI files?"),
tr("Do you want to delete the profile-specific game INI files? (If you select \"No\", the "
"INI files will be used again if you re-enable profile-specific game INI files.)"),
- QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
- QMessageBox::Cancel);
+ QDialogButtonBox::No | QDialogButtonBox::Yes | QDialogButtonBox::Cancel, QDialogButtonBox::No);
if (res == QMessageBox::Yes) {
QStringList filesToDelete;
for (QString file : m_GamePlugin->iniFiles()) {
@@ -880,7 +908,6 @@ QVariant Profile::setting(const QString &section, const QString &name,
return m_Settings->value(section + "/" + name, fallback);
}
-
void Profile::storeSetting(const QString &section, const QString &name,
const QVariant &value)
{
@@ -902,7 +929,129 @@ void Profile::removeSetting(const QString &name)
removeSetting("", name);
}
+QVariantMap Profile::settingsByGroup(const QString &section) const
+{
+ QVariantMap results;
+ m_Settings->beginGroup(section);
+ for (auto key : m_Settings->childKeys()) {
+ results[key] = m_Settings->value(key);
+ }
+ m_Settings->endGroup();
+ return results;
+}
+
+void Profile::storeSettingsByGroup(const QString &section, const QVariantMap &values)
+{
+ m_Settings->beginGroup(section);
+ for (auto key : values.keys()) {
+ m_Settings->setValue(key, values[key]);
+ }
+ m_Settings->endGroup();
+}
+
+QList<QVariantMap> Profile::settingsByArray(const QString &prefix) const
+{
+ QList<QVariantMap> results;
+ int size = m_Settings->beginReadArray(prefix);
+ for (int i = 0; i < size; i++) {
+ m_Settings->setArrayIndex(i);
+ QVariantMap item;
+ for (auto key : m_Settings->childKeys()) {
+ item[key] = m_Settings->value(key);
+ }
+ results.append(item);
+ }
+ m_Settings->endArray();
+ return results;
+}
+
+void Profile::storeSettingsByArray(const QString &prefix, const QList<QVariantMap> &values)
+{
+ m_Settings->beginWriteArray(prefix);
+ for (int i = 0; i < values.length(); i++) {
+ m_Settings->setArrayIndex(i);
+ for (auto key : values.at(i).keys()) {
+ m_Settings->setValue(key, values.at(i)[key]);
+ }
+ }
+ m_Settings->endArray();
+}
+
int Profile::getPriorityMinimum() const
{
return m_ModIndexByPriority.begin()->first;
}
+
+bool Profile::forcedLibrariesEnabled(const QString &executable)
+{
+ return setting("forced_libraries", executable + "/enabled", false).toBool();
+}
+
+void Profile::setForcedLibrariesEnabled(const QString &executable, bool enabled)
+{
+ storeSetting("forced_libraries", executable + "/enabled", enabled);
+}
+
+QList<ExecutableForcedLoadSetting> Profile::determineForcedLibraries(const QString &executable)
+{
+ QList<ExecutableForcedLoadSetting> results;
+
+ auto rawSettings = settingsByArray("forced_libraries/" + executable);
+ auto forcedLoads = m_GamePlugin->executableForcedLoads();
+
+ // look for enabled status on forced loads and add those
+ for (auto forcedLoad : forcedLoads) {
+ bool found = false;
+ for (auto rawSetting : rawSettings) {
+ if ((rawSetting.value("process").toString().compare(forcedLoad.process(), Qt::CaseInsensitive) == 0)
+ && (rawSetting.value("library").toString().compare(forcedLoad.library(), Qt::CaseInsensitive) == 0))
+ {
+ results.append(forcedLoad.withEnabled(rawSetting.value("enabled", false).toBool()));
+ found = true;
+ }
+ }
+ if (!found) {
+ results.append(forcedLoad);
+ }
+ }
+
+ // add everything else
+ for (auto rawSetting : rawSettings) {
+ bool add = true;
+ for (auto forcedLoad : forcedLoads) {
+ if ((rawSetting.value("process").toString().compare(forcedLoad.process(), Qt::CaseInsensitive) == 0)
+ && (rawSetting.value("library").toString().compare(forcedLoad.library(), Qt::CaseInsensitive) == 0))
+ {
+ add = false;
+ }
+ }
+ if (add) {
+ results.append(
+ ExecutableForcedLoadSetting(
+ rawSetting.value("process").toString(),
+ rawSetting.value("library").toString()
+ ).withEnabled(rawSetting.value("enabled", false).toBool())
+ );
+ }
+ }
+
+ return results;
+}
+
+void Profile::storeForcedLibraries(const QString &executable, const QList<ExecutableForcedLoadSetting> &values)
+{
+ QList<QVariantMap> rawSettings;
+ for (auto setting : values) {
+ QVariantMap rawSetting;
+ rawSetting["enabled"] = setting.enabled();
+ rawSetting["process"] = setting.process();
+ rawSetting["library"] = setting.library();
+ rawSettings.append(rawSetting);
+ }
+ storeSettingsByArray("forced_libraries/" + executable, rawSettings);
+}
+
+void Profile::removeForcedLibraries(const QString &executable)
+{
+ m_Settings->remove("forced_libraries/" + executable);
+}
diff --git a/src/profile.h b/src/profile.h
index a9d68062..a7ba7e91 100644
--- a/src/profile.h
+++ b/src/profile.h
@@ -24,12 +24,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "modinfo.h"
#include <iprofile.h>
#include <delayedfilewriter.h>
+#include "executableinfo.h"
#include <QByteArray>
#include <QDir>
#include <QObject>
#include <QString>
#include <QSettings>
+#include <QList>
#include <boost/shared_ptr.hpp>
@@ -234,6 +236,13 @@ public:
std::vector<std::tuple<QString, QString, int> > getActiveMods();
/**
+ * @brief retrieve a mod of the indexes ordered by priority
+ *
+ * @return map of indexes by priority
+ **/
+ std::map<int, unsigned int> getAllIndexesByPriority() { return m_ModIndexByPriority; }
+
+ /**
* retrieve the number of mods for which this object has status information.
* This is usually the same as ModInfo::getNumMods() except between
* calls to ModInfo::updateFromDisc() and the Profile::refreshModStatus()
@@ -265,6 +274,16 @@ public:
void setModEnabled(unsigned int index, bool enabled);
/**
+ * @brief enable or disable multiple mods at once
+ * This is an abbreviated process and should be immediately followed by a full refresh
+ * to maintain data consistency.
+ *
+ * @param modsToEnable list of mod indicies to enable
+ * @param modsToDisable list of mod indicies to disable
+ **/
+ void setModsEnabled(const QList<unsigned int> &modsToEnable, const QList<unsigned int> &modsToDisable);
+
+ /**
* change the priority of a mod. Of course this also changes the priority of other mods.
* The priority of the mods in the range ]old, new priority] are shifted so that no gaps
* are possible.
@@ -303,8 +322,20 @@ public:
void removeSetting(const QString &section, const QString &name = QString());
void removeSetting(const QString &name);
+ QVariantMap settingsByGroup(const QString &section) const;
+ void storeSettingsByGroup(const QString &section, const QVariantMap &values);
+
+ QList<QVariantMap> settingsByArray(const QString &prefix) const;
+ void storeSettingsByArray(const QString &prefix, const QList<QVariantMap> &values);
+
int getPriorityMinimum() const;
+ bool forcedLibrariesEnabled(const QString &executable);
+ void setForcedLibrariesEnabled(const QString &executable, bool enabled);
+ QList<MOBase::ExecutableForcedLoadSetting> determineForcedLibraries(const QString &executable);
+ void storeForcedLibraries(const QString &executable, const QList<MOBase::ExecutableForcedLoadSetting> &values);
+ void removeForcedLibraries(const QString &executable);
+
signals:
/**
@@ -314,6 +345,13 @@ signals:
**/
void modStatusChanged(unsigned int index);
+ /**
+ * @brief emitted whenever the status (enabled/disabled) of multiple mods change
+ *
+ * @param index list of indices of the mods that changed
+ **/
+ void modStatusChanged(QList<unsigned int> index);
+
public slots:
// should only be called by DelayedFileWriter, use writeModlist() and writeModlistNow() instead
diff --git a/src/profilesdialog.cpp b/src/profilesdialog.cpp
index 17844357..f9ea655f 100644
--- a/src/profilesdialog.cpp
+++ b/src/profilesdialog.cpp
@@ -214,7 +214,7 @@ void ProfilesDialog::on_removeProfileButton_clicked()
delete item;
}
if (!shellDelete(QStringList(profilePath))) {
- qWarning("Failed to shell-delete \"%s\" (errorcode %lu), trying regular delete", qPrintable(profilePath), ::GetLastError());
+ qWarning("Failed to shell-delete \"%s\" (errorcode %lu), trying regular delete", qUtf8Printable(profilePath), ::GetLastError());
if (!removeDir(profilePath)) {
qWarning("regular delete failed too");
}
diff --git a/src/profilesdialog.ui b/src/profilesdialog.ui
index f040faed..644b0720 100644
--- a/src/profilesdialog.ui
+++ b/src/profilesdialog.ui
@@ -25,10 +25,8 @@
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;This is the list of profiles. Each Profile contains its own list and installation order of enabled mods (from a shared pool), a configuration of enabled esps/esms, a copy of the games ini-file and an optional savegame filter.&lt;/p&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Note&lt;/span&gt; For technical reasons it's currently not possible to have seperate load-orders for esps. This means you can't load moda.esp before modb.esp in one profile and the other way around in another.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8.25pt;&quot;&gt;This is the list of profiles. Each profile contains its own list and installation order of enabled mods (from a shared pool), its own list and load order of enabled plugins (esps/esms), a copy of the games ini-file and an optional savegame filter.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
diff --git a/src/resources.qrc b/src/resources.qrc
index 1dccce0b..8645b27e 100644
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -48,6 +48,9 @@
<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>
@@ -79,6 +82,7 @@
<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>
@@ -93,8 +97,8 @@
<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>
+ <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>
diff --git a/src/resources/conflict-mixed-blue.png b/src/resources/conflict-mixed-blue.png
new file mode 100644
index 00000000..2afbb304
--- /dev/null
+++ b/src/resources/conflict-mixed-blue.png
Binary files differ
diff --git a/src/resources/conflict-overwrite-blue.png b/src/resources/conflict-overwrite-blue.png
new file mode 100644
index 00000000..3247e7b7
--- /dev/null
+++ b/src/resources/conflict-overwrite-blue.png
Binary files differ
diff --git a/src/resources/conflict-overwritten-blue.png b/src/resources/conflict-overwritten-blue.png
new file mode 100644
index 00000000..3a032a55
--- /dev/null
+++ b/src/resources/conflict-overwritten-blue.png
Binary files differ
diff --git a/src/resources/red-archive-conflict-loser.png b/src/resources/red-archive-conflict-loser.png
new file mode 100644
index 00000000..18284ee2
--- /dev/null
+++ b/src/resources/red-archive-conflict-loser.png
Binary files differ
diff --git a/src/resources/tracked.png b/src/resources/tracked.png
new file mode 100644
index 00000000..6aab3023
--- /dev/null
+++ b/src/resources/tracked.png
Binary files differ
diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp
index 7c0682bf..4c0f9a8d 100644
--- a/src/selfupdater.cpp
+++ b/src/selfupdater.cpp
@@ -126,37 +126,42 @@ void SelfUpdater::testForUpdate()
// TODO: if prereleases are disabled we could just request the latest release
// directly
try {
- m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
- [this](const QJsonArray &releases) {
- QJsonObject newest;
- for (const QJsonValue &releaseVal : releases) {
- QJsonObject release = releaseVal.toObject();
- if (!release["draft"].toBool() && (Settings::instance().usePrereleases()
- || !release["prerelease"].toBool())) {
- if (newest.empty() || (VersionInfo(release["tag_name"].toString())
- > VersionInfo(newest["tag_name"].toString()))) {
- newest = release;
+ m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
+ [this](const QJsonArray &releases) {
+ if (releases.isEmpty()) {
+ qDebug("Unable to connect to github.com to check version");
+ return;
+ }
+
+ QJsonObject newest;
+ for (const QJsonValue &releaseVal : releases) {
+ QJsonObject release = releaseVal.toObject();
+ if (!release["draft"].toBool() && (Settings::instance().usePrereleases()
+ || !release["prerelease"].toBool())) {
+ if (newest.empty() || (VersionInfo(release["tag_name"].toString())
+ > VersionInfo(newest["tag_name"].toString()))) {
+ newest = release;
+ }
}
}
- }
- if (!newest.empty()) {
- VersionInfo newestVer(newest["tag_name"].toString());
- if (newestVer > this->m_MOVersion) {
- m_UpdateCandidate = newest;
- qDebug("update available: %s -> %s",
- qPrintable(this->m_MOVersion.displayString()),
- qPrintable(newestVer.displayString()));
- emit updateAvailable();
- } else if (newestVer < this->m_MOVersion) {
- // this could happen if the user switches from using prereleases to
- // stable builds. Should we downgrade?
- qDebug("this version is newer than the newest installed one: %s -> %s",
- qPrintable(this->m_MOVersion.displayString()),
- qPrintable(newestVer.displayString()));
+ if (!newest.empty()) {
+ VersionInfo newestVer(newest["tag_name"].toString());
+ if (newestVer > this->m_MOVersion) {
+ m_UpdateCandidate = newest;
+ qDebug("update available: %s -> %s",
+ qUtf8Printable(this->m_MOVersion.displayString(3)),
+ qUtf8Printable(newestVer.displayString(3)));
+ emit updateAvailable();
+ } else if (newestVer < this->m_MOVersion) {
+ // this could happen if the user switches from using prereleases to
+ // stable builds. Should we downgrade?
+ qDebug("This version is newer than the latest released one: %s -> %s",
+ qUtf8Printable(this->m_MOVersion.displayString(3)),
+ qUtf8Printable(newestVer.displayString(3)));
+ }
}
- }
- });
+ });
}
//Catch all is bad by design, should be improved
catch (...) {
@@ -224,7 +229,7 @@ void SelfUpdater::closeProgress()
void SelfUpdater::openOutputFile(const QString &fileName)
{
QString outputPath = QDir::fromNativeSeparators(qApp->property("dataPath").toString()) + "/" + fileName;
- qDebug("downloading to %s", qPrintable(outputPath));
+ qDebug("downloading to %s", qUtf8Printable(outputPath));
m_UpdateFile.setFileName(outputPath);
m_UpdateFile.open(QIODevice::WriteOnly);
}
@@ -327,10 +332,10 @@ void SelfUpdater::installUpdate()
const QString mopath
= QDir::fromNativeSeparators(qApp->property("dataPath").toString());
- const std::wstring params = L"/DIR=" + qApp->applicationDirPath().toStdWString();
+ std::wstring parameters = ToWString("/DIR=\"" + qApp->applicationDirPath() + "\" ");
HINSTANCE res = ::ShellExecuteW(
- nullptr, L"open", m_UpdateFile.fileName().toStdWString().c_str(), params.c_str(),
+ nullptr, L"open", m_UpdateFile.fileName().toStdWString().c_str(), parameters.c_str(),
nullptr, SW_SHOW);
if (res > (HINSTANCE)32) {
@@ -338,9 +343,8 @@ void SelfUpdater::installUpdate()
} else {
reportError(tr("Failed to start %1: %2")
.arg(m_UpdateFile.fileName())
- .arg((int)res));
+ .arg((INT_PTR)res));
}
-
m_UpdateFile.remove();
}
diff --git a/src/settings.cpp b/src/settings.cpp
index 18e893cb..231487bb 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -57,6 +57,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QtDebug> // for qDebug, qWarning
#include <Windows.h> // For ShellExecuteW, HINSTANCE, etc
+#include <wincred.h> // For storage
#include <algorithm> // for sort
#include <memory>
@@ -80,10 +81,6 @@ private:
int m_SortRole;
};
-
-static const unsigned char Key2[20] = { 0x99, 0xb8, 0x76, 0x42, 0x3e, 0xc1, 0x60, 0xa4, 0x5b, 0x01,
- 0xdb, 0xf8, 0x43, 0x3a, 0xb7, 0xb6, 0x98, 0xd4, 0x7d, 0xa2 };
-
Settings *Settings::s_Instance = nullptr;
@@ -142,7 +139,7 @@ void Settings::registerAsNXMHandler(bool force)
}
parameters += L" \"" + executable + L"\"";
HINSTANCE res = ::ShellExecuteW(nullptr, L"open", nxmPath.c_str(), parameters.c_str(), nullptr, SW_SHOWNORMAL);
- if ((int)res <= 32) {
+ if ((INT_PTR)res <= 32) {
QMessageBox::critical(nullptr, tr("Failed"),
tr("Sorry, failed to start the helper application"));
}
@@ -161,13 +158,13 @@ void Settings::managedGameChanged(IPluginGame const *gamePlugin)
void Settings::registerPlugin(IPlugin *plugin)
{
m_Plugins.push_back(plugin);
- m_PluginSettings.insert(plugin->name(), QMap<QString, QVariant>());
- m_PluginDescriptions.insert(plugin->name(), QMap<QString, QVariant>());
+ m_PluginSettings.insert(plugin->name(), QVariantMap());
+ m_PluginDescriptions.insert(plugin->name(), QVariantMap());
for (const PluginSetting &setting : plugin->settings()) {
QVariant temp = m_Settings.value("Plugins/" + plugin->name() + "/" + setting.key, setting.defaultValue);
if (!temp.convert(setting.defaultValue.type())) {
qWarning("failed to interpret \"%s\" as correct type for \"%s\" in plugin \"%s\", using default",
- qPrintable(temp.toString()), qPrintable(setting.key), qPrintable(plugin->name()));
+ qUtf8Printable(temp.toString()), qUtf8Printable(setting.key), qUtf8Printable(plugin->name()));
temp = setting.defaultValue;
}
m_PluginSettings[plugin->name()][setting.key] = temp;
@@ -175,31 +172,68 @@ void Settings::registerPlugin(IPlugin *plugin)
}
}
-QString Settings::obfuscate(const QString &password)
+bool Settings::obfuscate(const QString key, const QString data)
{
- QByteArray temp = password.toUtf8();
+ QString finalKey("ModOrganizer2_" + key);
+ wchar_t* keyData = new wchar_t[finalKey.size()+1];
+ finalKey.toWCharArray(keyData);
+ keyData[finalKey.size()] = L'\0';
+ bool result = false;
+ if (data.isEmpty()) {
+ result = CredDeleteW(keyData, CRED_TYPE_GENERIC, 0);
+ if (!result)
+ if (GetLastError() == ERROR_NOT_FOUND)
+ result = true;
+ } else {
+ wchar_t* charData = new wchar_t[data.size()];
+ data.toWCharArray(charData);
- QByteArray buffer;
- for (int i = 0; i < temp.length(); ++i) {
- buffer.append(temp.at(i) ^ Key2[i % 20]);
+ CREDENTIALW cred = {};
+ cred.Flags = 0;
+ cred.Type = CRED_TYPE_GENERIC;
+ cred.TargetName = keyData;
+ cred.CredentialBlob = (LPBYTE)charData;
+ cred.CredentialBlobSize = sizeof(wchar_t) * data.size();
+ cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
+
+ result = CredWriteW(&cred, 0);
+ delete[] charData;
}
- return buffer.toBase64();
+ delete[] keyData;
+ return result;
}
-QString Settings::deObfuscate(const QString &password)
+QString Settings::deObfuscate(const QString key)
{
- QByteArray temp(QByteArray::fromBase64(password.toUtf8()));
-
- QByteArray buffer;
- for (int i = 0; i < temp.length(); ++i) {
- buffer.append(temp.at(i) ^ Key2[i % 20]);
+ QString result;
+ QString finalKey("ModOrganizer2_" + key);
+ wchar_t* keyData = new wchar_t[finalKey.size()+1];
+ finalKey.toWCharArray(keyData);
+ keyData[finalKey.size()] = L'\0';
+ PCREDENTIALW creds;
+ if (CredReadW(keyData, 1, 0, &creds)) {
+ wchar_t *charData = (wchar_t *)creds->CredentialBlob;
+ 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;
+ }
}
- return QString::fromUtf8(buffer.constData());
+ delete[] keyData;
+ return result;
}
-QColor Settings::getIdealTextColor(const QColor& rBackgroundColor)
+QColor Settings::getIdealTextColor(const QColor& rBackgroundColor)
{
- const int THRESHOLD = 106;
+ if (rBackgroundColor.alpha() == 0)
+ return QColor(Qt::black);
+
+ const int THRESHOLD = 106 * 255.0f / rBackgroundColor.alpha();
int BackgroundDelta = (rBackgroundColor.red() * 0.299) + (rBackgroundColor.green() * 0.587) + (rBackgroundColor.blue() * 0.114);
return QColor((255 - BackgroundDelta <= THRESHOLD) ? Qt::black : Qt::white);
}
@@ -319,37 +353,26 @@ QString Settings::getOverwriteDirectory(bool resolve) const
ToQString(AppConfig::overwritePath()), resolve);
}
-QString Settings::getNMMVersion() const
-{
- static const QString MIN_NMM_VERSION = "0.65.2";
- QString result = m_Settings.value("Settings/nmm_version", MIN_NMM_VERSION).toString();
- if (VersionInfo(result) < VersionInfo(MIN_NMM_VERSION)) {
- result = MIN_NMM_VERSION;
- }
- return result;
-}
-
-bool Settings::getNexusLogin(QString &username, QString &password) const
+bool Settings::getNexusApiKey(QString &apiKey) const
{
- if (m_Settings.value("Settings/nexus_login", false).toBool()) {
- username = m_Settings.value("Settings/nexus_username", "").toString();
- password = deObfuscate(m_Settings.value("Settings/nexus_password", "").toString());
- return true;
- } else {
+ QString tempKey = deObfuscate("APIKEY");
+ if (tempKey.isEmpty())
return false;
- }
+ apiKey = tempKey;
+ return true;
}
bool Settings::getSteamLogin(QString &username, QString &password) const
{
- if (m_Settings.contains("Settings/steam_username")
- && m_Settings.contains("Settings/steam_password")) {
- username = m_Settings.value("Settings/steam_username").toString();
- password = deObfuscate(m_Settings.value("Settings/steam_password").toString());
- return true;
- } else {
- return false;
+ if (m_Settings.contains("Settings/steam_username")) {
+ QString tempPass = deObfuscate("steam_password");
+ if (!tempPass.isEmpty()) {
+ username = m_Settings.value("Settings/steam_username").toString();
+ password = tempPass;
+ return true;
+ }
}
+ return false;
}
bool Settings::compactDownloads() const
{
@@ -391,6 +414,16 @@ QColor Settings::modlistOverwritingLooseColor() const
return m_Settings.value("Settings/overwritingLooseFilesColor", QColor(255, 0, 0, 64)).value<QColor>();
}
+QColor Settings::modlistOverwrittenArchiveColor() const
+{
+ return m_Settings.value("Settings/overwrittenArchiveFilesColor", QColor(0, 255, 255, 64)).value<QColor>();
+}
+
+QColor Settings::modlistOverwritingArchiveColor() const
+{
+ return m_Settings.value("Settings/overwritingArchiveFilesColor", QColor(255, 0, 255, 64)).value<QColor>();
+}
+
QColor Settings::modlistContainsPluginColor() const
{
return m_Settings.value("Settings/containsPluginColor", QColor(0, 0, 255, 64)).value<QColor>();
@@ -411,15 +444,22 @@ QString Settings::executablesBlacklist() const
<< "TGitCache.exe"
<< "Steam.exe"
<< "GameOverlayUI.exe"
+ << "Discord.exe"
+ << "GalaxyClient.exe"
+ << "Spotify.exe"
).join(";")
).toString();
}
-void Settings::setNexusLogin(QString username, QString password)
+void Settings::setNexusApiKey(QString apiKey)
{
- m_Settings.setValue("Settings/nexus_login", true);
- m_Settings.setValue("Settings/nexus_username", username);
- m_Settings.setValue("Settings/nexus_password", obfuscate(password));
+ 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)
@@ -430,10 +470,12 @@ void Settings::setSteamLogin(QString username, QString password)
} else {
m_Settings.setValue("Settings/steam_username", username);
}
- if (password == "") {
- m_Settings.remove("Settings/steam_password");
- } else {
- m_Settings.setValue("Settings/steam_password", obfuscate(password));
+ 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;
}
}
@@ -459,6 +501,16 @@ bool Settings::useProxy() const
return m_Settings.value("Settings/use_proxy", false).toBool();
}
+bool Settings::endorsementIntegration() const
+{
+ return m_Settings.value("Settings/endorsement_integration", true).toBool();
+}
+
+bool Settings::hideAPICounter() const
+{
+ return m_Settings.value("Settings/hide_api_counter", false).toBool();
+}
+
bool Settings::displayForeign() const
{
return m_Settings.value("Settings/display_foreign", true).toBool();
@@ -474,6 +526,11 @@ uint Settings::getMotDHash() const
return m_Settings.value("motd_hash", 0).toUInt();
}
+bool Settings::archiveParsing() const
+{
+ return m_Settings.value("Settings/archive_parsing_experimental", false).toBool();
+}
+
QVariant Settings::pluginSetting(const QString &pluginName, const QString &key) const
{
auto iterPlugin = m_PluginSettings.find(pluginName);
@@ -566,7 +623,7 @@ void Settings::updateServers(const QList<ServerInfo> &servers)
QVariantMap val = m_Settings.value(key).toMap();
QDate lastSeen = val["lastSeen"].toDate();
if (lastSeen.daysTo(now) > 30) {
- qDebug("removing server %s since it hasn't been available for downloads in over a month", qPrintable(key));
+ qDebug("removing server %s since it hasn't been available for downloads in over a month", qUtf8Printable(key));
m_Settings.remove(key);
}
}
@@ -584,6 +641,7 @@ void Settings::addBlacklistPlugin(const QString &fileName)
void Settings::writePluginBlacklist()
{
+ m_Settings.remove("pluginBlacklist");
m_Settings.beginWriteArray("pluginBlacklist");
int idx = 0;
for (const QString &plugin : m_PluginBlacklist) {
@@ -647,11 +705,43 @@ 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);
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;
@@ -698,6 +788,25 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent)
}
m_Settings.setValue(key, dialog.saveGeometry());
+ // These changes happen regardless of accepted or rejected
+ bool restartNeeded = false;
+ if (dialog.getApiKeyChanged()) {
+ restartNeeded = true;
+ }
+ if (dialog.getResetGeometries()) {
+ restartNeeded = true;
+ m_Settings.setValue("reset_geometry", true);
+ }
+ if (restartNeeded) {
+ if (QMessageBox::question(nullptr,
+ tr("Restart Mod Organizer?"),
+ tr("In order to finish configuration changes, MO must be restarted.\n"
+ "Restart it now?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ qApp->exit(INT_MAX);
+ }
+ }
+
}
Settings::SettingsTab::SettingsTab(Settings *m_parent, SettingsDialog &m_dialog)
@@ -719,6 +828,8 @@ Settings::GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog)
, m_usePrereleaseBox(m_dialog.findChild<QCheckBox *>("usePrereleaseBox"))
, m_overwritingBtn(m_dialog.findChild<QPushButton *>("overwritingBtn"))
, m_overwrittenBtn(m_dialog.findChild<QPushButton *>("overwrittenBtn"))
+ , m_overwritingArchiveBtn(m_dialog.findChild<QPushButton *>("overwritingArchiveBtn"))
+ , m_overwrittenArchiveBtn(m_dialog.findChild<QPushButton *>("overwrittenArchiveBtn"))
, m_containsBtn(m_dialog.findChild<QPushButton *>("containsBtn"))
, m_containedBtn(m_dialog.findChild<QPushButton *>("containedBtn"))
, m_colorSeparatorsBox(m_dialog.findChild<QCheckBox *>("colorSeparatorsBox"))
@@ -773,21 +884,17 @@ Settings::GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog)
*/
//version with stylesheet
- m_overwritingBtn->setStyleSheet(m_dialog.getColoredButtonStyleSheet().arg(
- m_parent->modlistOverwritingLooseColor().name()).arg(getIdealTextColor(
- m_parent->modlistOverwritingLooseColor()).name()));
- m_overwrittenBtn->setStyleSheet(m_dialog.getColoredButtonStyleSheet().arg(
- m_parent->modlistOverwrittenLooseColor().name()).arg(getIdealTextColor(
- m_parent->modlistOverwrittenLooseColor()).name()));
- m_containsBtn->setStyleSheet(m_dialog.getColoredButtonStyleSheet().arg(
- m_parent->modlistContainsPluginColor().name()).arg(getIdealTextColor(
- m_parent->modlistContainsPluginColor()).name()));
- m_containedBtn->setStyleSheet(m_dialog.getColoredButtonStyleSheet().arg(
- m_parent->pluginListContainedColor().name()).arg(getIdealTextColor(
- m_parent->pluginListContainedColor()).name()));
+ m_dialog.setButtonColor(m_overwritingBtn, m_parent->modlistOverwritingLooseColor());
+ m_dialog.setButtonColor(m_overwrittenBtn, m_parent->modlistOverwrittenLooseColor());
+ m_dialog.setButtonColor(m_overwritingArchiveBtn, m_parent->modlistOverwritingArchiveColor());
+ m_dialog.setButtonColor(m_overwrittenArchiveBtn, m_parent->modlistOverwrittenArchiveColor());
+ m_dialog.setButtonColor(m_containsBtn, m_parent->modlistContainsPluginColor());
+ m_dialog.setButtonColor(m_containedBtn, m_parent->pluginListContainedColor());
m_dialog.setOverwritingColor(m_parent->modlistOverwritingLooseColor());
m_dialog.setOverwrittenColor(m_parent->modlistOverwrittenLooseColor());
+ m_dialog.setOverwritingArchiveColor(m_parent->modlistOverwritingArchiveColor());
+ m_dialog.setOverwrittenArchiveColor(m_parent->modlistOverwrittenArchiveColor());
m_dialog.setContainsColor(m_parent->modlistContainsPluginColor());
m_dialog.setContainedColor(m_parent->pluginListContainedColor());
@@ -815,6 +922,8 @@ void Settings::GeneralTab::update()
m_Settings.setValue("Settings/overwritingLooseFilesColor", m_dialog.getOverwritingColor());
m_Settings.setValue("Settings/overwrittenLooseFilesColor", m_dialog.getOverwrittenColor());
+ m_Settings.setValue("Settings/overwritingArchiveFilesColor", m_dialog.getOverwritingArchiveColor());
+ m_Settings.setValue("Settings/overwrittenArchiveFilesColor", m_dialog.getOverwrittenArchiveColor());
m_Settings.setValue("Settings/containsPluginColor", m_dialog.getContainsColor());
m_Settings.setValue("Settings/containedColor", m_dialog.getContainedColor());
m_Settings.setValue("Settings/compact_downloads", m_compactBox->isChecked());
@@ -933,23 +1042,24 @@ void Settings::DiagnosticsTab::update()
Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog)
: Settings::SettingsTab(parent, dialog)
- , m_loginCheckBox(dialog.findChild<QCheckBox *>("loginCheckBox"))
- , m_usernameEdit(dialog.findChild<QLineEdit *>("usernameEdit"))
- , m_passwordEdit(dialog.findChild<QLineEdit *>("passwordEdit"))
+ , m_nexusConnect(dialog.findChild<QPushButton *>("nexusConnect"))
, m_offlineBox(dialog.findChild<QCheckBox *>("offlineBox"))
, m_proxyBox(dialog.findChild<QCheckBox *>("proxyBox"))
, m_knownServersList(dialog.findChild<QListWidget *>("knownServersList"))
, m_preferredServersList(
dialog.findChild<QListWidget *>("preferredServersList"))
+ , m_endorsementBox(dialog.findChild<QCheckBox *>("endorsementBox"))
+ , m_hideAPICounterBox(dialog.findChild<QCheckBox *>("hideAPICounterBox"))
{
- if (parent->automaticLoginEnabled()) {
- m_loginCheckBox->setChecked(true);
- m_usernameEdit->setText(m_Settings.value("Settings/nexus_username", "").toString());
- m_passwordEdit->setText(deObfuscate(m_Settings.value("Settings/nexus_password", "").toString()));
+ 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());
+ m_hideAPICounterBox->setChecked(parent->hideAPICounter());
// display server preferences
m_Settings.beginGroup("Servers");
@@ -980,6 +1090,7 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog)
void Settings::NexusTab::update()
{
+ /*
if (m_loginCheckBox->isChecked()) {
m_Settings.setValue("Settings/nexus_login", true);
m_Settings.setValue("Settings/nexus_username", m_usernameEdit->text());
@@ -989,8 +1100,11 @@ void Settings::NexusTab::update()
m_Settings.remove("Settings/nexus_username");
m_Settings.remove("Settings/nexus_password");
}
+ */
m_Settings.setValue("Settings/offline_mode", m_offlineBox->isChecked());
m_Settings.setValue("Settings/use_proxy", m_proxyBox->isChecked());
+ m_Settings.setValue("Settings/endorsement_integration", m_endorsementBox->isChecked());
+ m_Settings.setValue("Settings/hide_api_counter", m_hideAPICounterBox->isChecked());
// store server preference
m_Settings.beginGroup("Servers");
@@ -1017,8 +1131,9 @@ Settings::SteamTab::SteamTab(Settings *m_parent, SettingsDialog &m_dialog)
{
if (m_Settings.contains("Settings/steam_username")) {
m_steamUserEdit->setText(m_Settings.value("Settings/steam_username", "").toString());
- if (m_Settings.contains("Settings/steam_password")) {
- m_steamPassEdit->setText(deObfuscate(m_Settings.value("Settings/steam_password", "").toString()));
+ QString password = deObfuscate("steam_password");
+ if (!password.isEmpty()) {
+ m_steamPassEdit->setText(password);
}
}
}
@@ -1080,11 +1195,12 @@ Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent,
: Settings::SettingsTab(m_parent, m_dialog)
, m_appIDEdit(m_dialog.findChild<QLineEdit *>("appIDEdit"))
, m_mechanismBox(m_dialog.findChild<QComboBox *>("mechanismBox"))
- , m_nmmVersionEdit(m_dialog.findChild<QLineEdit *>("nmmVersionEdit"))
, m_hideUncheckedBox(m_dialog.findChild<QCheckBox *>("hideUncheckedBox"))
, m_forceEnableBox(m_dialog.findChild<QCheckBox *>("forceEnableBox"))
, m_displayForeignBox(m_dialog.findChild<QCheckBox *>("displayForeignBox"))
, m_lockGUIBox(m_dialog.findChild<QCheckBox *>("lockGUIBox"))
+ , m_enableArchiveParsingBox(m_dialog.findChild<QCheckBox *>("enableArchiveParsingBox"))
+ , m_resetGeometriesBtn(m_dialog.findChild<QPushButton *>("resetGeometryBtn"))
{
m_appIDEdit->setText(m_parent->getSteamAppID());
@@ -1114,11 +1230,13 @@ Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent,
m_mechanismBox->setCurrentIndex(index);
- m_nmmVersionEdit->setText(m_parent->getNMMVersion());
m_hideUncheckedBox->setChecked(m_parent->hideUncheckedPlugins());
m_forceEnableBox->setChecked(m_parent->forceEnableCoreFiles());
m_displayForeignBox->setChecked(m_parent->displayForeign());
m_lockGUIBox->setChecked(m_parent->lockGUI());
+ m_enableArchiveParsingBox->setChecked(m_parent->archiveParsing());
+
+ m_resetGeometriesBtn->setChecked(m_parent->directInterface().value("reset_geometry", false).toBool());
m_dialog.setExecutableBlacklist(m_parent->executablesBlacklist());
@@ -1132,22 +1250,11 @@ void Settings::WorkaroundsTab::update()
m_Settings.remove("Settings/app_id");
}
m_Settings.setValue("Settings/load_mechanism", m_mechanismBox->itemData(m_mechanismBox->currentIndex()).toInt());
- m_Settings.setValue("Settings/nmm_version", m_nmmVersionEdit->text());
m_Settings.setValue("Settings/hide_unchecked_plugins", m_hideUncheckedBox->isChecked());
m_Settings.setValue("Settings/force_enable_core_files", m_forceEnableBox->isChecked());
m_Settings.setValue("Settings/display_foreign", m_displayForeignBox->isChecked());
m_Settings.setValue("Settings/lock_gui", m_lockGUIBox->isChecked());
+ m_Settings.setValue("Settings/archive_parsing_experimental", m_enableArchiveParsingBox->isChecked());
m_Settings.setValue("Settings/executable_blacklist", m_dialog.getExecutableBlacklist());
-
- if (m_dialog.getResetGeometries()) {
- m_Settings.setValue("reset_geometry", true);
- if (QMessageBox::question(nullptr,
- tr("Restart Mod Organizer?"),
- tr("In order to reset the window geometries, MO must be restarted.\n"
- "Restart it now?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- qApp->exit(INT_MAX);
- }
- }
}
diff --git a/src/settings.h b/src/settings.h
index 912864e2..ed49a1bc 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -25,11 +25,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QList>
#include <QMap>
#include <QObject>
+#include <QPushButton>
#include <QSet>
#include <QSettings>
#include <QString>
#include <QVariant>
#include <QColor>
+#include <QMetaType>
#include <QtGlobal> //for uint
@@ -151,11 +153,6 @@ public:
QString getModDirectory(bool resolve = true) const;
/**
- * returns the version of nmm to impersonate when connecting to nexus
- **/
- QString getNMMVersion() const;
-
- /**
* retrieve the directory where the web cache is stored (with native separators)
**/
QString getCacheDirectory(bool resolve = true) const;
@@ -188,7 +185,7 @@ public:
* @param password (out) received the password for nexus
* @return true if automatic login is active, false otherwise
**/
- bool getNexusLogin(QString &username, QString &password) const;
+ bool getNexusApiKey(QString &apiKey) const;
/**
* @brief retrieve the login information for steam
@@ -233,6 +230,10 @@ public:
QColor modlistOverwritingLooseColor() const;
+ QColor modlistOverwrittenArchiveColor() const;
+
+ QColor modlistOverwritingArchiveColor() const;
+
QColor modlistContainsPluginColor() const;
QColor pluginListContainedColor() const;
@@ -245,7 +246,7 @@ public:
* @param username username
* @param password password
*/
- void setNexusLogin(QString username, QString password);
+ void setNexusApiKey(QString apiKey);
/**
* @brief set the steam login information
@@ -271,6 +272,16 @@ public:
bool useProxy() const;
/**
+ * @return true if endorsement integration is enabled
+ */
+ bool endorsementIntegration() const;
+
+ /**
+ * @return true if the API counter should be hidden
+ */
+ bool hideAPICounter() const;
+
+ /**
* @return true if the user wants to see non-official plugins installed outside MO in his mod list
*/
bool displayForeign() const;
@@ -281,6 +292,11 @@ public:
void setMotDHash(uint hash);
/**
+ * @return true if the user wants to have archives being parsed to show conflicts and contents
+ */
+ bool archiveParsing() const;
+
+ /**
* @return hash of the last displayed message of the day
**/
uint getMotDHash() const;
@@ -378,8 +394,8 @@ public:
static QColor getIdealTextColor(const QColor& rBackgroundColor);
private:
- static QString obfuscate(const QString &password);
- static QString deObfuscate(const QString &password);
+ static bool obfuscate(const QString key, const QString data);
+ static QString deObfuscate(const QString key);
void addLanguages(QComboBox *languageBox);
void addStyles(QComboBox *styleBox);
@@ -418,6 +434,8 @@ private:
QCheckBox *m_usePrereleaseBox;
QPushButton *m_overwritingBtn;
QPushButton *m_overwrittenBtn;
+ QPushButton *m_overwritingArchiveBtn;
+ QPushButton *m_overwrittenArchiveBtn;
QPushButton *m_containsBtn;
QPushButton *m_containedBtn;
QCheckBox *m_colorSeparatorsBox;
@@ -459,17 +477,16 @@ private:
{
public:
NexusTab(Settings *m_parent, SettingsDialog &m_dialog);
-
void update();
private:
- QCheckBox *m_loginCheckBox;
- QLineEdit *m_usernameEdit;
- QLineEdit *m_passwordEdit;
+ QPushButton *m_nexusConnect;
QCheckBox *m_offlineBox;
QCheckBox *m_proxyBox;
QListWidget *m_knownServersList;
QListWidget *m_preferredServersList;
+ QCheckBox *m_endorsementBox;
+ QCheckBox *m_hideAPICounterBox;
};
/** Display/store the configuration in the 'steam' tab of the settings dialogue */
@@ -509,16 +526,20 @@ private:
private:
QLineEdit *m_appIDEdit;
QComboBox *m_mechanismBox;
- QLineEdit *m_nmmVersionEdit;
QCheckBox *m_hideUncheckedBox;
QCheckBox *m_forceEnableBox;
QCheckBox *m_displayForeignBox;
QCheckBox *m_lockGUIBox;
+ QCheckBox *m_enableArchiveParsingBox;
+ QPushButton *m_resetGeometriesBtn;
};
private slots:
void resetDialogs();
+ void processApiKey(const QString &);
+ void clearApiKey(QPushButton *nexusButton);
+ void checkApiKey(QPushButton *nexusButton);
signals:
@@ -537,8 +558,8 @@ private:
std::vector<MOBase::IPlugin*> m_Plugins;
- QMap<QString, QMap<QString, QVariant>> m_PluginSettings;
- QMap<QString, QMap<QString, QVariant>> m_PluginDescriptions;
+ QMap<QString, QVariantMap> m_PluginSettings;
+ QMap<QString, QVariantMap> m_PluginDescriptions;
QSet<QString> m_PluginBlacklist;
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 0d96e63a..74091469 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -29,12 +29,17 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "nexusinterface.h"
#include "plugincontainer.h"
+#include <boost/uuid/uuid_generators.hpp>
+#include <boost/uuid/uuid_io.hpp>
+
#include <QDirIterator>
#include <QFileDialog>
#include <QMessageBox>
#include <QShortcut>
#include <QColorDialog>
#include <QInputDialog>
+#include <QJsonDocument>
+#include <QDesktopServices>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
@@ -42,11 +47,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
-
SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent)
: TutorableDialog("SettingsDialog", parent)
, ui(new Ui::SettingsDialog)
, m_PluginContainer(pluginContainer)
+ , m_nexusLogin(new QWebSocket)
+ , m_KeyReceived(false)
+ , m_KeyCleared(false)
+ , m_GeometriesReset(false)
{
ui->setupUi(this);
ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}");
@@ -54,10 +62,19 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent
QShortcut *delShortcut
= new QShortcut(QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem()));
+ connect(m_nexusLogin, SIGNAL(connected()), this, SLOT(dispatchLogin()));
+ 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);
}
SettingsDialog::~SettingsDialog()
{
+ m_loginTimer.stop();
+ m_nexusLogin->close();
+ disconnect(this);
delete ui;
}
@@ -71,6 +88,23 @@ QString SettingsDialog::getColoredButtonStyleSheet() const
"}");
}
+void SettingsDialog::setButtonColor(QPushButton *button, QColor &color)
+{
+ button->setStyleSheet(
+ QString("QPushButton {"
+ "background-color: rgba(%1, %2, %3, %4);"
+ "color: %5;"
+ "border: 1px solid;"
+ "padding: 3px;"
+ "}")
+ .arg(color.red())
+ .arg(color.green())
+ .arg(color.blue())
+ .arg(color.alpha())
+ .arg(Settings::getIdealTextColor(color).name())
+ );
+};
+
void SettingsDialog::accept()
{
QString newModPath = ui->modDirEdit->text();
@@ -99,17 +133,9 @@ bool SettingsDialog::getResetGeometries()
return ui->resetGeometryBtn->isChecked();
}
-void SettingsDialog::on_loginCheckBox_toggled(bool checked)
+bool SettingsDialog::getApiKeyChanged()
{
- QLineEdit *usernameEdit = findChild<QLineEdit*>("usernameEdit");
- QLineEdit *passwordEdit = findChild<QLineEdit*>("passwordEdit");
- if (checked) {
- passwordEdit->setEnabled(true);
- usernameEdit->setEnabled(true);
- } else {
- passwordEdit->setEnabled(false);
- usernameEdit->setEnabled(false);
- }
+ return m_KeyReceived || m_KeyCleared;
}
void SettingsDialog::on_categoriesBtn_clicked()
@@ -232,73 +258,55 @@ void SettingsDialog::on_browseGameDirBtn_clicked()
void SettingsDialog::on_containsBtn_clicked()
{
- QColor result = QColorDialog::getColor(m_ContainsColor, this);
+ QColor result = QColorDialog::getColor(m_ContainsColor, this, "Color Picker: Mod contains selected plugin", QColorDialog::ShowAlphaChannel);
if (result.isValid()) {
m_ContainsColor = result;
-
- ui->containsBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- result.name()).arg(Settings::getIdealTextColor(
- result).name()));
-
- /*ui->containsBtn->setAutoFillBackground(true);
- ui->containsBtn->setPalette(QPalette(result));
- QPalette palette = ui->containsBtn->palette();
- palette.setColor(QPalette::Background, result);
- ui->containsBtn->setPalette(palette);*/
+ setButtonColor(ui->containsBtn, result);
}
}
void SettingsDialog::on_containedBtn_clicked()
{
- QColor result = QColorDialog::getColor(m_ContainedColor, this);
+ QColor result = QColorDialog::getColor(m_ContainedColor, this, "ColorPicker: Plugin is Contained in selected Mod", QColorDialog::ShowAlphaChannel);
if (result.isValid()) {
m_ContainedColor = result;
-
- ui->containedBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- result.name()).arg(Settings::getIdealTextColor(
- result).name()));
-
- /*ui->containedBtn->setAutoFillBackground(true);
- ui->containedBtn->setPalette(QPalette(result));
- QPalette palette = ui->containedBtn->palette();
- palette.setColor(QPalette::Background, result);
- ui->containedBtn->setPalette(palette);*/
+ setButtonColor(ui->containedBtn, result);
}
}
void SettingsDialog::on_overwrittenBtn_clicked()
{
- QColor result = QColorDialog::getColor(m_OverwrittenColor, this);
+ QColor result = QColorDialog::getColor(m_OverwrittenColor, this, "ColorPicker: Is overwritten (loose files)", QColorDialog::ShowAlphaChannel);
if (result.isValid()) {
m_OverwrittenColor = result;
-
- ui->overwrittenBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- result.name()).arg(Settings::getIdealTextColor(
- result).name()));
-
- /*ui->overwrittenBtn->setAutoFillBackground(true);
- ui->overwrittenBtn->setPalette(QPalette(result));
- QPalette palette = ui->overwrittenBtn->palette();
- palette.setColor(QPalette::Background, result);
- ui->overwrittenBtn->setPalette(palette);*/
+ setButtonColor(ui->overwrittenBtn, result);
}
}
void SettingsDialog::on_overwritingBtn_clicked()
{
- QColor result = QColorDialog::getColor(m_OverwritingColor, this);
+ QColor result = QColorDialog::getColor(m_OverwritingColor, this, "ColorPicker: Is overwriting (loose files)", QColorDialog::ShowAlphaChannel);
if (result.isValid()) {
m_OverwritingColor = result;
+ setButtonColor(ui->overwritingBtn, result);
+ }
+}
- ui->overwritingBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- result.name()).arg(Settings::getIdealTextColor(
- result).name()));
+void SettingsDialog::on_overwrittenArchiveBtn_clicked()
+{
+ QColor result = QColorDialog::getColor(m_OverwrittenArchiveColor, this, "ColorPicker: Is overwritten (archive files)", QColorDialog::ShowAlphaChannel);
+ if (result.isValid()) {
+ m_OverwrittenArchiveColor = result;
+ setButtonColor(ui->overwrittenArchiveBtn, result);
+ }
+}
- /*ui->overwritingBtn->setAutoFillBackground(true);
- ui->overwritingBtn->setPalette(QPalette(result));
- QPalette palette = ui->overwritingBtn->palette();
- palette.setColor(QPalette::Background, result);
- ui->overwritingBtn->setPalette(palette);*/
+void SettingsDialog::on_overwritingArchiveBtn_clicked()
+{
+ QColor result = QColorDialog::getColor(m_OverwritingArchiveColor, this, "ColorPicker: Is overwriting (archive files)", QColorDialog::ShowAlphaChannel);
+ if (result.isValid()) {
+ m_OverwritingArchiveColor = result;
+ setButtonColor(ui->overwritingArchiveBtn, result);
}
}
@@ -306,25 +314,17 @@ void SettingsDialog::on_resetColorsBtn_clicked()
{
m_OverwritingColor = QColor(255, 0, 0, 64);
m_OverwrittenColor = QColor(0, 255, 0, 64);
+ m_OverwritingArchiveColor = QColor(255, 0, 255, 64);
+ m_OverwrittenArchiveColor = QColor(0, 255, 255, 64);
m_ContainsColor = QColor(0, 0, 255, 64);
m_ContainedColor = QColor(0, 0, 255, 64);
- ui->overwritingBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- QColor(255, 0, 0, 64).name()).arg(Settings::getIdealTextColor(
- QColor(255, 0, 0, 64)).name()));
-
- ui->overwrittenBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- QColor(0, 255, 0, 64).name()).arg(Settings::getIdealTextColor(
- QColor(0, 255, 0, 64)).name()));
-
- ui->containsBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- QColor(0, 0, 255, 64).name()).arg(Settings::getIdealTextColor(
- QColor(0, 0, 255, 64)).name()));
-
- ui->containedBtn->setStyleSheet(getColoredButtonStyleSheet().arg(
- QColor(0, 0, 255, 64).name()).arg(Settings::getIdealTextColor(
- QColor(0, 0, 255, 64)).name()));
-
+ setButtonColor(ui->overwritingBtn, m_OverwritingColor);
+ setButtonColor(ui->overwrittenBtn, m_OverwrittenColor);
+ setButtonColor(ui->overwritingArchiveBtn, m_OverwritingArchiveColor);
+ setButtonColor(ui->overwrittenArchiveBtn, m_OverwrittenArchiveColor);
+ setButtonColor(ui->containsBtn, m_ContainsColor);
+ setButtonColor(ui->containedBtn, m_ContainedColor);
}
void SettingsDialog::on_resetDialogsButton_clicked()
@@ -336,10 +336,87 @@ 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);
+ QUrl url = QUrl("wss://sso.nexusmods.com");
+ m_nexusLogin->open(url);
+}
+
+void SettingsDialog::dispatchLogin()
+{
+ m_KeyReceived = false;
+ QJsonObject login;
+ if (m_UUID.isEmpty()) {
+ boost::uuids::random_generator generator;
+ boost::uuids::uuid sessionId = generator();
+ m_UUID = boost::uuids::to_string(sessionId).c_str();
+ }
+ login.insert(QString("id"), QJsonValue(m_UUID));
+ login.insert(QString("token"), QJsonValue(m_AuthToken));
+ login.insert(QString("protocol"), 2);
+ QJsonDocument loginDoc(login);
+ QString finalMessage(loginDoc.toJson());
+ m_nexusLogin->sendTextMessage(finalMessage);
+ QDesktopServices::openUrl(QUrl(QString("https://www.nexusmods.com/sso?id=%1&application=%2").arg(m_UUID).arg("modorganizer2")));
+ m_loginTimer.start(30000);
+}
+
+void SettingsDialog::loginPing()
+{
+ if (m_nexusLogin->isValid()) {
+ m_nexusLogin->ping();
+ m_totalPings++;
+ }
+ if (m_totalPings >= 60) {
+ m_loginTimer.stop();
+ m_totalPings = 0;
+ m_nexusLogin->close(QWebSocketProtocol::CloseCodeGoingAway, "Timeout: No response received after thirty minutes. Cancelling request.");
+ }
+}
+
+void SettingsDialog::authError(QAbstractSocket::SocketError error)
+{
+ auto errorInfo = m_nexusLogin->errorString();
+ qCritical() << "An error occurred: " << errorInfo;
+}
+
+void SettingsDialog::receiveApiKey(const QString &response)
+{
+ QJsonDocument responseDoc = QJsonDocument::fromJson(response.toUtf8());
+ QVariantMap responseData = responseDoc.object().toVariantMap();
+ if (responseData["success"].toBool()) {
+ QVariantMap data = responseData["data"].toMap();
+ if (data.contains("connection_token")) {
+ m_AuthToken = data["connection_token"].toString();
+ } else {
+ m_KeyReceived = true;
+ emit processApiKey(data["api_key"].toString());
+ m_nexusLogin->close();
+ ui->nexusConnect->setText("Nexus API Key Stored");
+ m_loginTimer.stop();
+ m_totalPings = 0;
+ }
+ } else {
+ QString error("There was a problem with SSO initialization: %1");
+ qCritical() << error.arg(responseData["error"].toString());
+ m_nexusLogin->close();
+ }
+}
+
+void SettingsDialog::completeApiConnection()
+{
+ if (m_KeyReceived == true || !m_loginTimer.isActive())
+ emit closeApiConnection(ui->nexusConnect);
+ else
+ emit retryApiConnection();
+}
+
void SettingsDialog::storeSettings(QListWidgetItem *pluginItem)
{
if (pluginItem != nullptr) {
- QMap<QString, QVariant> settings = pluginItem->data(Qt::UserRole + 1).toMap();
+ QVariantMap settings = pluginItem->data(Qt::UserRole + 1).toMap();
for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) {
const QTreeWidgetItem *item = ui->pluginSettingsList->topLevelItem(i);
@@ -360,8 +437,8 @@ void SettingsDialog::on_pluginsList_currentItemChanged(QListWidgetItem *current,
ui->versionLabel->setText(plugin->version().canonicalString());
ui->descriptionLabel->setText(plugin->description());
- QMap<QString, QVariant> settings = current->data(Qt::UserRole + 1).toMap();
- QMap<QString, QVariant> descriptions = current->data(Qt::UserRole + 2).toMap();
+ QVariantMap settings = current->data(Qt::UserRole + 1).toMap();
+ QVariantMap descriptions = current->data(Qt::UserRole + 2).toMap();
ui->pluginSettingsList->setEnabled(settings.count() != 0);
for (auto iter = settings.begin(); iter != settings.end(); ++iter) {
QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(iter.key()));
@@ -403,6 +480,12 @@ void SettingsDialog::on_clearCacheButton_clicked()
NexusInterface::instance(m_PluginContainer)->clearCache();
}
+void SettingsDialog::on_revokeNexusAuthButton_clicked()
+{
+ emit revokeApiKey(ui->nexusConnect);
+ m_KeyCleared = true;
+}
+
void SettingsDialog::normalizePath(QLineEdit *lineEdit)
{
QString text = lineEdit->text();
@@ -441,3 +524,9 @@ void SettingsDialog::on_overwriteDirEdit_editingFinished()
{
normalizePath(ui->overwriteDirEdit);
}
+
+void SettingsDialog::on_resetGeometryBtn_clicked()
+{
+ m_GeometriesReset = true;
+ ui->resetGeometryBtn->setChecked(true);
+}
diff --git a/src/settingsdialog.h b/src/settingsdialog.h
index f0567d87..92a97c3f 100644
--- a/src/settingsdialog.h
+++ b/src/settingsdialog.h
@@ -1,151 +1,196 @@
-/*
-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/>.
-*/
-
-#ifndef SETTINGSDIALOG_H
-#define SETTINGSDIALOG_H
-
-#include "tutorabledialog.h"
-#include <iplugin.h>
-#include <QDialog>
-#include <QListWidgetItem>
-
-class PluginContainer;
-
-namespace Ui {
- class SettingsDialog;
-}
-
-/**
- * dialog used to change settings for Mod Organizer. On top of the
- * settings managed by the "Settings" class, this offers a button to open the
- * CategoriesDialog
- **/
-class SettingsDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT
-
-public:
- explicit SettingsDialog(PluginContainer *pluginContainer, QWidget *parent = 0);
- ~SettingsDialog();
-
- /**
- * @brief get stylesheet of settings buttons with colored background
- * @return string of stylesheet
- */
- QString getColoredButtonStyleSheet() const;
-
-public slots:
-
- virtual void accept();
-
-signals:
-
- void resetDialogs();
-
-private:
-
- void storeSettings(QListWidgetItem *pluginItem);
- void normalizePath(QLineEdit *lineEdit);
-
-public:
-
- QColor getOverwritingColor() { return m_OverwritingColor; }
- QColor getOverwrittenColor() { return m_OverwrittenColor; }
- QColor getContainsColor() { return m_ContainsColor; }
- QColor getContainedColor() { return m_ContainedColor; }
- QString getExecutableBlacklist() { return m_ExecutableBlacklist; }
- bool getResetGeometries();
-
- void setOverwritingColor(QColor col) { m_OverwritingColor = col; }
- void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; }
- void setContainsColor(QColor col) { m_ContainsColor = col; }
- void setContainedColor(QColor col) { m_ContainedColor = col; }
- void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; }
-
-
-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_browseBaseDirBtn_clicked();
-
- void on_browseOverwriteDirBtn_clicked();
-
- void on_browseProfilesDirBtn_clicked();
-
- void on_browseGameDirBtn_clicked();
-
- void on_overwritingBtn_clicked();
-
- void on_overwrittenBtn_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();
-
-private:
- Ui::SettingsDialog *ui;
- PluginContainer *m_PluginContainer;
-
- QColor m_OverwritingColor;
- QColor m_OverwrittenColor;
- QColor m_ContainsColor;
- QColor m_ContainedColor;
-
- QString m_ExecutableBlacklist;
-};
-
-
-
-#endif // SETTINGSDIALOG_H
+/*
+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/>.
+*/
+
+#ifndef SETTINGSDIALOG_H
+#define SETTINGSDIALOG_H
+
+#include "tutorabledialog.h"
+#include <iplugin.h>
+#include <QComboBox>
+#include <QDialog>
+#include <QWebSocket>
+#include <QListWidgetItem>
+#include <QTimer>
+
+class PluginContainer;
+
+namespace Ui {
+ class SettingsDialog;
+}
+
+/**
+ * dialog used to change settings for Mod Organizer. On top of the
+ * settings managed by the "Settings" class, this offers a button to open the
+ * CategoriesDialog
+ **/
+class SettingsDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
+
+public:
+ explicit SettingsDialog(PluginContainer *pluginContainer, QWidget *parent = 0);
+ ~SettingsDialog();
+
+ /**
+ * @brief get stylesheet of settings buttons with colored background
+ * @return string of stylesheet
+ */
+ QString getColoredButtonStyleSheet() const;
+
+ void setButtonColor(QPushButton *button, QColor &color);
+
+public slots:
+
+ virtual void accept();
+
+signals:
+
+ void resetDialogs();
+ void processApiKey(const QString &);
+ void closeApiConnection(QPushButton *);
+ void revokeApiKey(QPushButton *);
+ void retryApiConnection();
+
+private:
+
+ void storeSettings(QListWidgetItem *pluginItem);
+ void normalizePath(QLineEdit *lineEdit);
+
+public:
+
+ QColor getOverwritingColor() { return m_OverwritingColor; }
+ QColor getOverwrittenColor() { return m_OverwrittenColor; }
+ QColor getOverwritingArchiveColor() { return m_OverwritingArchiveColor; }
+ QColor getOverwrittenArchiveColor() { return m_OverwrittenArchiveColor; }
+ QColor getContainsColor() { return m_ContainsColor; }
+ QColor getContainedColor() { return m_ContainedColor; }
+ QString getExecutableBlacklist() { return m_ExecutableBlacklist; }
+ bool getResetGeometries();
+ bool getApiKeyChanged();
+
+ void setOverwritingColor(QColor col) { m_OverwritingColor = col; }
+ void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; }
+ void setOverwritingArchiveColor(QColor col) { m_OverwritingArchiveColor = col; }
+ void setOverwrittenArchiveColor(QColor col) { m_OverwrittenArchiveColor = col; }
+ void setContainsColor(QColor col) { m_ContainsColor = col; }
+ void setContainedColor(QColor col) { m_ContainedColor = col; }
+ void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; }
+
+
+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_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 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;
+
+ 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;
+
+ QString m_ExecutableBlacklist;
+ QWebSocket *m_nexusLogin;
+ QTimer m_loginTimer;
+ int m_totalPings = 0;
+};
+
+
+
+#endif // SETTINGSDIALOG_H
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index 0412fc10..fa8893b9 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -1,1403 +1,1397 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>SettingsDialog</class>
- <widget class="QDialog" name="SettingsDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>586</width>
- <height>462</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Settings</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QTabWidget" name="tabWidget">
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="generalTab">
- <attribute name="title">
- <string>General</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="QLabel" name="label_5">
- <property name="text">
- <string>Language</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="languageBox">
- <property name="toolTip">
- <string>The display language</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The display language. This will only displaye languages for which you have a translation installed.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_4">
- <item>
- <widget class="QLabel" name="label_10">
- <property name="text">
- <string>Style</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="styleBox">
- <property name="toolTip">
- <string>graphical style</string>
- </property>
- <property name="whatsThis">
- <string>graphical style of the MO user interface</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QCheckBox" name="usePrereleaseBox">
- <property name="toolTip">
- <string>Update to non-stable releases.</string>
- </property>
- <property name="whatsThis">
- <string>If this is enabled, the integrated update mechanism will notify of all releases, including pre-releases (alphas, betas).
-
-Please use this only if you're sufficiently tech-savvy to investigate issues, look for known problems in the issue tracker and create meaningful reports.
-
-If you use pre-releases, never contact me directly by e-mail or via private messages!</string>
- </property>
- <property name="text">
- <string>Install Pre-releases (Betas)</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QGroupBox" name="groupBox">
- <property name="title">
- <string>User interface</string>
- </property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="2" column="0" colspan="2">
- <widget class="QGroupBox" name="ModlistGroupBox">
- <property name="title">
- <string>Colors</string>
- </property>
- <layout class="QGridLayout" name="gridLayout_5">
- <item row="4" column="0" colspan="2">
- <widget class="QPushButton" name="resetColorsBtn">
- <property name="text">
- <string>Reset Colors</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QPushButton" name="overwrittenBtn">
- <property name="text">
- <string>Is overwritten (loose files)</string>
- </property>
- </widget>
- </item>
- <item row="2" column="0" colspan="2">
- <widget class="QPushButton" name="containsBtn">
- <property name="text">
- <string>Mod Contains selected Plugin</string>
- </property>
- </widget>
- </item>
- <item row="3" column="0" colspan="2">
- <widget class="QPushButton" name="containedBtn">
- <property name="text">
- <string>Plugin is Contained in selected Mod</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QPushButton" name="overwritingBtn">
- <property name="text">
- <string>Is overwriting (loose files)</string>
- </property>
- </widget>
- </item>
- <item row="5" column="0">
- <widget class="QCheckBox" name="colorSeparatorsBox">
- <property name="toolTip">
- <string>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</string>
- </property>
- <property name="whatsThis">
- <string>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</string>
- </property>
- <property name="text">
- <string>Show mod list separator colors on the scrollbar</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item row="6" column="0" colspan="2">
- <widget class="QPushButton" name="categoriesBtn">
- <property name="toolTip">
- <string>Modify the categories available to arrange your mods.</string>
- </property>
- <property name="whatsThis">
- <string>Modify the categories available to arrange your mods.</string>
- </property>
- <property name="text">
- <string>Configure Mod Categories</string>
- </property>
- </widget>
- </item>
- <item row="5" column="0" colspan="2">
- <widget class="QPushButton" name="resetDialogsButton">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Reset stored information from dialogs.</string>
- </property>
- <property name="whatsThis">
- <string>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box.</string>
- </property>
- <property name="text">
- <string>Reset Dialogs</string>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QCheckBox" name="compactBox">
- <property name="toolTip">
- <string>If checked, the download interface will be more compact.</string>
- </property>
- <property name="text">
- <string>Compact Download Interface</string>
- </property>
- </widget>
- </item>
- <item row="4" column="0" colspan="2">
- <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>
- <item row="0" column="1">
- <widget class="QCheckBox" name="showMetaBox">
- <property name="toolTip">
- <string>If checked, the download list will display meta information instead of file names.</string>
- </property>
- <property name="text">
- <string>Download Meta Information</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="pathsTab">
- <attribute name="title">
- <string>Paths</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_10">
- <item>
- <layout class="QGridLayout" name="gridLayout_2">
- <item row="6" column="2">
- <widget class="QPushButton" name="browseOverwriteDirBtn">
- <property name="text">
- <string>...</string>
- </property>
- </widget>
- </item>
- <item row="3" column="2">
- <widget class="QPushButton" name="browseModDirBtn">
- <property name="text">
- <string notr="true">...</string>
- </property>
- </widget>
- </item>
- <item row="6" column="1">
- <widget class="QLineEdit" name="overwriteDirEdit"/>
- </item>
- <item row="0" column="2">
- <widget class="QPushButton" name="browseBaseDirBtn">
- <property name="text">
- <string>...</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="baseDirEdit"/>
- </item>
- <item row="4" column="0">
- <widget class="QLabel" name="label_9">
- <property name="text">
- <string>Caches</string>
- </property>
- </widget>
- </item>
- <item row="6" column="0">
- <widget class="QLabel" name="label_24">
- <property name="text">
- <string>Overwrite</string>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QLineEdit" name="downloadDirEdit">
- <property name="toolTip">
- <string>Directory where downloads are stored.</string>
- </property>
- <property name="whatsThis">
- <string>Directory where downloads are stored.</string>
- </property>
- </widget>
- </item>
- <item row="4" column="1">
- <widget class="QLineEdit" name="cacheDirEdit"/>
- </item>
- <item row="4" column="2">
- <widget class="QPushButton" name="browseCacheDirBtn">
- <property name="text">
- <string notr="true">...</string>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="label_7">
- <property name="text">
- <string>Downloads</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <spacer name="verticalSpacer_8">
- <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 row="5" column="0">
- <widget class="QLabel" name="label_22">
- <property name="text">
- <string>Profiles</string>
- </property>
- </widget>
- </item>
- <item row="2" column="2">
- <widget class="QPushButton" name="browseDownloadDirBtn">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string notr="true">...</string>
- </property>
- </widget>
- </item>
- <item row="5" column="1">
- <widget class="QLineEdit" name="profilesDirEdit"/>
- </item>
- <item row="3" column="1">
- <widget class="QLineEdit" name="modDirEdit">
- <property name="toolTip">
- <string>Directory where mods are stored.</string>
- </property>
- <property name="whatsThis">
- <string>Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name).</string>
- </property>
- </widget>
- </item>
- <item row="5" column="2">
- <widget class="QPushButton" name="browseProfilesDirBtn">
- <property name="text">
- <string>...</string>
- </property>
- </widget>
- </item>
- <item row="11" column="1">
- <widget class="QLineEdit" name="managedGameDirEdit">
- <property name="acceptDrops">
- <bool>false</bool>
- </property>
- <property name="readOnly">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QLabel" name="label_8">
- <property name="text">
- <string>Mods</string>
- </property>
- </widget>
- </item>
- <item row="10" column="1">
- <spacer name="verticalSpacer_7">
- <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 row="11" column="0">
- <widget class="QLabel" name="label_29">
- <property name="text">
- <string>Managed Game</string>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="label_25">
- <property name="text">
- <string>Base Directory</string>
- </property>
- </widget>
- </item>
- <item row="9" column="0" colspan="3">
- <widget class="QLabel" name="label_26">
- <property name="text">
- <string>Use %BASE_DIR% to refer to the Base Directory.</string>
- </property>
- </widget>
- </item>
- <item row="11" column="2">
- <widget class="QPushButton" name="browseGameDirBtn">
- <property name="text">
- <string>...</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <spacer name="verticalSpacer_6">
- <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="QLabel" name="label_23">
- <property name="text">
- <string>Important: All directories have to be writeable!</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="nexusTab">
- <attribute name="title">
- <string>Nexus</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <item>
- <widget class="QGroupBox" name="nexusBox">
- <property name="toolTip">
- <string>Allows automatic log-in when the Nexus-Page for the game is clicked.</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;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.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="title">
- <string>Nexus</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QCheckBox" name="loginCheckBox">
- <property name="toolTip">
- <string/>
- </property>
- <property name="whatsThis">
- <string>If checked and if correct credentials are entered below, log-in to Nexus (for browsing and downloading) is automatic.</string>
- </property>
- <property name="text">
- <string>Automatically Log-In to Nexus</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_10">
- <item>
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>Username</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="usernameEdit">
- <property name="enabled">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string>Password</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="passwordEdit">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_11">
- <item>
- <widget class="QPushButton" name="clearCacheButton">
- <property name="toolTip">
- <string>Remove cache and cookies. Forces a new login.</string>
- </property>
- <property name="text">
- <string>Clear Cache</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_5">
- <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>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="offlineBox">
- <property name="statusTip">
- <string>Disable automatic internet features</string>
- </property>
- <property name="whatsThis">
- <string>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)</string>
- </property>
- <property name="text">
- <string>Offline Mode</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="proxyBox">
- <property name="statusTip">
- <string>Use a proxy for network connections.</string>
- </property>
- <property name="whatsThis">
- <string>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.</string>
- </property>
- <property name="text">
- <string>Use HTTP Proxy (Uses System Settings)</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_7">
- <item>
- <widget class="QPushButton" name="associateButton">
- <property name="text">
- <string>Associate with &quot;Download with manager&quot; links</string>
- </property>
- </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>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_8" stretch="0,0">
- <property name="spacing">
- <number>4</number>
- </property>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_6">
- <item>
- <widget class="QLabel" name="label_16">
- <property name="text">
- <string>Known Servers (updated on download)</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QListWidget" name="knownServersList">
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_8">
- <item>
- <widget class="QLabel" name="label_17">
- <property name="text">
- <string>Preferred Servers (Drag &amp; Drop)</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QListWidget" name="preferredServersList">
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </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>
- </widget>
- <widget class="QWidget" name="steamTab">
- <attribute name="title">
- <string>Steam</string>
- </attribute>
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <widget class="QLabel" name="label_19">
- <property name="text">
- <string>Username</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="steamUserEdit"/>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="label_20">
- <property name="text">
- <string>Password</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QLineEdit" name="steamPassEdit">
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <spacer name="verticalSpacer_5">
- <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 row="3" column="0" colspan="2">
- <widget class="QLabel" name="label_21">
- <property name="text">
- <string>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.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="4" column="0">
- <spacer name="verticalSpacer_4">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Expanding</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>232</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="pluginsTab">
- <attribute name="title">
- <string>Plugins</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_9" stretch="0,0,0">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_9">
- <item>
- <widget class="QListWidget" name="pluginsList">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,1">
- <item>
- <layout class="QFormLayout" name="formLayout_2">
- <item row="0" column="0">
- <widget class="QLabel" name="label_13">
- <property name="text">
- <string>Author:</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLabel" name="authorLabel">
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="label_15">
- <property name="text">
- <string>Version:</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QLabel" name="versionLabel">
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="label_14">
- <property name="text">
- <string>Description:</string>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QLabel" name="descriptionLabel">
- <property name="text">
- <string/>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QTreeWidget" name="pluginSettingsList">
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="rootIsDecorated">
- <bool>false</bool>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="expandsOnDoubleClick">
- <bool>false</bool>
- </property>
- <attribute name="headerVisible">
- <bool>false</bool>
- </attribute>
- <attribute name="headerDefaultSectionSize">
- <number>170</number>
- </attribute>
- <column>
- <property name="text">
- <string>Key</string>
- </property>
- </column>
- <column>
- <property name="text">
- <string>Value</string>
- </property>
- </column>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QLabel" name="label_18">
- <property name="text">
- <string>Blacklisted Plugins (use &lt;del&gt; to remove):</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QListWidget" name="pluginBlacklist"/>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="workaroundTab">
- <attribute name="title">
- <string>Workarounds</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,0">
- <item>
- <widget class="QLabel" name="label_4">
- <property name="text">
- <string>Steam App ID</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="QLineEdit" name="appIDEdit">
- <property name="toolTip">
- <string>The Steam AppID for your game</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The Steam App ID is required to directly start some games. For Skyrim, if this is not set or wrong, the &amp;quot;Mod Organizer&amp;quot; load mechanism may not work properly.&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The preset for this is the App ID of the &amp;quot;regular&amp;quot; version so in most cases, you should be set.&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;If you think you have a different version (GotY or something), follow these steps to get to the id:&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;1. Navigate to the game library in steam&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;2. right-click on the game you need the id for and choose &lt;/span&gt;&lt;span style=&quot; font-size:8pt; font-weight:600;&quot;&gt;Create desktop shortcut&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;3. right-click on the newly created shortcut on your desktop and select &lt;/span&gt;&lt;span style=&quot; font-size:8pt; font-weight:600;&quot;&gt;Properties&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;4. in the URL-field you should see something like this: &lt;/span&gt;&lt;span style=&quot; font-size:8pt; font-style:italic;&quot;&gt;steam://rungameid/22380&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;22380 is the id you're looking for.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,1">
- <property name="spacing">
- <number>7</number>
- </property>
- <item>
- <widget class="QLabel" name="label">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string>Load Mechanism</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="QComboBox" name="mechanismBox">
- <property name="toolTip">
- <string>Select loading mechanism. See help for details.</string>
- </property>
- <property name="whatsThis">
- <string>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.
-*Script Extender* In this mode, MO is installed as a Script Extender (obse, fose, nvse, skse) plugin.
-*Proxy DLL* In this mode, MO replaces one of the game's dlls with one that loads MO (and the original dll of course). This will ONLY work with Steam games and it has only been tested with Skyrim. Please use this only if the other mechanisms don't work.
-
-If you use the Steam version of Oblivion the default will NOT work. In this case, please install obse and use &quot;Script Extender&quot; as the load mechanism. Also you can then not start Oblivion from MO. Instead, use MO only to set up your mods, then quit and start Oblivion through Steam.</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="0,0,0">
- <item>
- <widget class="QLabel" name="label_11">
- <property name="text">
- <string>NMM Version</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="QLineEdit" name="nmmVersionEdit">
- <property name="toolTip">
- <string>The Version of Nexus Mod Manager to impersonate.</string>
- </property>
- <property name="whatsThis">
- <string>Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in.
-On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here.
-Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a &quot;compatible&quot; NMM version to the user agent.
-
-tl;dr-version: If Nexus-features don't work, insert the current version number of NMM here and try again.</string>
- </property>
- <property name="inputMask">
- <string notr="true">009.009.009</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- <property name="placeholderText">
- <string/>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QGridLayout" name="gridLayout_6">
- <item row="1" column="1">
- <widget class="QCheckBox" name="forceEnableBox">
- <property name="toolTip">
- <string>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)</string>
- </property>
- <property name="whatsThis">
- <string>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.</string>
- </property>
- <property name="text">
- <string>Force-enable game files</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QCheckBox" name="displayForeignBox">
- <property name="toolTip">
- <string>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.</string>
- </property>
- <property name="whatsThis">
- <string>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.
-
-If you disable this feature, MO will only display official DLCs this way. Please note that plugins (esps, esms, and esls) displayed in the right pane are completely unaffected by this feature.</string>
- </property>
- <property name="text">
- <string>Display mods installed outside MO</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QCheckBox" name="hideUncheckedBox">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Enforces that inactive ESPs and ESMs are never loaded.</string>
- </property>
- <property name="whatsThis">
- <string>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.</string>
- </property>
- <property name="text">
- <string>Hide inactive ESPs/ESMs</string>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QCheckBox" name="lockGUIBox">
- <property name="toolTip">
- <string>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</string>
- </property>
- <property name="whatsThis">
- <string>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</string>
- </property>
- <property name="text">
- <string>Lock GUI when running executable</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QPushButton" name="bsaDateBtn">
- <property name="toolTip">
- <string>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!</string>
- </property>
- <property name="whatsThis">
- <string>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!</string>
- </property>
- <property name="text">
- <string>Back-date BSAs</string>
- </property>
- <property name="icon">
- <iconset>
- <normaloff>:/MO/gui/resources/emblem-readonly.png</normaloff>:/MO/gui/resources/emblem-readonly.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="execBlacklistBtn">
- <property name="toolTip">
- <string>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.</string>
- </property>
- <property name="whatsThis">
- <string>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.</string>
- </property>
- <property name="text">
- <string>Configure Executables Blacklist</string>
- </property>
- <property name="autoDefault">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="resetGeometryBtn">
- <property name="toolTip">
- <string>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.</string>
- </property>
- <property name="whatsThis">
- <string>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.</string>
- </property>
- <property name="text">
- <string>Reset Window Geometries</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <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="QLabel" name="label_6">
- <property name="text">
- <string>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="diagnosticsTab">
- <attribute name="title">
- <string>Diagnostics</string>
- </attribute>
- <layout class="QGridLayout" name="gridLayout_4">
- <item row="2" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_13">
- <item>
- <widget class="QLabel" name="label_28">
- <property name="text">
- <string>Max Dumps To Keep</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_6">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>60</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QSpinBox" name="dumpsMaxEdit">
- <property name="toolTip">
- <string>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</string>
- </property>
- <property name="whatsThis">
- <string>
- Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
- Set &quot;Crash Dumps&quot; above to None to disable crash dump collection.
- </string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="3" column="0">
- <widget class="QLabel" name="diagnosticsExplainedLabel">
- <property name="toolTip">
- <string>Hint: right click link and copy link location</string>
- </property>
- <property name="text">
- <string>
- Logs and crash dumps are stored under your current instance in the &lt;a href=&quot;LOGS_FULL_PATH&quot;&gt;LOGS_DIR&lt;/a&gt;
- and &lt;a href=&quot;DUMPS_FULL_PATH&quot;&gt;DUMPS_DIR&lt;/a&gt; folders.
- Sending logs and/or crash dumps to the developers can help investigate issues.
- It is recommended to compress large log and dmp files before sending.
- </string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_12">
- <item>
- <widget class="QLabel" name="label_27">
- <property name="text">
- <string>Crash Dumps</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="dumpsTypeBox">
- <property name="toolTip">
- <string>Decides which type of crash dumps are collected when injected processes crash.</string>
- </property>
- <property name="whatsThis">
- <string>
- Decides which type of crash dumps are collected when injected processes crash.
- &quot;None&quot; Disables the generation of crash dumps by MO.
- &quot;Mini&quot; Default level which generates small dumps (only stack traces).
- &quot;Data&quot; Much larger dumps with additional information which may be need (also data segments).
- &quot;Full&quot; Even larger dumps with a full memory dump of the process.
- </string>
- </property>
- <item>
- <property name="text">
- <string>None</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Mini (recommended)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Data</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Full</string>
- </property>
- </item>
- </widget>
- </item>
- </layout>
- </item>
- <item row="4" column="0">
- <spacer name="verticalSpacer_10">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Expanding</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>232</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="0" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <item>
- <widget class="QLabel" name="label_12">
- <property name="text">
- <string>Log Level</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="logLevelBox">
- <property name="toolTip">
- <string>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</string>
- </property>
- <property name="whatsThis">
- <string>
- Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
- &quot;Debug&quot; 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 &quot;Info&quot; level for regluar use. On the &quot;Error&quot; level the log file usually remains empty.
- </string>
- </property>
- <item>
- <property name="text">
- <string>Debug</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Info (recommended)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Warning</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Error</string>
- </property>
- </item>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </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>
- <tabstops>
- <tabstop>languageBox</tabstop>
- <tabstop>styleBox</tabstop>
- <tabstop>logLevelBox</tabstop>
- <tabstop>usePrereleaseBox</tabstop>
- <tabstop>compactBox</tabstop>
- <tabstop>categoriesBtn</tabstop>
- <tabstop>baseDirEdit</tabstop>
- <tabstop>browseBaseDirBtn</tabstop>
- <tabstop>downloadDirEdit</tabstop>
- <tabstop>browseDownloadDirBtn</tabstop>
- <tabstop>modDirEdit</tabstop>
- <tabstop>browseModDirBtn</tabstop>
- <tabstop>cacheDirEdit</tabstop>
- <tabstop>browseCacheDirBtn</tabstop>
- <tabstop>profilesDirEdit</tabstop>
- <tabstop>browseProfilesDirBtn</tabstop>
- <tabstop>overwriteDirEdit</tabstop>
- <tabstop>browseOverwriteDirBtn</tabstop>
- <tabstop>loginCheckBox</tabstop>
- <tabstop>usernameEdit</tabstop>
- <tabstop>passwordEdit</tabstop>
- <tabstop>clearCacheButton</tabstop>
- <tabstop>offlineBox</tabstop>
- <tabstop>proxyBox</tabstop>
- <tabstop>associateButton</tabstop>
- <tabstop>knownServersList</tabstop>
- <tabstop>preferredServersList</tabstop>
- <tabstop>steamUserEdit</tabstop>
- <tabstop>steamPassEdit</tabstop>
- <tabstop>pluginsList</tabstop>
- <tabstop>pluginSettingsList</tabstop>
- <tabstop>pluginBlacklist</tabstop>
- <tabstop>appIDEdit</tabstop>
- <tabstop>mechanismBox</tabstop>
- <tabstop>nmmVersionEdit</tabstop>
- <tabstop>bsaDateBtn</tabstop>
- <tabstop>tabWidget</tabstop>
- </tabstops>
- <resources>
- <include location="resources.qrc"/>
- </resources>
- <connections>
- <connection>
- <sender>buttonBox</sender>
- <signal>accepted()</signal>
- <receiver>SettingsDialog</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>SettingsDialog</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>
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SettingsDialog</class>
+ <widget class="QDialog" name="SettingsDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>486</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="generalTab">
+ <attribute name="title">
+ <string>General</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Language</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="languageBox">
+ <property name="toolTip">
+ <string>The display language</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The display language. This will only displaye languages for which you have a translation installed.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QLabel" name="label_10">
+ <property name="text">
+ <string>Style</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="styleBox">
+ <property name="toolTip">
+ <string>graphical style</string>
+ </property>
+ <property name="whatsThis">
+ <string>graphical style of the MO user interface</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="usePrereleaseBox">
+ <property name="toolTip">
+ <string>Update to non-stable releases.</string>
+ </property>
+ <property name="whatsThis">
+ <string>If this is enabled, the integrated update mechanism will notify of all releases, including pre-releases (alphas, betas).
+
+Please use this only if you're sufficiently tech-savvy to investigate issues, look for known problems in the issue tracker and create meaningful reports.
+
+If you use pre-releases, never contact me directly by e-mail or via private messages!</string>
+ </property>
+ <property name="text">
+ <string>Install Pre-releases (Betas)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>User interface</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0" colspan="2">
+ <widget class="QGroupBox" name="ModlistGroupBox">
+ <property name="title">
+ <string>Colors</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_5">
+ <item row="6" column="0">
+ <widget class="QCheckBox" name="colorSeparatorsBox">
+ <property name="toolTip">
+ <string>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</string>
+ </property>
+ <property name="whatsThis">
+ <string>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</string>
+ </property>
+ <property name="text">
+ <string>Show mod list separator colors on the scrollbar</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <widget class="QPushButton" name="containedBtn">
+ <property name="text">
+ <string>Plugin is Contained in selected Mod</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="overwrittenBtn">
+ <property name="text">
+ <string>Is overwritten (loose files)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="overwritingBtn">
+ <property name="text">
+ <string>Is overwriting (loose files)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" colspan="2">
+ <widget class="QPushButton" name="resetColorsBtn">
+ <property name="text">
+ <string>Reset Colors</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QPushButton" name="containsBtn">
+ <property name="text">
+ <string>Mod Contains selected Plugin</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="overwrittenArchiveBtn">
+ <property name="text">
+ <string>Is overwritten (archive files)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QPushButton" name="overwritingArchiveBtn">
+ <property name="text">
+ <string>Is overwriting (archive files)</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="6" column="0" colspan="2">
+ <widget class="QPushButton" name="categoriesBtn">
+ <property name="toolTip">
+ <string>Modify the categories available to arrange your mods.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Modify the categories available to arrange your mods.</string>
+ </property>
+ <property name="text">
+ <string>Configure Mod Categories</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" colspan="2">
+ <widget class="QPushButton" name="resetDialogsButton">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Reset stored information from dialogs.</string>
+ </property>
+ <property name="whatsThis">
+ <string>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box.</string>
+ </property>
+ <property name="text">
+ <string>Reset Dialogs</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="compactBox">
+ <property name="toolTip">
+ <string>If checked, the download interface will be more compact.</string>
+ </property>
+ <property name="text">
+ <string>Compact Download Interface</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <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>
+ <item row="0" column="1">
+ <widget class="QCheckBox" name="showMetaBox">
+ <property name="toolTip">
+ <string>If checked, the download list will display meta information instead of file names.</string>
+ </property>
+ <property name="text">
+ <string>Download Meta Information</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="pathsTab">
+ <attribute name="title">
+ <string>Paths</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="6" column="2">
+ <widget class="QPushButton" name="browseOverwriteDirBtn">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <widget class="QPushButton" name="browseModDirBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QLineEdit" name="overwriteDirEdit"/>
+ </item>
+ <item row="0" column="2">
+ <widget class="QPushButton" name="browseBaseDirBtn">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="baseDirEdit"/>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_9">
+ <property name="text">
+ <string>Caches</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="label_24">
+ <property name="text">
+ <string>Overwrite</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="downloadDirEdit">
+ <property name="toolTip">
+ <string>Directory where downloads are stored.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Directory where downloads are stored.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLineEdit" name="cacheDirEdit"/>
+ </item>
+ <item row="4" column="2">
+ <widget class="QPushButton" name="browseCacheDirBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string>Downloads</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <spacer name="verticalSpacer_8">
+ <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 row="5" column="0">
+ <widget class="QLabel" name="label_22">
+ <property name="text">
+ <string>Profiles</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="browseDownloadDirBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QLineEdit" name="profilesDirEdit"/>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="modDirEdit">
+ <property name="toolTip">
+ <string>Directory where mods are stored.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name).</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="2">
+ <widget class="QPushButton" name="browseProfilesDirBtn">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="11" column="1">
+ <widget class="QLineEdit" name="managedGameDirEdit">
+ <property name="acceptDrops">
+ <bool>false</bool>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_8">
+ <property name="text">
+ <string>Mods</string>
+ </property>
+ </widget>
+ </item>
+ <item row="10" column="1">
+ <spacer name="verticalSpacer_7">
+ <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 row="11" column="0">
+ <widget class="QLabel" name="label_29">
+ <property name="text">
+ <string>Managed Game</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_25">
+ <property name="text">
+ <string>Base Directory</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="0" colspan="3">
+ <widget class="QLabel" name="label_26">
+ <property name="text">
+ <string>Use %BASE_DIR% to refer to the Base Directory.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="11" column="2">
+ <widget class="QPushButton" name="browseGameDirBtn">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_6">
+ <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="QLabel" name="label_23">
+ <property name="text">
+ <string>Important: All directories have to be writeable!</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="nexusTab">
+ <attribute name="title">
+ <string>Nexus</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QGroupBox" name="nexusBox">
+ <property name="toolTip">
+ <string>Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things.</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &amp;quot;Connect to Nexus&amp;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="title">
+ <string>Nexus</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QPushButton" name="nexusConnect">
+ <property name="text">
+ <string>Connect to Nexus</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_11">
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <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="revokeNexusAuthButton">
+ <property name="toolTip">
+ <string>Clear the stored Nexus API key and force reauthorization.</string>
+ </property>
+ <property name="text">
+ <string>Revoke Nexus Authorization</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="clearCacheButton">
+ <property name="toolTip">
+ <string>Remove cache and cookies.</string>
+ </property>
+ <property name="text">
+ <string>Clear Cache</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <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>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_8">
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="offlineBox">
+ <property name="statusTip">
+ <string>Disable automatic internet features</string>
+ </property>
+ <property name="whatsThis">
+ <string>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)</string>
+ </property>
+ <property name="text">
+ <string>Offline Mode</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="proxyBox">
+ <property name="statusTip">
+ <string>Use a proxy for network connections.</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="text">
+ <string>Use HTTP Proxy (Uses System Settings)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QCheckBox" name="endorsementBox">
+ <property name="text">
+ <string>Endorsement Integration</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="hideAPICounterBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Hide API Request Counter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <item>
+ <widget class="QPushButton" name="associateButton">
+ <property name="text">
+ <string>Associate with &quot;Download with manager&quot; links</string>
+ </property>
+ </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>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8" stretch="0,0">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QLabel" name="label_16">
+ <property name="text">
+ <string>Known Servers (updated on download)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListWidget" name="knownServersList">
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_17">
+ <property name="text">
+ <string>Preferred Servers (Drag &amp; Drop)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListWidget" name="preferredServersList">
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </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>
+ </widget>
+ <widget class="QWidget" name="steamTab">
+ <attribute name="title">
+ <string>Steam</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_19">
+ <property name="text">
+ <string>Username</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="steamUserEdit"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_20">
+ <property name="text">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="steamPassEdit">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <spacer name="verticalSpacer_5">
+ <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 row="3" column="0" colspan="2">
+ <widget class="QLabel" name="label_21">
+ <property name="text">
+ <string>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.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>232</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="pluginsTab">
+ <attribute name="title">
+ <string>Plugins</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9" stretch="0,0,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QListWidget" name="pluginsList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,1">
+ <item>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_13">
+ <property name="text">
+ <string>Author:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="authorLabel">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_15">
+ <property name="text">
+ <string>Version:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_14">
+ <property name="text">
+ <string>Description:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeWidget" name="pluginSettingsList">
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="expandsOnDoubleClick">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="headerDefaultSectionSize">
+ <number>170</number>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>Key</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Value</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_18">
+ <property name="text">
+ <string>Blacklisted Plugins (use &lt;del&gt; to remove):</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListWidget" name="pluginBlacklist"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="workaroundTab">
+ <attribute name="title">
+ <string>Workarounds</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,0">
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Steam App ID</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="QLineEdit" name="appIDEdit">
+ <property name="toolTip">
+ <string>The Steam AppID for your game</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The Steam App ID is required to directly start some games. For Skyrim, if this is not set or wrong, the &amp;quot;Mod Organizer&amp;quot; load mechanism may not work properly.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The preset for this is the App ID of the &amp;quot;regular&amp;quot; version so in most cases, you should be set.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;If you think you have a different version (GotY or something), follow these steps to get to the id:&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;1. Navigate to the game library in steam&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;2. right-click on the game you need the id for and choose &lt;/span&gt;&lt;span style=&quot; font-size:8pt; font-weight:600;&quot;&gt;Create desktop shortcut&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;3. right-click on the newly created shortcut on your desktop and select &lt;/span&gt;&lt;span style=&quot; font-size:8pt; font-weight:600;&quot;&gt;Properties&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;4. in the URL-field you should see something like this: &lt;/span&gt;&lt;span style=&quot; font-size:8pt; font-style:italic;&quot;&gt;steam://rungameid/22380&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;22380 is the id you're looking for.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,1">
+ <property name="spacing">
+ <number>7</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Load Mechanism</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="QComboBox" name="mechanismBox">
+ <property name="toolTip">
+ <string>Select loading mechanism. See help for details.</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.
+*Script Extender* In this mode, MO is installed as a Script Extender (obse, fose, nvse, skse) plugin.
+*Proxy DLL* In this mode, MO replaces one of the game's dlls with one that loads MO (and the original dll of course). This will ONLY work with Steam games and it has only been tested with Skyrim. Please use this only if the other mechanisms don't work.
+
+If you use the Steam version of Oblivion the default will NOT work. In this case, please install obse and use &quot;Script Extender&quot; as the load mechanism. Also you can then not start Oblivion from MO. Instead, use MO only to set up your mods, then quit and start Oblivion through Steam.</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_6">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="hideUncheckedBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Enforces that inactive ESPs and ESMs are never loaded.</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="text">
+ <string>Hide inactive ESPs/ESMs</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QCheckBox" name="displayForeignBox">
+ <property name="toolTip">
+ <string>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.</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.
+
+If you disable this feature, MO will only display official DLCs this way. Please note that plugins (esps, esms, and esls) displayed in the right pane are completely unaffected by this feature.</string>
+ </property>
+ <property name="text">
+ <string>Display mods installed outside MO</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="forceEnableBox">
+ <property name="toolTip">
+ <string>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="text">
+ <string>Force-enable game files</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QCheckBox" name="lockGUIBox">
+ <property name="toolTip">
+ <string>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</string>
+ </property>
+ <property name="text">
+ <string>Lock GUI when running executable</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="enableArchiveParsingBox">
+ <property name="toolTip">
+ <string>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;If you disable this feature, MO will only display conflicts between loose files.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable parsing of Archives (Experimental Feature)</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="bsaDateBtn">
+ <property name="toolTip">
+ <string>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!</string>
+ </property>
+ <property name="whatsThis">
+ <string>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!</string>
+ </property>
+ <property name="text">
+ <string>Back-date BSAs</string>
+ </property>
+ <property name="icon">
+ <iconset>
+ <normaloff>:/MO/gui/resources/emblem-readonly.png</normaloff>:/MO/gui/resources/emblem-readonly.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="execBlacklistBtn">
+ <property name="toolTip">
+ <string>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.</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="text">
+ <string>Configure Executables Blacklist</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="resetGeometryBtn">
+ <property name="toolTip">
+ <string>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.</string>
+ </property>
+ <property name="whatsThis">
+ <string>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.</string>
+ </property>
+ <property name="text">
+ <string>Reset Window Geometries</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <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="QLabel" name="label_6">
+ <property name="text">
+ <string>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="diagnosticsTab">
+ <attribute name="title">
+ <string>Diagnostics</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="2" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout_13">
+ <item>
+ <widget class="QLabel" name="label_28">
+ <property name="text">
+ <string>Max Dumps To Keep</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>60</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="dumpsMaxEdit">
+ <property name="toolTip">
+ <string>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</string>
+ </property>
+ <property name="whatsThis">
+ <string>
+ Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
+ Set &quot;Crash Dumps&quot; above to None to disable crash dump collection.
+ </string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="diagnosticsExplainedLabel">
+ <property name="toolTip">
+ <string>Hint: right click link and copy link location</string>
+ </property>
+ <property name="text">
+ <string>
+ Logs and crash dumps are stored under your current instance in the &lt;a href=&quot;LOGS_FULL_PATH&quot;&gt;LOGS_DIR&lt;/a&gt;
+ and &lt;a href=&quot;DUMPS_FULL_PATH&quot;&gt;DUMPS_DIR&lt;/a&gt; folders.
+ Sending logs and/or crash dumps to the developers can help investigate issues.
+ It is recommended to compress large log and dmp files before sending.
+ </string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout_12">
+ <item>
+ <widget class="QLabel" name="label_27">
+ <property name="text">
+ <string>Crash Dumps</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="dumpsTypeBox">
+ <property name="toolTip">
+ <string>Decides which type of crash dumps are collected when injected processes crash.</string>
+ </property>
+ <property name="whatsThis">
+ <string>
+ Decides which type of crash dumps are collected when injected processes crash.
+ &quot;None&quot; Disables the generation of crash dumps by MO.
+ &quot;Mini&quot; Default level which generates small dumps (only stack traces).
+ &quot;Data&quot; Much larger dumps with additional information which may be need (also data segments).
+ &quot;Full&quot; Even larger dumps with a full memory dump of the process.
+ </string>
+ </property>
+ <item>
+ <property name="text">
+ <string>None</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Mini (recommended)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Data</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Full</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="4" column="0">
+ <spacer name="verticalSpacer_10">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>232</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="0" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QLabel" name="label_12">
+ <property name="text">
+ <string>Log Level</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="logLevelBox">
+ <property name="toolTip">
+ <string>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</string>
+ </property>
+ <property name="whatsThis">
+ <string>
+ Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
+ &quot;Debug&quot; 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 &quot;Info&quot; level for regluar use. On the &quot;Error&quot; level the log file usually remains empty.
+ </string>
+ </property>
+ <item>
+ <property name="text">
+ <string>Debug</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Info (recommended)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Warning</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Error</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </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>
+ <tabstops>
+ <tabstop>languageBox</tabstop>
+ <tabstop>styleBox</tabstop>
+ <tabstop>logLevelBox</tabstop>
+ <tabstop>usePrereleaseBox</tabstop>
+ <tabstop>compactBox</tabstop>
+ <tabstop>categoriesBtn</tabstop>
+ <tabstop>baseDirEdit</tabstop>
+ <tabstop>browseBaseDirBtn</tabstop>
+ <tabstop>downloadDirEdit</tabstop>
+ <tabstop>browseDownloadDirBtn</tabstop>
+ <tabstop>modDirEdit</tabstop>
+ <tabstop>browseModDirBtn</tabstop>
+ <tabstop>cacheDirEdit</tabstop>
+ <tabstop>browseCacheDirBtn</tabstop>
+ <tabstop>profilesDirEdit</tabstop>
+ <tabstop>browseProfilesDirBtn</tabstop>
+ <tabstop>overwriteDirEdit</tabstop>
+ <tabstop>browseOverwriteDirBtn</tabstop>
+ <tabstop>clearCacheButton</tabstop>
+ <tabstop>associateButton</tabstop>
+ <tabstop>knownServersList</tabstop>
+ <tabstop>preferredServersList</tabstop>
+ <tabstop>steamUserEdit</tabstop>
+ <tabstop>steamPassEdit</tabstop>
+ <tabstop>pluginsList</tabstop>
+ <tabstop>pluginSettingsList</tabstop>
+ <tabstop>pluginBlacklist</tabstop>
+ <tabstop>appIDEdit</tabstop>
+ <tabstop>mechanismBox</tabstop>
+ <tabstop>bsaDateBtn</tabstop>
+ <tabstop>tabWidget</tabstop>
+ </tabstops>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>SettingsDialog</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>SettingsDialog</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/shared/directoryentry.cpp b/src/shared/directoryentry.cpp
index cebf270e..1179110a 100644
--- a/src/shared/directoryentry.cpp
+++ b/src/shared/directoryentry.cpp
@@ -220,50 +220,67 @@ std::vector<FileEntry::Ptr> FilesOrigin::getFiles() const
return result;
}
+bool FilesOrigin::containsArchive(std::wstring archiveName)
+{
+ for (FileEntry::Index fileIdx : m_Files)
+ if (FileEntry::Ptr p = m_FileRegister.lock()->getFile(fileIdx))
+ if (p->isFromArchive(archiveName)) return true;
+ return false;
+}
//
// FileEntry
//
-void FileEntry::addOrigin(int origin, FILETIME fileTime, const std::wstring &archive)
+void FileEntry::addOrigin(int origin, FILETIME fileTime, const std::wstring &archive, int order)
{
m_LastAccessed = time(nullptr);
if (m_Parent != nullptr) {
m_Parent->propagateOrigin(origin);
}
+
+ // If this file has no previous origin, this mod is now the origin with no alternatives
if (m_Origin == -1) {
m_Origin = origin;
m_FileTime = fileTime;
- m_Archive = archive;
- } else if ((m_Parent != nullptr)
- && (m_Parent->getOriginByID(origin).getPriority() > m_Parent->getOriginByID(m_Origin).getPriority())
- && (archive.size() == 0 || m_Archive.size() > 0 )) {
- if (std::find_if(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::wstring> &i) -> bool { return i.first == m_Origin; }) == m_Alternatives.end()) {
- m_Alternatives.push_back(std::pair<int, std::wstring>(m_Origin, m_Archive));
+ m_Archive = std::pair<std::wstring, int>(archive, order);
+ }
+
+ // If this mod has a higher priority than the origin mod OR
+ // this mod has a loose file and the origin mod has an archived file,
+ // this mod is now the origin and the previous origin is the first alternative
+ else if ((m_Parent != nullptr)
+ && ((m_Parent->getOriginByID(origin).getPriority() > m_Parent->getOriginByID(m_Origin).getPriority())
+ || (archive.size() == 0 && m_Archive.first.size() > 0 ))) {
+ if (std::find_if(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::pair<std::wstring, int>> &i) -> bool { return i.first == m_Origin; }) == m_Alternatives.end()) {
+ m_Alternatives.push_back(std::pair<int, std::pair<std::wstring, int>>(m_Origin, m_Archive));
}
m_Origin = origin;
m_FileTime = fileTime;
- m_Archive = archive;
- } else {
+ m_Archive = std::pair<std::wstring, int>(archive, order);
+ }
+
+ // This mod is just an alternative
+ else {
bool found = false;
if (m_Origin == origin) {
// already an origin
return;
}
- for (std::vector<std::pair<int, std::wstring>>::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
+ for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
if (iter->first == origin) {
// already an origin
return;
}
if ((m_Parent != nullptr)
&& (m_Parent->getOriginByID(iter->first).getPriority() < m_Parent->getOriginByID(origin).getPriority())) {
- m_Alternatives.insert(iter, std::pair<int, std::wstring>(origin, archive));
+ m_Alternatives.insert(iter, std::pair<int, std::pair<std::wstring, int>>(origin, std::pair<std::wstring, int>(archive, order)));
found = true;
break;
}
}
if (!found) {
- m_Alternatives.push_back(std::pair<int, std::wstring>(origin, archive));
+ m_Alternatives.push_back(std::pair<int, std::pair<std::wstring, int>>(origin, std::pair<std::wstring, int>(archive, order)));
}
}
}
@@ -273,38 +290,59 @@ bool FileEntry::removeOrigin(int origin)
if (m_Origin == origin) {
if (!m_Alternatives.empty()) {
// find alternative with the highest priority
- std::vector<std::pair<int, std::wstring>>::iterator currentIter = m_Alternatives.begin();
- for (std::vector<std::pair<int, std::wstring>>::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
- if ((m_Parent->getOriginByID(iter->first).getPriority() > m_Parent->getOriginByID(currentIter->first).getPriority()) &&
- (iter->first != origin)) {
- currentIter = iter;
+ std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator currentIter = m_Alternatives.begin();
+ for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
+ if (iter->first != origin) {
+
+ //Both files are not from archives.
+ if (!iter->second.first.size() && !currentIter->second.first.size()) {
+ if ((m_Parent->getOriginByID(iter->first).getPriority() > m_Parent->getOriginByID(currentIter->first).getPriority())) {
+ currentIter = iter;
+ }
+ }
+ else {
+ //Both files are from archives
+ if (iter->second.first.size() && currentIter->second.first.size()) {
+ if (iter->second.second > currentIter->second.second) {
+ currentIter = iter;
+ }
+ }
+ else {
+ //Only one of the two is an archive, so we change currentIter only if he is the archive one.
+ if (currentIter->second.first.size()) {
+ currentIter = iter;
+ }
+ }
+ }
}
}
int currentID = currentIter->first;
+ m_Archive = currentIter->second;
m_Alternatives.erase(currentIter);
m_Origin = currentID;
// now we need to update the file time...
- std::wstring filePath = getFullPath();
- HANDLE file = ::CreateFile(filePath.c_str(), GENERIC_READ | GENERIC_WRITE,
- 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
- if (!::GetFileTime(file, nullptr, nullptr, &m_FileTime)) {
+ //std::wstring filePath = getFullPath();
+ //HANDLE file = ::CreateFile(filePath.c_str(), GENERIC_READ | GENERIC_WRITE,
+ // 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ //if (!::GetFileTime(file, nullptr, nullptr, &m_FileTime)) {
// maybe this file is in a bsa, but there is no easy way to find out which. User should refresh
// the view to find out
- m_Archive = L"bsa?";
- } else {
- m_Archive = L"";
- }
+ //m_Archive = std::pair<std::wstring, int>(L"bsa?", -1);
+ //} else {
+ //m_Archive = std::pair<std::wstring, int>(L"", -1);
+ //}
- ::CloseHandle(file);
+ //::CloseHandle(file);
} else {
m_Origin = -1;
+ m_Archive = std::pair<std::wstring, int>(L"", -1);
return true;
}
} else {
- std::vector<std::pair<int, std::wstring>>::iterator newEnd = std::find_if(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::wstring> &i) -> bool { return i.first == origin; });
+ auto newEnd = std::remove_if(m_Alternatives.begin(), m_Alternatives.end(), [&](auto &i) -> bool { return i.first == origin; });
if (newEnd != m_Alternatives.end())
m_Alternatives.erase(newEnd, m_Alternatives.end());
}
@@ -318,7 +356,7 @@ FileEntry::FileEntry()
}
FileEntry::FileEntry(Index index, const std::wstring &name, DirectoryEntry *parent)
- : m_Index(index), m_Name(name), m_Origin(-1), m_Archive(L""), m_Parent(parent), m_LastAccessed(time(nullptr))
+ : m_Index(index), m_Name(name), m_Origin(-1), m_Archive(L"", -1), m_Parent(parent), m_LastAccessed(time(nullptr))
{
LEAK_TRACE;
}
@@ -330,16 +368,23 @@ FileEntry::~FileEntry()
void FileEntry::sortOrigins()
{
- m_Alternatives.push_back(std::pair<int, std::wstring>(m_Origin, m_Archive));
- std::sort(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::wstring> &LHS, const std::pair<int, std::wstring> &RHS) -> bool {
- if ((!LHS.second.size() && !RHS.second.size()) || (LHS.second.size() && RHS.second.size())) {
+ m_Alternatives.push_back(std::pair<int, std::pair<std::wstring, int>>(m_Origin, m_Archive));
+ std::sort(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::pair<std::wstring, int>> &LHS, const std::pair<int, std::pair<std::wstring, int>> &RHS) -> bool {
+ if (!LHS.second.first.size() && !RHS.second.first.size()) {
int l = m_Parent->getOriginByID(LHS.first).getPriority(); if (l < 0) l = INT_MAX;
int r = m_Parent->getOriginByID(RHS.first).getPriority(); if (r < 0) r = INT_MAX;
return l < r;
}
- if (RHS.second.size()) return false;
+ if (LHS.second.first.size() && RHS.second.first.size()) {
+ int l = LHS.second.second; if (l < 0) l = INT_MAX;
+ int r = RHS.second.second; if (r < 0) r = INT_MAX;
+
+ return l < r;
+ }
+
+ if (RHS.second.first.size()) return false;
return true;
});
if (!m_Alternatives.empty()) {
@@ -379,6 +424,16 @@ std::wstring FileEntry::getRelativePath() const
return result + L"\\" + m_Name;
}
+bool FileEntry::isFromArchive(std::wstring archiveName)
+{
+ if (archiveName.length() == 0) return m_Archive.first.length() != 0;
+ if (m_Archive.first.compare(archiveName) == 0) return true;
+ for (auto alternative : m_Alternatives) {
+ if (alternative.second.first.compare(archiveName) == 0) return true;
+ }
+ return false;
+}
+
//
// DirectoryEntry
@@ -451,7 +506,7 @@ void DirectoryEntry::addFromOrigin(const std::wstring &originName, const std::ws
}
-void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority)
+void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority, int order)
{
FilesOrigin &origin = createOrigin(originName, directory, priority);
@@ -459,23 +514,33 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di
if (::GetFileAttributesExW(fileName.c_str(), GetFileExInfoStandard, &fileData) == 0) {
throw windows_error("failed to determine file time");
}
+ FILETIME now;
+ ::GetSystemTimeAsFileTime(&now);
+
+ const double clfSecondsPer100ns = 100. * 1.E-9;
+
+ ((ULARGE_INTEGER *)&now)->QuadPart -= ((double)5) / clfSecondsPer100ns;
- BSA::Archive archive;
- BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str(), false);
- if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
- std::ostringstream stream;
- stream << "invalid bsa file: " << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError();
- throw std::runtime_error(stream.str());
- }
size_t namePos = fileName.find_last_of(L"\\/");
if (namePos == std::wstring::npos) {
namePos = 0;
- } else {
+ }
+ else {
++namePos;
}
- addFiles(origin, archive.getRoot(), fileData.ftLastWriteTime, fileName.substr(namePos));
- m_Populated = true;
+ if (!containsArchive(fileName.substr(namePos)) || ::CompareFileTime(&fileData.ftLastWriteTime, &now) > 0) {
+ BSA::Archive archive;
+ BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str(), false);
+ if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
+ std::ostringstream stream;
+ stream << "invalid bsa file: " << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError();
+ throw std::runtime_error(stream.str());
+ }
+
+ addFiles(origin, archive.getRoot(), fileData.ftLastWriteTime, fileName.substr(namePos), order);
+ m_Populated = true;
+ }
}
void DirectoryEntry::propagateOrigin(int origin)
@@ -536,7 +601,7 @@ void DirectoryEntry::addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOf
getSubDirectory(findData.cFileName, true, origin.getID())->addFiles(origin, buffer, bufferOffset + offset);
}
} else {
- insert(findData.cFileName, origin, findData.ftLastWriteTime, L"");
+ insert(findData.cFileName, origin, findData.ftLastWriteTime, L"", -1);
}
result = ::FindNextFileW(searchHandle, &findData);
}
@@ -546,12 +611,12 @@ void DirectoryEntry::addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOf
}
-void DirectoryEntry::addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName)
+void DirectoryEntry::addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName, int order)
{
// add files
for (unsigned int fileIdx = 0; fileIdx < archiveFolder->getNumFiles(); ++fileIdx) {
BSA::File::Ptr file = archiveFolder->getFile(fileIdx);
- insert(ToWString(file->getName(), true), origin, fileTime, archiveName);
+ insert(ToWString(file->getName(), true), origin, fileTime, archiveName, order);
}
// recurse into subdirectories
@@ -559,7 +624,7 @@ void DirectoryEntry::addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolde
BSA::Folder::Ptr folder = archiveFolder->getSubFolder(folderIdx);
DirectoryEntry *folderEntry = getSubDirectoryRecursive(ToWString(folder->getName(), true), true, origin.getID());
- folderEntry->addFiles(origin, folder, fileTime, archiveName);
+ folderEntry->addFiles(origin, folder, fileTime, archiveName, order);
}
}
@@ -626,7 +691,7 @@ void DirectoryEntry::insertFile(const std::wstring &filePath, FilesOrigin &origi
{
size_t pos = filePath.find_first_of(L"\\/");
if (pos == std::string::npos) {
- this->insert(filePath, origin, fileTime, std::wstring());
+ this->insert(filePath, origin, fileTime, std::wstring(), -1);
} else {
std::wstring dirName = filePath.substr(0, pos);
std::wstring rest = filePath.substr(pos + 1);
@@ -667,6 +732,14 @@ void DirectoryEntry::removeFiles(const std::set<FileEntry::Index> &indices)
}
}
+bool DirectoryEntry::containsArchive(std::wstring archiveName)
+{
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
+ if (entry->isFromArchive(archiveName)) return true;
+ }
+ return false;
+}
int DirectoryEntry::anyOrigin() const
{
@@ -890,7 +963,7 @@ void FileRegister::unregisterFile(FileEntry::Ptr file)
// unregister from origin
int originID = file->getOrigin(ignore);
m_OriginConnection->getByID(originID).removeFile(file->getIndex());
- const std::vector<std::pair<int, std::wstring>> &alternatives = file->getAlternatives();
+ const std::vector<std::pair<int, std::pair<std::wstring, int>>> &alternatives = file->getAlternatives();
for (auto iter = alternatives.begin(); iter != alternatives.end(); ++iter) {
m_OriginConnection->getByID(iter->first).removeFile(file->getIndex());
}
diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h
index 8dbedaf4..12cef11d 100644
--- a/src/shared/directoryentry.h
+++ b/src/shared/directoryentry.h
@@ -64,7 +64,7 @@ public:
time_t lastAccessed() const { return m_LastAccessed; }
- void addOrigin(int origin, FILETIME fileTime, const std::wstring &archive);
+ void addOrigin(int origin, FILETIME fileTime, const std::wstring &archive, int order);
// remove the specified origin from the list of origins that contain this file. if no origin is left,
// the file is effectively deleted and true is returned. otherwise, false is returned
bool removeOrigin(int origin);
@@ -72,13 +72,13 @@ 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::wstring>> &getAlternatives() const { return m_Alternatives; }
+ const std::vector<std::pair<int, std::pair<std::wstring, int>>> &getAlternatives() const { return m_Alternatives; }
const std::wstring &getName() const { return m_Name; }
int getOrigin() const { return m_Origin; }
- int getOrigin(bool &archive) const { archive = (m_Archive.length() != 0); return m_Origin; }
- const std::wstring &getArchive() const { return m_Archive; }
- bool isFromArchive() const { return m_Archive.length() != 0; }
+ int getOrigin(bool &archive) const { archive = (m_Archive.first.length() != 0); return m_Origin; }
+ const std::pair<std::wstring, int> &getArchive() const { return m_Archive; }
+ bool isFromArchive(std::wstring archiveName = L"");
std::wstring getFullPath() const;
std::wstring getRelativePath() const;
DirectoryEntry *getParent() { return m_Parent; }
@@ -97,8 +97,8 @@ private:
Index m_Index;
std::wstring m_Name;
int m_Origin = -1;
- std::wstring m_Archive;
- std::vector<std::pair<int, std::wstring>> m_Alternatives;
+ std::pair<std::wstring, int> m_Archive;
+ std::vector<std::pair<int, std::pair<std::wstring, int>>> m_Alternatives;
DirectoryEntry *m_Parent;
mutable FILETIME m_FileTime;
@@ -142,6 +142,8 @@ public:
void addFile(FileEntry::Index index) { m_Files.insert(index); }
void removeFile(FileEntry::Index index);
+ bool containsArchive(std::wstring archiveName);
+
private:
FilesOrigin(int ID, const std::wstring &name, const std::wstring &path, int priority,
@@ -221,7 +223,7 @@ public:
// add files to this directory (and subdirectories) from the specified origin. That origin may exist or not
void addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority);
- void addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority);
+ void addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority, int order);
void propagateOrigin(int origin);
@@ -253,6 +255,8 @@ public:
*/
const FileEntry::Ptr findFile(const std::wstring &name) const;
+ bool containsArchive(std::wstring archiveName);
+
/** search through this directory and all subdirectories for a file by the specified name (relative path).
if directory is not nullptr, the referenced variable will be set to the path containing the file */
const FileEntry::Ptr searchFile(const std::wstring &path, const DirectoryEntry **directory) const;
@@ -297,7 +301,7 @@ private:
DirectoryEntry(const DirectoryEntry &reference);
DirectoryEntry &operator=(const DirectoryEntry &reference);
- void insert(const std::wstring &fileName, FilesOrigin &origin, FILETIME fileTime, const std::wstring &archive) {
+ void insert(const std::wstring &fileName, FilesOrigin &origin, FILETIME fileTime, const std::wstring &archive, int order) {
std::wstring fileNameLower = ToLower(fileName);
auto iter = m_Files.find(fileNameLower);
FileEntry::Ptr file;
@@ -308,12 +312,12 @@ private:
// TODO this has been observed to cause a crash, no clue why
m_Files[fileNameLower] = file->getIndex();
}
- file->addOrigin(origin.getID(), fileTime, archive);
+ file->addOrigin(origin.getID(), fileTime, archive, order);
origin.addFile(file->getIndex());
}
void addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset);
- void addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName);
+ void addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName, int order);
DirectoryEntry *getSubDirectory(const std::wstring &name, bool create, int originID = -1);
diff --git a/src/shared/leaktrace.cpp b/src/shared/leaktrace.cpp
index 1ff1fa9d..466162a4 100644
--- a/src/shared/leaktrace.cpp
+++ b/src/shared/leaktrace.cpp
@@ -1,3 +1,8 @@
+//disable warning messages 4302 , 4311 and 4312
+#pragma warning( disable : 4302 )
+#pragma warning( disable : 4311 )
+#pragma warning( disable : 4312 )
+
#include "leaktrace.h"
#include "stackdata.h"
#include <Windows.h>
@@ -31,7 +36,7 @@ static struct __TraceData {
for (auto iter = result.begin(); iter != result.end(); ++iter) {
printf("-----------------------------------\n"
"%d objects not freed, allocated at:\n%s",
- iter->second.size(), iter->first.toString().c_str());
+ static_cast<int>(iter->second.size()), iter->first.toString().c_str());
printf("Addresses: ");
for (int i = 0;
i < (std::min<int>)(5, static_cast<int>(iter->second.size())); ++i) {
@@ -55,3 +60,9 @@ void LeakTrace::TraceDealloc(void *ptr)
{
__trace.deregTrace(ptr);
}
+
+//re-enable warning messages 4302 , 4311 and 4312
+#pragma warning( default : 4302 )
+#pragma warning( default : 4311 )
+#pragma warning( default : 4312 )
+
diff --git a/src/shared/util.cpp b/src/shared/util.cpp
index bad0d64d..ed7c434e 100644
--- a/src/shared/util.cpp
+++ b/src/shared/util.cpp
@@ -108,27 +108,31 @@ static auto locToLower = [] (char in) -> char {
std::string &ToLower(std::string &text)
{
- std::transform(text.begin(), text.end(), text.begin(), locToLower);
+ //std::transform(text.begin(), text.end(), text.begin(), locToLower);
+ CharLowerBuffA(const_cast<CHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
return text;
}
std::string ToLower(const std::string &text)
{
std::string result(text);
- std::transform(result.begin(), result.end(), result.begin(), locToLower);
+ //std::transform(result.begin(), result.end(), result.begin(), locToLower);
+ CharLowerBuffA(const_cast<CHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
return result;
}
std::wstring &ToLower(std::wstring &text)
{
- std::transform(text.begin(), text.end(), text.begin(), locToLowerW);
+ //std::transform(text.begin(), text.end(), text.begin(), locToLowerW);
+ CharLowerBuffW(const_cast<WCHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
return text;
}
std::wstring ToLower(const std::wstring &text)
{
std::wstring result(text);
- std::transform(result.begin(), result.end(), result.begin(), locToLowerW);
+ //std::transform(result.begin(), result.end(), result.begin(), locToLowerW);
+ CharLowerBuffW(const_cast<WCHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
return result;
}
diff --git a/src/singleinstance.cpp b/src/singleinstance.cpp
index 89332f9b..7abf3879 100644
--- a/src/singleinstance.cpp
+++ b/src/singleinstance.cpp
@@ -54,7 +54,7 @@ SingleInstance::SingleInstance(bool forcePrimary, QObject *parent) :
m_PrimaryInstance = true;
}
if (m_PrimaryInstance) {
- connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()));
+ connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()), Qt::QueuedConnection);
// has to be called before listen
m_Server.setSocketOptions(QLocalServer::WorldAccessOption);
m_Server.listen(s_Key);
diff --git a/src/spawn.cpp b/src/spawn.cpp
index f92387d5..e1de5c0f 100644
--- a/src/spawn.cpp
+++ b/src/spawn.cpp
@@ -26,6 +26,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <Shellapi.h>
#include <appconfig.h>
#include <windows_error.h>
+#include "helper.h"
#include <QApplication>
#include <QMessageBox>
@@ -93,7 +94,7 @@ static bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory,
PROCESS_INFORMATION pi;
BOOL success = FALSE;
if (hooked) {
- qDebug() << "Creating process hooked: <" << QString::fromWCharArray(commandLine, length) <<">";
+ qDebug() << "Creating process hooked: <" << QString::fromWCharArray(commandLine, static_cast<int>(length)) <<">";
success = ::CreateProcessHooked(nullptr,
commandLine,
nullptr, nullptr, // no special process or thread attributes
@@ -148,23 +149,31 @@ HANDLE startBinary(const QFileInfo &binary,
}
} catch (const windows_error &e) {
if (e.getErrorCode() == ERROR_ELEVATION_REQUIRED) {
- // TODO: check if this is really correct. Are all settings updated that the secondary instance may use?
-
- if (QMessageBox::question(nullptr, QObject::tr("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"
"\"%1\"\n"
"can be installed to work without elevation.\n\n"
- "Start elevated anyway? "
- "(you will be asked if you want to allow ModOrganizer.exe to make changes to the system)").arg(
+ "Restart Mod Organizer as an elevated process?\n"
+ "You will be asked if you want to allow helper.exe to make changes to the system. "
+ "You will need to relaunch the process above manually.").arg(
QDir::toNativeSeparators(binary.absoluteFilePath())),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- ::ShellExecuteW(nullptr, L"runas", ToWString(QCoreApplication::applicationFilePath()).c_str(),
- (std::wstring(L"\"") + binaryName + L"\" " + ToWString(arguments)).c_str(), currentDirectoryName.c_str(), SW_SHOWNORMAL);
- return INVALID_HANDLE_VALUE;
- } else {
- return INVALID_HANDLE_VALUE;
+ WCHAR cwd[MAX_PATH];
+ if (!GetCurrentDirectory(MAX_PATH, cwd)) {
+ reportError(QObject::tr("failed to spawn \"%1\": %2").arg(binary.fileName()).arg(::GetLastError()));
+ cwd[0] = L'\0';
+ }
+ if (!Helper::adminLaunch(
+ qApp->applicationDirPath().toStdWString(),
+ qApp->applicationFilePath().toStdWString(),
+ std::wstring(cwd))) {
+ return INVALID_HANDLE_VALUE;
+ }
+ qApp->exit(0);
}
+ return INVALID_HANDLE_VALUE;
+
} else {
reportError(QObject::tr("failed to spawn \"%1\": %2").arg(binary.fileName()).arg(e.what()));
return INVALID_HANDLE_VALUE;
diff --git a/src/stylesheets/Night Eyes.qss b/src/stylesheets/Night Eyes.qss
index e9bb6c44..6c6b0629 100644
--- a/src/stylesheets/Night Eyes.qss
+++ b/src/stylesheets/Night Eyes.qss
@@ -648,3 +648,27 @@ DownloadListWidgetCompact QLabel
background: #141414;
padding: 4px;
}
+
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
+DownloadListWidget::item:hover {
+ padding: 0px;
+}
+
+DownloadListWidget::item:selected {
+ padding: 0px;
+}
+
+QProgressBar
+{
+ border: 2px solid grey;
+ border-radius: 5px;
+ text-align: center;
+ margin: 0px;
+} \ No newline at end of file
diff --git a/src/stylesheets/Paper Automata.qss b/src/stylesheets/Paper Automata.qss
index 748e589e..d72aa7e2 100644
--- a/src/stylesheets/Paper Automata.qss
+++ b/src/stylesheets/Paper Automata.qss
@@ -1,11 +1,11 @@
-/* v1.1 Paper Automata by 6788-00 */
-/* https://6788-00.tumblr.com/ */
+/* v2.0 Paper Automata by 6788-00 */
+/* https://6788-00.tumblr.com/ */
/* Color Palette */
/* Background - Main | #CDC8B0 */
/* Background - Content | #DAD4BB */
-/* Hover | #B4AF9A */
-/* Selected | #4E4B42 */
+/* Hover | #B4AF9A */
+/* Selected | #4E4B42 */
/* Accent | #CD664D */
/* All */
@@ -55,6 +55,10 @@ QAbstractItemView {
selection-color: #4E4B42;
}
+QAbstractItemView::item {
+ min-height: 22px;
+}
+
QAbstractItemView::item:hover {
/* rows on left and right pane when moused-over */
background: #B4AF9A;
@@ -141,7 +145,7 @@ QToolButton QMenu {
qproperty-icon: url(./Paper/Automata/Toolbar/settings.svg);
}
-#actionProblems {
+#actionNotifications {
qproperty-icon: url(./Paper/Automata/Toolbar/problems.svg);
}
@@ -233,7 +237,7 @@ QGroupBox::title {
background: transparent;
subcontrol-origin: padding;
subcontrol-position: top left;
- padding: 8px;
+ padding: 4px 8px;
}
/* Text Fields */
@@ -335,90 +339,90 @@ QPushButton::menu-indicator {
/* Icons */
-QPushButton#listOptionsBtn {
+#listOptionsBtn {
/* Options button */
qproperty-icon: url(./Paper/Automata/dots.svg);
qproperty-iconSize: 16px;
padding-left: 2px;
}
-QPushButton#openFolderMenu {
+#openFolderMenu {
/* Open Folder button */
qproperty-icon: url(./Paper/Automata/folder.svg);
qproperty-iconSize: 14px;
padding-left: 4px;
}
-QPushButton#restoreModsButton,
-QPushButton#restoreButton {
+#restoreModsButton,
+#restoreButton {
/* Restore Backup buttons */
qproperty-icon: url(./Paper/Automata/restore.svg);
qproperty-iconSize: 14px;
}
-QPushButton#saveModsButton,
-QPushButton#saveButton {
+#saveModsButton,
+#saveButton {
/* Backup buttons */
qproperty-icon: url(./Paper/Automata/backup.svg);
qproperty-iconSize: 14px;
}
-QPushButton#bossButton {
+#bossButton {
/* Sort button */
qproperty-icon: url(./Paper/Automata/sort.svg);
qproperty-iconSize: 14px;
}
-QPushButton#linkButton {
+#linkButton {
/* Shortcuts button */
qproperty-icon: url(./Paper/Automata/shortcut.svg);
qproperty-iconSize: 14px;
}
-QPushButton#btnRefreshData,
-QPushButton#refreshButton {
+#btnRefreshData,
+#refreshButton {
/* Refresh buttons */
qproperty-icon: url(./Paper/Automata/refresh.svg);
qproperty-iconSize: 14px;
}
-QPushButton#endorseBtn {
+#endorseBtn {
/* Endorse button on the Nexus Info tab of the Information window */
qproperty-icon: url(./Paper/Automata/heart.svg);
qproperty-iconSize: 14px;
}
-QPushButton#clearCacheButton {
+#clearCacheButton {
/* Clear Cache button on the Nexus tab of the Settings window */
qproperty-icon: url(./Paper/Automata/cross.svg);
qproperty-iconSize: 14px;
}
-QToolButton#deactivateESP,
-QToolButton#activateESP {
+#deactivateESP,
+#activateESP {
/* activate and deactivate ESP buttons */
background: #DAD4BB;
}
-QToolButton:hover#deactivateESP,
-QToolButton:hover#activateESP {
+#deactivateESP:hover,
+#activateESP:hover {
/* activate and deactivate ESP buttons when moused-over */
background: #B4AF9A;
}
-QToolButton#deactivateESP {
+#deactivateESP {
/* icon for the deactivate ESP button */
qproperty-icon: url(./Paper/Automata/backup.svg);
}
-QToolButton#activateESP {
+#activateESP {
/* icon for the activate ESP button */
qproperty-icon: url(./Paper/Automata/restore-alt.svg);
}
/* Run button */
-QPushButton#startButton {
+#startButton {
/* Run button */
background: #4E4B42;
color: #DAD4BB;
@@ -427,7 +431,7 @@ QPushButton#startButton {
padding: 6px;
}
-QPushButton:hover#startButton {
+#startButton:hover {
/* Run button when moused-over*/
background: #B4AF9A;
color: #4E4B42;
@@ -504,7 +508,7 @@ QScrollBar::add-line:vertical {
}
QScrollBar::sub-line:vertical {
- /* scroll up button */
+ /* scroll up button */
background: #B4AF9A;
image: url(./Paper/Automata/Arrows/up.svg);
height: 23px;
@@ -517,29 +521,29 @@ 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 {
/* buttons and handles when moused-over */
background: #B4AF9A;
}
-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 {
/* buttons and handles when clicked */
background: #4E4B42;
}
-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 {
/* area on scroll bars where clicking it scrolls to where you clicked */
background: transparent;
@@ -671,7 +675,7 @@ QToolTip {
QProgressBar {
/* progress bars when downloading */
- background: #DAD4BB;
+ background: transparent;
text-align: center;
border: 0px;
margin: 0px 10px;
@@ -747,7 +751,7 @@ QTabBar QToolButton:disabled {
QLCDNumber {
/* LCD number on the Conflicts tab */
background: #DAD4BB;
- color: #CD664D;
+ color: #4E4B42;
border: none;
}
@@ -791,7 +795,7 @@ QRadioButton::indicator:unchecked:hover {
QTreeView::indicator:unchecked:selected,
QRadioButton::indicator:unchecked:selected {
/* a checkbox that is unchecked when clicked */
- image: url(./Paper/Automata/unchecked-alt.svg);
+ border: 2px solid #DAD4BB;
}
QTreeView::indicator:checked,
@@ -887,53 +891,100 @@ QSlider::handle:hover {
/* Downloads Tab */
-QWidget#downloadTab QAbstractScrollArea,
+#downloadTab QAbstractScrollArea,
DownloadListWidget {
/* background of the entire downloads tab */
- background: transparent;
+ background: #DAD4BB;
}
-DownloadListWidget QFrame,
-DownloadListWidgetCompact,
+DownloadListWidget QFrame,
+DownloadListWidgetCompact,
DownloadListWidgetCompact QLabel {
/* an entry on the Downloads tab */
background: #DAD4BB;
}
-DownloadListWidget QFrame#frame {
+DownloadListWidget#frame {
/* outer box of an entry on the Downloads tab */
border: 2px solid #DAD4BB;
}
-DownloadListWidget QLabel#installLabel {
+#installLabel {
/* installed/done label */
color: none;
}
-/* Compact Downloads View */
-
-DownloadListWidgetCompact,
+DownloadListWidgetCompact,
DownloadListWidgetCompact QLabel {
/* an entry on the Downloads tab */
background: #DAD4BB;
}
+/* New Downloads View */
+
+DownloadListWidget[downloadView=standard]::item {
+ /* Entries on the Standard Downloads View */
+ min-height: 44px;
+ margin: -16px 0;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ /* Entries on the Compact Downloads View */
+ min-height: 22px;
+ margin: -4px 0;
+}
+
+QProgressBar[downloadView=standard] {
+ /* Progress Bars on the Standard Downloads View */
+ background: transparent;
+ margin: 11px 0;
+}
+
+QProgressBar[downloadView=compact] {
+ /* Progress Bars on the Compact Downloads View */
+ background: transparent;
+}
+
/* Categories Filter */
-QPushButton#displayCategoriesBtn {
+#displayCategoriesBtn {
/* Filter button */
min-width: 12px;
}
-QTreeWidget#categoriesList {
+#categoriesList {
/* Categories panel */
min-width: 200px;
margin-bottom: 4px;
}
-QGroupBox#categoriesGroup {
+#categoriesGroup {
/* Categories group box */
- padding-bottom: 0px;
+ padding-bottom: 0;
+ margin-top: 3px;
+}
+
+/* Fixes */
+
+#executablesListBox {
+ /* Increase right margin of the select executables box */
+ margin-right: 8px;
+}
+
+#stepsStack QWidget {
+ /* Groupboxes on the FOMOD Installer Dialog */
+ background: #CDC8B0;
+ border: none;
+}
+
+#stepsStack QGroupBox {
+ /* Fix to reimplement styling for Groupboxes on the FOMOD Installer dialog */
+ border: 2px solid #4E4B42;
+}
+
+#activeModslabel, #activePluginsLabel {
+ /* Increase the left margin of the counters */
+ padding-left: 6px;
}
-/* For the Glory of Mankind */ \ No newline at end of file
+/* For the Glory of Mankind */
diff --git a/src/stylesheets/Paper Dark by 6788.qss b/src/stylesheets/Paper Dark by 6788.qss
index 9f4db66f..9abd8086 100644
--- a/src/stylesheets/Paper Dark by 6788.qss
+++ b/src/stylesheets/Paper Dark by 6788.qss
@@ -1,4 +1,4 @@
-/* v4.3 Paper Dark by 6788-00 */
+/* v5.0 Paper Dark by 6788-00 */
/* https://6788-00.tumblr.com/ */
/* Color Palette */
@@ -24,7 +24,7 @@ QWidget {
}
QWidget:disabled {
- /* disabled parts of the window like the update button when there are no updates */
+ /* disabled parts of the window like the update button when there are no updates */
background: #242424;
color: #808080;
}
@@ -39,7 +39,7 @@ QAbstractItemView {
}
QAbstractItemView::item {
- padding: 1px;
+ min-height: 22px;
}
QAbstractItemView::item:hover {
@@ -133,7 +133,7 @@ QToolButton:menu-indicator {
qproperty-icon: url(./Paper/Dark/Toolbar/settings.svg);
}
-#actionProblems {
+#actionNotifications {
qproperty-icon: url(./Paper/Dark/Toolbar/problems.svg);
}
@@ -190,14 +190,14 @@ QListView::item:hover {
/* a row on the saves tab when moused-over */
background: #3D3D3D;
color: #FFFFFF;
- padding: 2px 0px;
+ padding: 0;
}
QListView::item:selected {
/* a row on the saves tab when clicked */
background: #006868;
color: #FFFFFF;
- padding: 2px 0px;
+ padding: 0;
}
QTextEdit {
@@ -206,12 +206,6 @@ QTextEdit {
border-radius: 6px;
}
-QWebView {
- /* Nexus Info window */
- background: #141414;
- border-radius: 6px;
-}
-
/* Group Boxes */
QGroupBox {
@@ -500,7 +494,7 @@ QScrollBar::add-line:vertical {
}
QScrollBar::sub-line:vertical {
- /* scroll up button */
+ /* scroll up button */
background: #141414;
image: url(./Paper/Dark/Arrows/up.svg);
height: 23px;
@@ -513,29 +507,29 @@ 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 {
/* buttons and handles when moused-over */
background: #006868;
}
-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 {
/* buttons and handles when clicked */
background: #006868;
}
-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 {
/* area on scroll bars where clicking it scrolls to where you clicked */
background: transparent;
@@ -682,6 +676,7 @@ QToolTip {
QProgressBar {
/* progress bars when downloading */
background: #141414;
+ color: #FFFFFF;
text-align: center;
border: 2px solid #242424;
border-radius: 6px;
@@ -705,10 +700,6 @@ QTabWidget::pane {
border-radius: 10px;
}
-QTabWidget QAbstractScrollArea::item {
- padding: 2px;
-}
-
QTabWidget::tab-bar {
/* tabs */
alignment: center;
@@ -781,6 +772,7 @@ QLCDNumber {
background: #141414;
color: #006868;
border-radius: 6px;
+
}
/* Tables (Configure Mod Categories) */
@@ -912,15 +904,15 @@ QSlider::handle:hover {
background: #3D3D3D;
}
-/* Downloads Tab */
+/* Pre-v2.1.7 Downloads Tab */
#downloadTab QAbstractScrollArea {
/* background of the entire downloads tab */
- background: #242424;
+ background: #141414;
}
-DownloadListWidget QFrame,
-DownloadListWidgetCompact,
+DownloadListWidget QFrame,
+DownloadListWidgetCompact,
DownloadListWidgetCompact QLabel {
/* an entry on the Downloads tab */
background: #141414;
@@ -936,14 +928,43 @@ DownloadListWidget#frame {
color: none;
}
-/* Compact Downloads View */
-
-DownloadListWidgetCompact,
+DownloadListWidgetCompact,
DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab */
+ /* an entry on the Downloads tab in Compact View */
background: #141414;
}
+/* New Downloads View */
+
+DownloadListWidget[downloadView=standard]::item {
+ /* Entries on the Standard Downloads View */
+ min-height: 44px;
+ margin: -16px 0;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ /* Entries on the Compact Downloads View */
+ min-height: 22px;
+ margin: -4px 0;
+}
+
+QProgressBar[downloadView=standard] {
+ /* Progress Bars on the Standard Downloads View */
+ background: transparent;
+ margin: 11px 0;
+}
+
+QProgressBar[downloadView=standard]::chunk,
+QProgressBar[downloadView=compact]::chunk {
+ /* The Loading Portion of Progress Bars on the Downloads View */
+ border-right: 2px solid #242424;
+}
+
+QProgressBar[downloadView=compact] {
+ /* Progress Bars on the Compact Downloads View */
+ background: transparent;
+}
+
/* Categories Filter */
#displayCategoriesBtn {
@@ -964,7 +985,28 @@ DownloadListWidgetCompact QLabel {
/* Fixes */
+#executablesListBox {
+ /* Increase right margin of the select executables box */
+ margin-right: 8px;
+}
+
#executablesListBox::item {
/* fixes the black text problem on the Modify Executables window */
color: #D3D3D3;
-} \ No newline at end of file
+}
+
+#stepsStack QWidget {
+ /* Groupboxes on the FOMOD Installer Dialog */
+ border: none;
+}
+
+#stepsStack QGroupBox {
+ /* Fix to reimplement styling for Groupboxes on the FOMOD Installer dialog */
+ border: 2px solid #141414;
+ border-radius: 10px;
+}
+
+#activeModslabel, #activePluginsLabel {
+ /* Increase the left margin of the counters */
+ padding-left: 6px;
+}
diff --git a/src/stylesheets/Paper Light by 6788.qss b/src/stylesheets/Paper Light by 6788.qss
index 36585291..4aad55a8 100644
--- a/src/stylesheets/Paper Light by 6788.qss
+++ b/src/stylesheets/Paper Light by 6788.qss
@@ -1,4 +1,4 @@
-/* v4.3 Paper Light by 6788-00 */
+/* v5.0 Paper Light by 6788-00 */
/* https://6788-00.tumblr.com/ */
/* Color Palette */
@@ -24,7 +24,7 @@ QWidget {
}
QWidget:disabled {
- /* disabled parts of the window like the update button when there are no updates */
+ /* disabled parts of the window like the update button when there are no updates */
background: #EBEBEB;
color: #808080;
}
@@ -38,6 +38,10 @@ QAbstractItemView {
selection-color: #FFFFFF;
}
+QAbstractItemView::item {
+ min-height: 22px;
+}
+
QAbstractItemView::item:hover {
/* rows on left and right pane when moused-over */
background: #C2C2C2;
@@ -128,7 +132,7 @@ QToolButton:menu-indicator {
qproperty-icon: url(./Paper/Light/Toolbar/settings.svg);
}
-#actionProblems {
+#actionNotifications {
qproperty-icon: url(./Paper/Light/Toolbar/problems.svg);
}
@@ -187,22 +191,17 @@ QListView {
border-radius: 6px;
}
-QListView::item {
- /* Increases the line height of the Saves tab */
- padding: 2px 0px;
-}
-
QListView::item:hover {
/* uncertain, assumed: rows on the saves window when moused-over */
background: #C2C2C2;
- padding: 2px 0px;
+ padding: 0;
}
QListView::item:selected {
/*uncertain, assumed: rows on the saves window when clicked */
background: #008484;
color: #FFFFFF;
- padding: 2px 0px;
+ padding: 0;
}
QTextEdit {
@@ -211,12 +210,6 @@ QTextEdit {
border-radius: 6px;
}
-QWebView {
- /* Nexus Info window */
- background: #FFFFFF;
- border-radius: 6px;
-}
-
/* Group Boxes */
QGroupBox {
@@ -504,7 +497,7 @@ QScrollBar::add-line:vertical {
}
QScrollBar::sub-line:vertical {
- /* scroll up button */
+ /* scroll up button */
background: #FFFFFF;
image: url(./Paper/Light/Arrows/up.svg);
height: 23px;
@@ -517,29 +510,29 @@ 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 {
/* buttons and handles when moused-over */
background: #008484;
}
-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 {
/* buttons and handles when clicked */
background: #008484;
}
-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 {
/* area on scroll bars where clicking it scrolls to where you clicked */
background: transparent;
@@ -708,10 +701,6 @@ QTabWidget::pane {
border-radius: 10px;
}
-QTabWidget QAbstractScrollArea::item {
- padding: 2px;
-}
-
QTabWidget::tab-bar {
/* tabs */
alignment: center;
@@ -922,15 +911,15 @@ QSlider::handle:hover {
background: #008484;
}
-/* Downloads Tab */
+/* Pre-v2.1.7 Downloads Tab */
#downloadTab QAbstractScrollArea {
/* background of the entire downloads tab */
- background: #EBEBEB;
+ background: #FFFFFF;
}
-DownloadListWidget QFrame,
-DownloadListWidgetCompact,
+DownloadListWidget QFrame,
+DownloadListWidgetCompact,
DownloadListWidgetCompact QLabel {
/* an entry on the Downloads tab */
background: #FFFFFF;
@@ -946,14 +935,24 @@ DownloadListWidget #frame {
color: none;
}
-/* Compact Downloads View */
-
-DownloadListWidgetCompact,
+DownloadListWidgetCompact,
DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab */
+ /* an entry on the Downloads tab in Compacts View*/
background: #FFFFFF;
}
+/* New Downloads View */
+
+DownloadListWidget[downloadView=standard]::item {
+ /* an entry on the Downloads view */
+ min-height: 44px;
+}
+
+QProgressBar[downloadView=standard] {
+ /* a progress bar on the Downloads view */
+ margin: 11px 0;
+}
+
/* Categories Filter */
#displayCategoriesBtn {
@@ -970,4 +969,27 @@ DownloadListWidgetCompact QLabel {
#categoriesGroup {
/* Categories group box */
padding-bottom: 0px;
-} \ No newline at end of file
+}
+
+/* Fixes */
+
+#executablesListBox {
+ /* Increase right margin of the select executables box */
+ margin-right: 8px;
+}
+
+#stepsStack QWidget {
+ /* Groupboxes on the FOMOD Installer Dialog */
+ border: none;
+}
+
+#stepsStack QGroupBox {
+ /* Fix to reimplement styling for Groupboxes on the FOMOD Installer dialog */
+ border: 2px solid #EBEBEB;
+ border-radius: 10px;
+}
+
+#activeModslabel, #activePluginsLabel {
+ /* Increase the left margin of the counters */
+ padding-left: 6px;
+}
diff --git a/src/stylesheets/Paper/Dark/check.svg b/src/stylesheets/Paper/Dark/check.svg
index 101ee45e..2d113294 100644
--- a/src/stylesheets/Paper/Dark/check.svg
+++ b/src/stylesheets/Paper/Dark/check.svg
@@ -17,7 +17,7 @@
inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
inkscape:export-xdpi="307.20001"
inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="check.svg">
<defs
id="defs2" />
@@ -28,9 +28,9 @@
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
- inkscape:zoom="24.357491"
- inkscape:cx="1.6266143"
- inkscape:cy="5.0770231"
+ inkscape:zoom="11.625"
+ inkscape:cx="-3.537598"
+ inkscape:cy="-2.9645926"
inkscape:document-units="px"
inkscape:current-layer="layer25"
showgrid="false"
@@ -42,7 +42,7 @@
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
- showguides="true"
+ showguides="false"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1027"
diff --git a/src/stylesheets/Paper/Dark/highlight.svg b/src/stylesheets/Paper/Dark/highlight.svg
new file mode 100644
index 00000000..9dee51d8
--- /dev/null
+++ b/src/stylesheets/Paper/Dark/highlight.svg
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="128"
+ height="64"
+ viewBox="0 0 128 64"
+ version="1.1"
+ id="svg8"
+ inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
+ inkscape:export-xdpi="307.20001"
+ inkscape:export-ydpi="307.20001"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="highlight.svg">
+ <defs
+ id="defs2">
+ <meshgradient
+ inkscape:collect="always"
+ id="meshgradient3955"
+ gradientUnits="userSpaceOnUse"
+ x="0"
+ y="-44">
+ <meshrow
+ id="meshrow18393">
+ <meshpatch
+ id="meshpatch18395">
+ <stop
+ path="c 4.96082,0 9.92164,0 14.8825,0"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18397" />
+ <stop
+ path="c 0,10.6667 0,21.3333 0,32"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18399" />
+ <stop
+ path="c -4.96082,1.77636e-015 -9.92165,1.77636e-015 -14.8825,1.77636e-015"
+ style="stop-color:#006868;stop-opacity:1"
+ id="stop18401" />
+ <stop
+ path="c 0,-10.6667 0,-21.3333 0,-32"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18403" />
+ </meshpatch>
+ <meshpatch
+ id="meshpatch18405">
+ <stop
+ path="c 16.3725,0 32.7451,0 49.1175,0"
+ id="stop18407" />
+ <stop
+ path="c 0,10.6667 0,21.3333 0,32"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18409" />
+ <stop
+ path="c -16.3725,0 -32.745,0 -49.1175,0"
+ style="stop-color:#006868;stop-opacity:1"
+ id="stop18411" />
+ </meshpatch>
+ <meshpatch
+ id="meshpatch18413">
+ <stop
+ path="c 17.6536,0 35.3073,0 52.9609,0"
+ id="stop18415" />
+ <stop
+ path="c 0,10.6667 0,21.3333 0,32"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18417" />
+ <stop
+ path="c -17.6536,0 -35.3073,0 -52.9609,0"
+ style="stop-color:#006868;stop-opacity:1"
+ id="stop18419" />
+ </meshpatch>
+ <meshpatch
+ id="meshpatch18421">
+ <stop
+ path="c 3.67971,0 7.35941,0 11.0391,0"
+ id="stop18423" />
+ <stop
+ path="c 0,10.6667 0,21.3333 0,32"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18425" />
+ <stop
+ path="c -3.6797,0 -7.35941,0 -11.0391,0"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18427" />
+ </meshpatch>
+ </meshrow>
+ <meshrow
+ id="meshrow18429">
+ <meshpatch
+ id="meshpatch18431">
+ <stop
+ path="c 0,10.6667 0,21.3333 0,32"
+ id="stop18433" />
+ <stop
+ path="c -4.96082,-3.55271e-015 -9.92165,-3.55271e-015 -14.8825,-3.55271e-015"
+ style="stop-color:#007d7d;stop-opacity:1"
+ id="stop18435" />
+ <stop
+ path="c 0,-10.6667 0,-21.3333 0,-32"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18437" />
+ </meshpatch>
+ <meshpatch
+ id="meshpatch18439">
+ <stop
+ path="c 0,10.6667 0,21.3333 0,32"
+ id="stop18441" />
+ <stop
+ path="c -16.3725,0 -32.745,0 -49.1175,0"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18443" />
+ </meshpatch>
+ <meshpatch
+ id="meshpatch18445">
+ <stop
+ path="c 7.38594e-006,10.6667 0.000207035,21.333 2.21578e-005,32"
+ id="stop18447" />
+ <stop
+ path="c -17.6536,-4.55705e-005 -35.3071,-0.000147247 -52.9609,1.82555e-005"
+ style="stop-color:#007f7f;stop-opacity:1"
+ id="stop18449" />
+ </meshpatch>
+ <meshpatch
+ id="meshpatch18451">
+ <stop
+ path="c 0,10.6667 0.000232338,21.3329 0,32"
+ id="stop18453" />
+ <stop
+ path="c -3.6797,-1.69572e-006 -7.3594,-8.75677e-006 -11.0391,-1.82555e-005"
+ style="stop-color:#008484;stop-opacity:1"
+ id="stop18455" />
+ </meshpatch>
+ </meshrow>
+ </meshgradient>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#505050"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.2410685"
+ inkscape:cx="55.924572"
+ inkscape:cy="37.651744"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer25"
+ showgrid="false"
+ units="px"
+ borderlayer="true"
+ inkscape:showpageshadow="false"
+ inkscape:snap-page="true"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1027"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:snap-center="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:measure-start="18.8975,12.498"
+ inkscape:measure-end="20,12.498"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-paths="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-global="true" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer25"
+ inkscape:label="Content"
+ transform="translate(0,44)">
+ <rect
+ style="opacity:1;fill:url(#meshgradient3955);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:markers stroke fill"
+ id="rect3848"
+ width="128"
+ height="64"
+ x="0"
+ y="-44" />
+ </g>
+</svg>
diff --git a/src/stylesheets/Parchment v1.1 by Bob.qss b/src/stylesheets/Parchment v1.1 by Bob.qss
new file mode 100644
index 00000000..da3546d1
--- /dev/null
+++ b/src/stylesheets/Parchment v1.1 by Bob.qss
@@ -0,0 +1,587 @@
+/* v1.1 Parchment by Bob */
+
+
+/* Special Thanks */
+
+/* Parchment is based on Paper Themes for Mod Organizer 2 by 6788
+https://www.nexusmods.com/skyrim/mods/64439
+https://6788-00.tumblr.com/
+
+and
+
+*************************************
+ Skyrim
+****************************************
+ Author: chintsu_kun
+ Version: 1.1.2
+ Licence: GNU General Public License v3.0 (https://www.gnu.org/licenses/gpl-3.0.en.html)
+ Url: http://www.nexusmods.com/skyrim/mods/73817/?
+
+****************************************
+
+For the Checkbox Coding and for the Picture Files which I Edited in Paint 3D */
+
+/* Main Window */
+
+QWidget {
+ background: #DBD399;
+ color: #000000;
+}
+
+QWidget:disabled {
+ background: #DBD399;
+ color: #baae94;
+}
+
+QMainWindow::separator {
+ border: 0px;
+}
+
+QAbstractItemView {
+ background: #F7F6CF;
+ alternate-background-color: #EFE9BA;
+ show-decoration-selected: 1;
+ selection-background-color: #0CA6FF;
+ selection-color: #F7F6CF;
+}
+
+QAbstractItemView::item:hover {
+ background: #008F8F;
+ color: #F7F6CF;
+}
+
+QAbstractItemView::item:selected {
+ background: #0CA6FF;
+ color: #F7F6CF;
+}
+
+QAbstractScrollArea::corner {
+ background: #F7F6CF;
+ border: 2px solid #DBD399;
+ border-bottom-right-radius: 6px;
+ margin: 0px -2px -2px 0px;
+}
+
+/* Toolbar */
+
+QToolBar {
+ background: #DBD399;
+ border: 1px solid #DBD399;
+}
+
+QToolBar::separator {
+ background: #DBD399;
+}
+
+QToolButton {
+ background: #F7F6CF;
+ padding: 4px 6px;
+ border-radius: 6px;
+ margin: 4px 4px 0px 4px;
+}
+
+QToolButton:hover {
+ background: #008F8F;
+}
+
+QToolButton:pressed {
+ background: #0CA6FF;
+}
+
+/* Left Pane & File Trees */
+
+QTreeView {
+ border-radius: 6px;
+}
+
+QTreeView::branch:hover {
+ background: #008F8F;
+ color: #F7F6CF;
+}
+
+QTreeView::branch:selected {
+ background: #0CA6FF;
+ color: #F7F6CF;
+}
+
+QTreeView::item:selected {
+ background: #0CA6FF;
+ color: #F7F6CF;
+}
+
+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 {
+ image: url(:/stylesheet/branch-open.png);
+ border: 0px;
+}
+
+QListView {
+ border-radius: 6px;
+}
+
+QListView::item:hover {
+ background: #008F8F;
+ color: #F7F6CF;
+}
+
+QListView::item:selected {
+ background: #0CA6FF;
+ color: #F7F6CF;
+}
+
+QTextEdit {
+ background: #F7F6CF;
+ border-radius: 6px;
+}
+
+QWebView {
+ background: #F7F6CF;
+ border-radius: 6px;
+}
+
+/* Group Boxes */
+
+QGroupBox {
+ padding: 24px 4px;
+ border: 2px solid #F7F6CF;
+ border-radius: 10px;
+}
+
+QGroupBox::title {
+ subcontrol-origin: padding;
+ subcontrol-position: top left;
+ padding: 8px;
+}
+
+/* Checkboxes and Radio buttons common #QCheckBox, #QRadioButton */
+/*common*/
+QGroupBox::indicator,
+QTreeView::indicator,
+
+QCheckBox::indicator {
+ background-color: transparent;
+ border: none;
+ width: 14px;
+ height: 14px;
+}
+
+QGroupBox::indicator:checked, QGroupBox::indicator:indeterminate,
+QTreeView::indicator:checked,
+QTreeView::indicator:indeterminate,
+QCheckBox::indicator:checked,
+
+QCheckBox::indicator:indeterminate {
+ image: url(./Parchment/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(./Parchment/checkbox-checked-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(./Parchment/checkbox-checked-disabled.png);
+}
+
+ QGroupBox::indicator:unchecked,
+ QTreeView::indicator:unchecked,
+
+ QCheckBox::indicator:unchecked {
+ image: url(./Parchment/checkbox.png);
+}
+
+ QGroupBox::indicator:unchecked:hover,
+ QTreeView::indicator:unchecked:hover,
+
+ QCheckBox::indicator:unchecked:hover {
+ image: url(./Parchment/checkbox-hover.png);
+}
+
+ QGroupBox::indicator:unchecked:disabled,
+ QTreeView::indicator:unchecked:disabled,
+
+ QCheckBox::indicator:unchecked:disabled {
+ image: url(./Parchment/checkbox-disabled.png);
+}
+
+/* Search Boxes */
+
+QLineEdit {
+ background: #F7F6CF;
+ min-height: 14px;
+ padding: 2px;
+ border: 2px solid #F7F6CF;
+ border-radius: 6px;
+ margin-top: 3px;
+}
+
+QLineEdit:hover {
+ border: 2px solid #008F8F;
+}
+
+/* Most Dropdowns */
+
+QComboBox {
+ background: #F7F6CF;
+ min-height: 20px;
+ padding-left: 5px;
+ border: 2px solid #F7F6CF;
+ border-radius: 6px;
+ margin: 3px 0px 1px 0px;
+}
+
+QComboBox:hover {
+ border: 2px solid #008F8F;
+}
+
+QComboBox:on {
+ background: #0CA6FF;
+ color: #F7F6CF;
+ border: 2px solid #0CA6FF;
+}
+
+QComboBox::drop-down {
+ width: 20px;
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ border: none;
+}
+
+QComboBox QAbstractItemView {
+ border: 0px;
+}
+
+QComboBox::down-arrow {
+ image: url(:/stylesheet/combobox-down.png);
+}
+
+/* Most Buttons */
+
+QPushButton {
+ background: #F7F6CF;
+ color: #000000;
+ min-height: 28px;
+ padding: 4px 22px;
+ border-radius: 9px;
+}
+
+QPushButton:hover {
+ background: #008F8F;
+ color: #F7F6CF;
+}
+
+QPushButton:pressed {
+ background: #0CA6FF;
+ color: #F7F6CF;
+}
+
+QPushButton:checked {
+ background: #0CA6FF;
+ color: #0CA6FF;
+ margin: 4px;
+}
+
+/* Scroll Bars */
+
+/* Horizontal */
+
+QScrollBar:horizontal {
+ background: #F7F6CF;
+ height: 20px;
+ border: 2px solid #DBD399;
+ margin: 0px 23px -2px 23px;
+}
+
+QScrollBar::handle:horizontal {
+ background: #DBD399;
+ min-width: 32px;
+ border-radius: 6px;
+ margin: 2px;
+}
+
+QScrollBar::add-line:horizontal {
+ background: #F7F6CF;
+ width: 23px;
+ subcontrol-position: right;
+ subcontrol-origin: margin;
+ border: 2px solid #DBD399;
+ margin: 0px -2px -2px 0px;
+}
+
+QScrollBar::sub-line:horizontal {
+ background: #F7F6CF;
+ width: 23px;
+ subcontrol-position: left;
+ subcontrol-origin: margin;
+ border: 2px solid #DBD399;
+ border-bottom-left-radius: 6px;
+ margin: 0px 0px -2px -2px;
+}
+
+/* Vertical */
+
+QScrollBar:vertical {
+ background: #F7F6CF;
+ width: 20px;
+ border: 2px solid #DBD399;
+ margin: 23px -2px 23px 0px;
+}
+
+QScrollBar::handle:vertical {
+ background: #DBD399;
+ min-height: 32px;
+ border-radius: 6px;
+ margin: 2px;
+}
+
+QScrollBar::add-line:vertical {
+ background: #F7F6CF;
+ height: 23px;
+ subcontrol-position: bottom;
+ subcontrol-origin: margin;
+ border: 2px solid #DBD399;
+ border-bottom-right-radius: 6px;
+ margin: 0px -2px -2px 0px;
+}
+
+QScrollBar::sub-line:vertical {
+ background: #F7F6CF;
+ height: 23px;
+ subcontrol-position: top;
+ subcontrol-origin: margin;
+ border: 2px solid #DBD399;
+ border-top-right-radius: 6px;
+ margin: -2px -2px 0px 0px;
+}
+
+/* Combined */
+
+QScrollBar::handle:horizontal:hover,
+QScrollBar::handle: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: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::sub-page:vertical {
+ background: transparent;
+}
+
+QScrollBar::up-arrow:vertical,
+QScrollBar::right-arrow:horizontal,
+QScrollBar::down-arrow:vertical,
+QScrollBar::left-arrow:horizontal {
+ height: 1px;
+ width: 1px;
+ border: 1px solid #DBD399;
+}
+
+/* Header Rows */
+
+QHeaderView {
+ background: #DBD399;
+}
+
+QHeaderView::section {
+ background: #F7F6CF;
+ color: #000000;
+ height: 22px;
+ padding: 0px 5px;
+ border: 0px;
+ border-bottom: 2px solid #DBD399;
+ border-right: 2px solid #DBD399;
+}
+
+QHeaderView::section:first {
+ border-top-left-radius: 6px;
+}
+
+QHeaderView::section:last {
+ border-right: 0px;
+ border-top-right-radius: 6px;
+}
+
+QHeaderView::section:hover {
+ background: #008F8F;
+ color: #F7F6CF;
+}
+
+QHeaderView::down-arrow {
+ padding-right: 4px;
+ height: 10px;
+ width: 10px;
+}
+
+/* Context Menus, Toolbar Dropdowns, & Tooltips */
+
+QMenu {
+ background: #F7F6CF;
+ selection-color: #F7F6CF;
+ border: 0px;
+}
+
+QMenu::item {
+ background: #F7F6CF;
+ selection-background-color: #008F8F;
+ padding: 4px 20px;
+}
+
+QMenu::item:selected {
+ background: #008F8F;
+ color: #F7F6CF;
+}
+
+QMenu::item:disabled {
+ background: #F7F6CF;
+ color: #444444;
+}
+
+QMenu::separator {
+ background: #DBD399;
+ height: 2px;
+}
+
+QMenu::icon {
+ margin: 1px;
+}
+
+QToolTip {
+ background: #F7F6CF;
+ color: #000000;
+ padding: 1px;
+ border: 0px;
+}
+
+/* Progress Bars (Downloads) */
+
+QProgressBar {
+ background: #F7F6CF;
+ text-align: center;
+ border: 0px;
+ border-radius: 6px;
+ margin: 0px 10px;
+}
+
+QProgressBar::chunk {
+ background: #008F8F;
+}
+
+/* Right Pane and Tab Bars */
+
+QTabWidget::pane {
+ top: 1px;
+ padding: 2px 2px 10px 2px;
+ border: 2px solid #F7F6CF;
+ border-radius: 10px;
+}
+
+QTabWidget::tab-bar {
+ alignment: center;
+}
+
+QTabBar::tab {
+ background: #F7F6CF;
+ color: #000000;
+ padding: 4px 1em;
+ border: 1px solid #DBD399;
+ border-top: 0px;
+ border-bottom: 0px;
+}
+
+QTabBar::tab:!selected {
+ background: #F7F6CF;
+ color: #000000;
+}
+
+QTabBar::tab:disabled {
+ background: #DBD399;
+ color: #444444;
+}
+
+QTabBar::tab:selected {
+ color: #0CA6FF;
+
+}
+
+QTabBar::tab:!selected:hover {
+ background: #0CA6FF;
+ color: #F7F6CF;
+}
+
+QTabBar::tab:first {
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+}
+
+QTabBar::tab:last {
+ border-top-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+}
+
+QTabBar QToolButton {
+ background: #008F8F;
+ padding: 1px;
+ border-radius: 6px;
+ margin: 1px;
+}
+
+QTabBar QToolButton:disabled {
+ background: transparent;
+}
+
+/* Sliders (Configurator) */
+
+/* QSlider::groove:horizontal {
+ background: #F7F6CF;
+ height: 1px;
+ border: 1px solid #F7F6CF;
+}
+
+QSlider::handle:horizontal {
+ background: #008F8F;
+ width: 10px;
+ border: 2px solid #008F8F;
+ border-radius: 6px;
+ margin: -10px 0px;
+}
+
+QSlider::handle:horizontal:hover {
+ background: #0CA6FF;
+ border: 2px solid #0CA6FF;
+} */
+
+/* Tables (Configure Mod Categories) */
+
+QTableView {
+ gridline-color: #DBD399;
+ border: 0px;
+} \ No newline at end of file
diff --git a/src/stylesheets/Parchment/checkbox-alt-checked.png b/src/stylesheets/Parchment/checkbox-alt-checked.png
new file mode 100644
index 00000000..b2f32952
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-alt-checked.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-alt-unchecked-hover.png b/src/stylesheets/Parchment/checkbox-alt-unchecked-hover.png
new file mode 100644
index 00000000..bf4cb027
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-alt-unchecked-hover.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-alt-unchecked.png b/src/stylesheets/Parchment/checkbox-alt-unchecked.png
new file mode 100644
index 00000000..35073ad7
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-alt-unchecked.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-checked-disabled.png b/src/stylesheets/Parchment/checkbox-checked-disabled.png
new file mode 100644
index 00000000..21e7e057
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-checked-disabled.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-checked-hover.png b/src/stylesheets/Parchment/checkbox-checked-hover.png
new file mode 100644
index 00000000..6603a99e
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-checked-hover.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-checked.png b/src/stylesheets/Parchment/checkbox-checked.png
new file mode 100644
index 00000000..0f098137
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-checked.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-disabled.png b/src/stylesheets/Parchment/checkbox-disabled.png
new file mode 100644
index 00000000..d1d5a887
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-disabled.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox-hover.png b/src/stylesheets/Parchment/checkbox-hover.png
new file mode 100644
index 00000000..92df6af4
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox-hover.png
Binary files differ
diff --git a/src/stylesheets/Parchment/checkbox.png b/src/stylesheets/Parchment/checkbox.png
new file mode 100644
index 00000000..e02aa21d
--- /dev/null
+++ b/src/stylesheets/Parchment/checkbox.png
Binary files differ
diff --git a/src/stylesheets/dark.qss b/src/stylesheets/dark.qss
index d5a7aedf..5bbb0f0c 100644
--- a/src/stylesheets/dark.qss
+++ b/src/stylesheets/dark.qss
@@ -257,7 +257,6 @@ QProgressBar
QProgressBar::chunk
{
background-color: #427683;
- width: 20px;
}
QTabBar::tab
@@ -366,4 +365,12 @@ QTreeView::branch:open:has-children:has-siblings
DownloadListWidget QLabel#installLabel {
color: none;
+}
+
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
} \ No newline at end of file
diff --git a/src/stylesheets/dracula.qss b/src/stylesheets/dracula.qss
index 99cf918d..385fca16 100644
--- a/src/stylesheets/dracula.qss
+++ b/src/stylesheets/dracula.qss
@@ -394,4 +394,32 @@ SaveGameInfoWidget {
DownloadListWidget QLabel#installLabel {
color: none;
-} \ No newline at end of file
+}
+
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
+DownloadListWidget::item:hover {
+ padding: 0px;
+}
+
+DownloadListWidget::item:selected {
+ padding: 0px;
+}
+
+QProgressBar
+{
+ border: 2px solid grey;
+ border-radius: 5px;
+ text-align: center;
+}
+
+QProgressBar::chunk
+{
+ background-color: #427683;
+}
diff --git a/src/stylesheets/skyrim.qss b/src/stylesheets/skyrim.qss
index e4d87499..d36eac57 100644
--- a/src/stylesheets/skyrim.qss
+++ b/src/stylesheets/skyrim.qss
@@ -534,7 +534,7 @@ QProgressBar {
background-color: transparent;
color: transparent;
height: 14px;
- margin: 0 10px;
+ margin: 0 0px;
border-width: 4px 21px;
border-style: solid;
border-color: transparent;
@@ -542,6 +542,14 @@ QProgressBar {
QProgressBar::chunk {
background: url(./skyrim/progress-bar-chunk.png) center center repeat-x qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #95BED9, stop:0.78781 #6EB9CE); }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 15px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border: none;
diff --git a/src/stylesheets/vs15 Dark-Green.qss b/src/stylesheets/vs15 Dark-Green.qss
index 5427b38a..10f923cb 100644
--- a/src/stylesheets/vs15 Dark-Green.qss
+++ b/src/stylesheets/vs15 Dark-Green.qss
@@ -532,8 +532,8 @@ QHeaderView::up-arrow {
image: url(./vs15/sort-asc.png);
margin-bottom: -37px; }
-QTreeView#downloadView QHeaderView::up-arrow {
- margin-bottom: -32px; }
+DownloadListWidget QHeaderView::up-arrow {
+ margin-bottom: -47px; }
QHeaderView::down-arrow {
image: url(./vs15/sort-desc.png);
@@ -599,11 +599,19 @@ QProgressBar {
text-align: center;
border-style: solid;
border-width: 1px;
- margin: 0 10px; }
+ margin: 0 0px; }
QProgressBar::chunk {
background: #06B025; }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Orange.qss b/src/stylesheets/vs15 Dark-Orange.qss
index fe65ece0..bbde1f82 100644
--- a/src/stylesheets/vs15 Dark-Orange.qss
+++ b/src/stylesheets/vs15 Dark-Orange.qss
@@ -533,8 +533,8 @@ QHeaderView::up-arrow {
image: url(./vs15/sort-asc.png);
margin-bottom: -37px; }
-QTreeView#downloadView QHeaderView::up-arrow {
- margin-bottom: -32px; }
+DownloadListWidget QHeaderView::up-arrow {
+ margin-bottom: -47px; }
QHeaderView::down-arrow {
image: url(./vs15/sort-desc.png);
@@ -600,11 +600,19 @@ QProgressBar {
text-align: center;
border-style: solid;
border-width: 1px;
- margin: 0 10px; }
+ margin: 0 0px; }
QProgressBar::chunk {
background: #06B025; }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Purple.qss b/src/stylesheets/vs15 Dark-Purple.qss
index eb2e8b82..faad7297 100644
--- a/src/stylesheets/vs15 Dark-Purple.qss
+++ b/src/stylesheets/vs15 Dark-Purple.qss
@@ -533,8 +533,8 @@ QHeaderView::up-arrow {
image: url(./vs15/sort-asc.png);
margin-bottom: -37px; }
-QTreeView#downloadView QHeaderView::up-arrow {
- margin-bottom: -32px; }
+DownloadListWidget QHeaderView::up-arrow {
+ margin-bottom: -47px; }
QHeaderView::down-arrow {
image: url(./vs15/sort-desc.png);
@@ -600,11 +600,19 @@ QProgressBar {
text-align: center;
border-style: solid;
border-width: 1px;
- margin: 0 10px; }
+ margin: 0px 0px; }
QProgressBar::chunk {
background: #06B025; }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Red.qss b/src/stylesheets/vs15 Dark-Red.qss
index ee363e0b..2ffcff68 100644
--- a/src/stylesheets/vs15 Dark-Red.qss
+++ b/src/stylesheets/vs15 Dark-Red.qss
@@ -533,8 +533,8 @@ QHeaderView::up-arrow {
image: url(./vs15/sort-asc.png);
margin-bottom: -37px; }
-QTreeView#downloadView QHeaderView::up-arrow {
- margin-bottom: -32px; }
+DownloadListWidget QHeaderView::up-arrow {
+ margin-bottom: -47px; }
QHeaderView::down-arrow {
image: url(./vs15/sort-desc.png);
@@ -600,11 +600,19 @@ QProgressBar {
text-align: center;
border-style: solid;
border-width: 1px;
- margin: 0 10px; }
+ margin: 0 0px; }
QProgressBar::chunk {
background: #06B025; }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Yellow.qss b/src/stylesheets/vs15 Dark-Yellow.qss
index 08dbe758..24afe005 100644
--- a/src/stylesheets/vs15 Dark-Yellow.qss
+++ b/src/stylesheets/vs15 Dark-Yellow.qss
@@ -533,8 +533,8 @@ QHeaderView::up-arrow {
image: url(./vs15/sort-asc.png);
margin-bottom: -37px; }
-QTreeView#downloadView QHeaderView::up-arrow {
- margin-bottom: -32px; }
+DownloadListWidget QHeaderView::up-arrow {
+ margin-bottom: -47px; }
QHeaderView::down-arrow {
image: url(./vs15/sort-desc.png);
@@ -600,11 +600,19 @@ QProgressBar {
text-align: center;
border-style: solid;
border-width: 1px;
- margin: 0 10px; }
+ margin: 0 0px; }
QProgressBar::chunk {
background: #06B025; }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark.qss b/src/stylesheets/vs15 Dark.qss
index 8c4d354e..6a551775 100644
--- a/src/stylesheets/vs15 Dark.qss
+++ b/src/stylesheets/vs15 Dark.qss
@@ -532,8 +532,8 @@ QHeaderView::up-arrow {
image: url(./vs15/sort-asc.png);
margin-bottom: -37px; }
-QTreeView#downloadView QHeaderView::up-arrow {
- margin-bottom: -32px; }
+DownloadListWidget QHeaderView::up-arrow {
+ margin-bottom: -47px; }
QHeaderView::down-arrow {
image: url(./vs15/sort-desc.png);
@@ -599,11 +599,19 @@ QProgressBar {
text-align: center;
border-style: solid;
border-width: 1px;
- margin: 0 10px; }
+ margin: 0 0px; }
QProgressBar::chunk {
background: #06B025; }
+DownloadListWidget[downloadView=standard]::item {
+ padding: 16px;
+}
+
+DownloadListWidget[downloadView=compact]::item {
+ padding: 4px;
+}
+
/* Right Pane and Tab Bars #QTabWidget, #QTabBar */
QTabWidget::pane {
border-color: #3F3F46;
diff --git a/src/syncoverwritedialog.cpp b/src/syncoverwritedialog.cpp
index f03e29c5..4ee4716e 100644
--- a/src/syncoverwritedialog.cpp
+++ b/src/syncoverwritedialog.cpp
@@ -86,7 +86,7 @@ void SyncOverwriteDialog::readTree(const QString &path, DirectoryEntry *director
if (subDir != nullptr) {
readTree(fileInfo.absoluteFilePath(), subDir, newItem);
} else {
- qCritical("no directory structure for %s?", file.toUtf8().constData());
+ qCritical("no directory structure for %s?", qUtf8Printable(file));
delete newItem;
newItem = nullptr;
}
@@ -98,8 +98,8 @@ void SyncOverwriteDialog::readTree(const QString &path, DirectoryEntry *director
bool ignore;
int origin = entry->getOrigin(ignore);
addToComboBox(combo, ToQString(m_DirectoryStructure->getOriginByID(origin).getName()), origin);
- const std::vector<std::pair<int, std::wstring>> &alternatives = entry->getAlternatives();
- for (std::vector<std::pair<int, std::wstring>>::const_iterator iter = alternatives.begin(); iter != alternatives.end(); ++iter) {
+ const std::vector<std::pair<int, std::pair<std::wstring, int>>> &alternatives = entry->getAlternatives();
+ for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::const_iterator iter = alternatives.begin(); iter != alternatives.end(); ++iter) {
addToComboBox(combo, ToQString(m_DirectoryStructure->getOriginByID(iter->first).getName()), iter->first);
}
combo->setCurrentIndex(combo->count() - 1);
diff --git a/src/transfersavesdialog.cpp b/src/transfersavesdialog.cpp
index 61dab6e5..130df14f 100644
--- a/src/transfersavesdialog.cpp
+++ b/src/transfersavesdialog.cpp
@@ -342,7 +342,7 @@ bool TransferSavesDialog::transferCharacters(
if (!method(sourceFile.absoluteFilePath(), destinationFile)) {
qCritical(errmsg,
sourceFile.absoluteFilePath().toUtf8().constData(),
- destinationFile.toUtf8().constData());
+ qUtf8Printable(destinationFile));
}
}
}
diff --git a/src/tutorials/tutorial_firststeps_main.js b/src/tutorials/tutorial_firststeps_main.js
index c47b5c0d..ee97766b 100644
--- a/src/tutorials/tutorial_firststeps_main.js
+++ b/src/tutorials/tutorial_firststeps_main.js
@@ -14,12 +14,12 @@ function getTutorialSteps()
},
function() {
- tutorial.text = qsTr("The highlighted button provides hints on solving problems MO recognized automatically.")
- if (tutorialControl.waitForAction("actionProblems")) {
- tutorial.text += qsTr("\nThere IS a problem now but you may want to hold off on fixing it until after completing the tutorial.")
- highlightAction("actionProblems", true)
+ tutorial.text = qsTr("The highlighted button provides hints on solving potential problems MO recognized automatically.")
+ if (tutorialControl.waitForAction("actionNotifications")) {
+ tutorial.text += qsTr("\nThere IS a notification now but you may want to hold off on clearing it until after completing the tutorial.")
+ highlightAction("actionNotifications", true)
} else {
- highlightAction("actionProblems", false)
+ highlightAction("actionNotifications", false)
waitForClick()
}
},
diff --git a/src/tutorials/tutorial_primer_main.js b/src/tutorials/tutorial_primer_main.js
index 5d5597c1..5af99237 100644
--- a/src/tutorials/tutorial_primer_main.js
+++ b/src/tutorials/tutorial_primer_main.js
@@ -81,7 +81,7 @@ function setupTooptips() {
tooltipWidget("logList", qsTr("Log messages produced by MO. Please note that messages with a light bulb usually don't require your attention."))
tooltipAction("actionSettings", qsTr("Configure Mod Organizer."))
- tooltipAction("actionProblems", qsTr("Reports potential Problems about the current setup."))
+ 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."))
switch (manager.findControl("tabWidget").currentIndex) {
diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp
index 9c81d0d9..5ad19fb0 100644
--- a/src/usvfsconnector.cpp
+++ b/src/usvfsconnector.cpp
@@ -61,7 +61,7 @@ LogWorker::LogWorker()
{
m_LogFile.open(QIODevice::WriteOnly);
qDebug("usvfs log messages are written to %s",
- qPrintable(m_LogFile.fileName()));
+ qUtf8Printable(m_LogFile.fileName()));
}
LogWorker::~LogWorker()
@@ -138,6 +138,8 @@ UsvfsConnector::UsvfsConnector()
BlacklistExecutable(buf.data());
}
+ ClearLibraryForceLoads();
+
m_LogWorker.moveToThread(&m_WorkerThread);
connect(&m_WorkerThread, SIGNAL(started()), &m_LogWorker, SLOT(process()));
@@ -203,7 +205,8 @@ void UsvfsConnector::updateMapping(const MappingType &mapping)
*/
}
-void UsvfsConnector::updateParams(int logLevel, int crashDumpsType, QString executableBlacklist) {
+void UsvfsConnector::updateParams(int logLevel, int crashDumpsType, QString executableBlacklist)
+{
USVFSUpdateParams(::logLevel(logLevel), ::crashDumpsType(crashDumpsType));
ClearExecutableBlacklist();
for (auto exec : executableBlacklist.split(";")) {
@@ -211,3 +214,16 @@ void UsvfsConnector::updateParams(int logLevel, int crashDumpsType, QString exec
BlacklistExecutable(buf.data());
}
}
+
+void UsvfsConnector::updateForcedLibraries(const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries)
+{
+ ClearLibraryForceLoads();
+ for (auto setting : forcedLibraries) {
+ if (setting.enabled()) {
+ ForceLoadLibrary(
+ setting.process().toStdWString().data(),
+ setting.library().toStdWString().data()
+ );
+ }
+ }
+}
diff --git a/src/usvfsconnector.h b/src/usvfsconnector.h
index 3aefb703..8a88bde5 100644
--- a/src/usvfsconnector.h
+++ b/src/usvfsconnector.h
@@ -27,7 +27,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QThread>
#include <QFile>
#include <QDebug>
+#include <QList>
#include <usvfsparameters.h>
+#include "executableinfo.h"
class LogWorker : public QThread {
@@ -83,6 +85,7 @@ public:
void updateMapping(const MappingType &mapping);
void updateParams(int logLevel, int crashDumpsType, QString executableBlacklist);
+ void updateForcedLibraries(const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries);
private:
diff --git a/src/version.rc b/src/version.rc
index 88874c2e..117bb015 100644
--- a/src/version.rc
+++ b/src/version.rc
@@ -3,14 +3,14 @@
// 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,1,6
-#define VER_FILEVERSION_STR "2.1.6\0"
+#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)
+FILEFLAGS (0)
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE (0)
@@ -24,7 +24,7 @@ BEGIN
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-2018 Mod Organizer 2 contributors\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