summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSilarn <jrim@rimpo.org>2018-08-20 18:34:31 -0500
committerSilarn <jrim@rimpo.org>2019-02-18 21:26:44 -0600
commit9fe2f6126dc7b9396d188b57ccb097f0035f57b7 (patch)
treedfb6038767f55238506122682b644ac86415050b /src
parentbd9cc6254c8ded2f726e2b001a4ecf61ac0e5a1d (diff)
Initial Nexus API changes:
- Switch to SSO with WebSocket - Update endpoints (all but version checking)
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt7
-rw-r--r--src/credentialsdialog.cpp10
-rw-r--r--src/credentialsdialog.h3
-rw-r--r--src/downloadmanager.cpp19
-rw-r--r--src/main.cpp5
-rw-r--r--src/mainwindow.cpp13112
-rw-r--r--src/modinforegular.cpp23
-rw-r--r--src/nexusinterface.cpp101
-rw-r--r--src/nexusinterface.h10
-rw-r--r--src/nxmaccessmanager.cpp615
-rw-r--r--src/nxmaccessmanager.h61
-rw-r--r--src/organizer_en.ts2930
-rw-r--r--src/organizercore.cpp4946
-rw-r--r--src/organizercore.h5
-rw-r--r--src/settings.cpp48
-rw-r--r--src/settings.h12
-rw-r--r--src/settingsdialog.cpp42
-rw-r--r--src/settingsdialog.h340
-rw-r--r--src/settingsdialog.ui44
19 files changed, 10890 insertions, 11443 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5cbd5aa9..a518ccd4 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -267,6 +267,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})
@@ -325,7 +326,7 @@ 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
@@ -368,12 +369,12 @@ SET(windeploy_parameters "--no-translations --plugindir qtplugins --libdir dlls
INSTALL(
CODE
"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
)
# 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
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/downloadmanager.cpp b/src/downloadmanager.cpp
index ab8da7b8..c927695e 100644
--- a/src/downloadmanager.cpp
+++ b/src/downloadmanager.cpp
@@ -1536,10 +1536,10 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file
if (!info->version.isValid()) {
info->version = info->newestVersion;
}
- info->fileName = result["uri"].toString();
+ info->fileName = result["file_name"].toString();
info->fileCategory = result["category_id"].toInt();
- info->fileTime = matchDate(result["date"].toString());
- info->description = BBCode::convertToHTML(result["description"].toString());
+ info->fileTime = matchDate(result["uploaded_timestamp"].toString());
+ info->description = BBCode::convertToHTML(result["changelog_html"].toString());
info->repository = "Nexus";
info->gameName = gameName;
@@ -1554,23 +1554,12 @@ static int evaluateFileInfoMap(const QVariantMap &map, const std::map<QString, i
{
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());
+ auto preference = preferredServers.find(map["name"].toString());
if (preference != preferredServers.end()) {
result += 100 + preference->second * 20;
}
- if (map["IsPremium"].toBool()) result += 5;
-
return result;
}
diff --git a/src/main.cpp b/src/main.cpp
index 275845e2..631ec5b5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -621,7 +621,10 @@ int runApplication(MOApplication &application, SingleInstance &instance,
splash.show();
splash.activateWindow();
- NexusInterface::instance(&pluginContainer)->getAccessManager()->startLoginCheck();
+ QString apiKey;
+ if (organizer.settings().getNexusApiKey(apiKey)) {
+ NexusInterface::instance(&pluginContainer)->getAccessManager()->apiCheck(apiKey);
+ }
qDebug("initializing tutorials");
TutorialManager::init(
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index a30fdaac..add1ef7f 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,6554 +1,6558 @@
-/*
-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();
- ui->actionEndorseMO->setVisible(false);
-
- 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."));
- }
-
- 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(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(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());
-
- 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 (IPluginDiagnose *diagnose : m_PluginContainer.plugins<IPluginDiagnose>()) {
- 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(on_actionEndorseMO_triggered()));
- buttonMenu->addAction(endorseAction);
-
- QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu);
- connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(wontEndorse()));
- 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();
- }
- );
-
- // 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 (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));
- }
-}
-
-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::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());
- 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>());
- }
-/* 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::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()->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()
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) {
- 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 username, password;
- if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
- 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(boost::bind(&MainWindow::endorseMod, this, modInfo));
- }
- NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
- } 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)
-{
- 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::unendorse_clicked()
-{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn()) {
- 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 username, password;
- if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
- 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(boost::bind(&MainWindow::unendorseMod, this, modInfo));
- }
- NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
- } else {
- MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this);
- return;
- }
- }
- }
- else {
- unendorseMod(ModInfo::getByIndex(m_ContextRow));
- }
-}
-
-void MainWindow::loginFailed(const QString &error)
-{
- qDebug("login 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));
- 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();
- 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()
-{
- 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);
- }
- if (m_ModListSortProxy != nullptr) {
- m_ModListSortProxy->invalidate();
- }
-}
-
-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 all for update"), 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->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;
- }
- }
-
- 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());
- }
-
- NexusInterface::instance(&m_PluginContainer)->setNMMVersion(settings.getNMMVersion());
-
- 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.compare("en", Qt::CaseInsensitive)) {
- qDebug("localization file %s not found", qUtf8Printable(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", 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);
- }
- }
-
- ui->actionEndorseMO->setVisible(false);
-}
-
-
-void MainWindow::notEndorsedYet()
-{
- if (!Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) {
- ui->actionEndorseMO->setVisible(true);
- }
-}
-
-
-void MainWindow::wontEndorse()
-{
- Settings::instance().directInterface().setValue("wont_endorse_MO", true);
- ui->actionEndorseMO->setVisible(false);
-}
-
-
-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::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(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::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() &&
- Settings::instance().endorsementIntegration() &&
- !Settings::instance().directInterface().value("wont_endorse_MO", false).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") &&
- Settings::instance().endorsementIntegration()) {
- // 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, 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<IPluginDiagnose>(), 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();
-}
-
+/*
+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();
+ ui->actionEndorseMO->setVisible(false);
+
+ 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."));
+ }
+
+ 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(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(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());
+
+ 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 (IPluginDiagnose *diagnose : m_PluginContainer.plugins<IPluginDiagnose>()) {
+ 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(on_actionEndorseMO_triggered()));
+ buttonMenu->addAction(endorseAction);
+
+ QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu);
+ connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(wontEndorse()));
+ 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();
+ }
+ );
+
+ // 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 (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));
+ }
+}
+
+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::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());
+ 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>());
+ }
+/* 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::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(boost::bind(&MainWindow::endorseMod, this, 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()->loggedIn()) {
+ 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 username, password;
+ if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
+ 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(boost::bind(&MainWindow::endorseMod, this, modInfo));
+ }
+ NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
+ } 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)
+{
+ QString apiKey;
+ if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) {
+ ModInfo::getByIndex(m_ContextRow)->endorse(false);
+ } else {
+ if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) {
+ m_OrganizerCore.doAfterLogin([this] () { this->unendorse_clicked(); });
+ 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()->loggedIn()) {
+ 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 username, password;
+ if (m_OrganizerCore.settings().getNexusLogin(username, password)) {
+ 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(boost::bind(&MainWindow::unendorseMod, this, modInfo));
+ }
+ NexusInterface::instance(&m_PluginContainer)->getAccessManager()->login(username, password);
+ } else {
+ MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this);
+ return;
+ }
+ }
+ }
+ else {
+ unendorseMod(ModInfo::getByIndex(m_ContextRow));
+ }
+}
+
+void MainWindow::loginFailed(const QString &error)
+{
+ qDebug("login 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));
+ 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();
+ 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()
+{
+ statusBar()->show();
+ if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) {
+ m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this);
+ m_RefreshProgress->setRange(0, m_ModsToUpdate);
+ } else {
+ QString apiKey;
+ if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) {
+ m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); });
+ NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey);
+ } 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);
+ }
+ if (m_ModListSortProxy != nullptr) {
+ m_ModListSortProxy->invalidate();
+ }
+}
+
+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 all for update"), 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->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;
+ }
+ }
+
+ 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());
+ }
+
+ NexusInterface::instance(&m_PluginContainer)->setNMMVersion(settings.getNMMVersion());
+
+ 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.compare("en", Qt::CaseInsensitive)) {
+ qDebug("localization file %s not found", qUtf8Printable(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", 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);
+ }
+ }
+
+ ui->actionEndorseMO->setVisible(false);
+}
+
+
+void MainWindow::notEndorsedYet()
+{
+ if (!Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) {
+ ui->actionEndorseMO->setVisible(true);
+ }
+}
+
+
+void MainWindow::wontEndorse()
+{
+ Settings::instance().directInterface().setValue("wont_endorse_MO", true);
+ ui->actionEndorseMO->setVisible(false);
+}
+
+
+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(), m_OrganizerCore.getVersion().canonicalString(), true, 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(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::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() &&
+ Settings::instance().endorsementIntegration() &&
+ !Settings::instance().directInterface().value("wont_endorse_MO", false).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()->validated() &&
+ result.contains("voted_by_user") &&
+ Settings::instance().endorsementIntegration()) {
+ // 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)
+{
+ QMap results = resultData.toMap();
+ if (results["code"].toInt() == 200) {
+ if (results["status"].toString().compare("Endorsed") == 0) {
+ QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!"));
+ } else {
+ QMessageBox::information(this, tr("Okay."), tr("This mod will not be endorsed and will no longer ask you to endorse."));
+ }
+ ui->actionEndorseMO->setVisible(false);
+ 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, 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<IPluginDiagnose>(), 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/modinforegular.cpp b/src/modinforegular.cpp
index 529f20cb..3523feae 100644
--- a/src/modinforegular.cpp
+++ b/src/modinforegular.cpp
@@ -216,8 +216,10 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re
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();
+ setEndorsedState(endorsementStatus.compare("Endorsed") == 0 ? ENDORSED_TRUE : ENDORSED_FALSE);
}
m_LastNexusQuery = QDateTime::currentDateTime();
//m_MetaInfoChanged = true;
@@ -228,10 +230,17 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re
void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData)
{
- m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE;
- m_MetaInfoChanged = true;
- saveMeta();
- emit modDetailsUpdated(true);
+ QMap results = resultData.toMap();
+ if (results["code"].toInt() == 200 || results["code"].toInt() == 201) {
+ if (results["status"].toString().compare("Endorsed") == 0) {
+ m_EndorsedState = ENDORSED_TRUE;
+ } else {
+ m_EndorsedState = ENDORSED_FALSE;
+ }
+ m_MetaInfoChanged = true;
+ saveMeta();
+ emit modDetailsUpdated(true);
+ }
}
@@ -435,7 +444,7 @@ 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));
}
}
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp
index 993ae41e..4fe86136 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -28,6 +28,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QApplication>
#include <QNetworkCookieJar>
+#include <QJsonDocument>
#include <regex>
@@ -62,9 +63,9 @@ void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QV
m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule));
}
-void NexusBridge::requestToggleEndorsement(QString gameName, int modID, bool endorse, QVariant userData)
+void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData)
{
- m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, endorse, this, userData, m_SubModule));
+ m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule));
}
void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
@@ -85,17 +86,18 @@ void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userDa
QList<ModRepositoryFileInfo> fileInfoList;
- QVariantList resultList = resultData.toList();
+ QVariantMap resultInfo = resultData.toMap();
+ QList resultList = resultInfo["files"].toList();
for (const QVariant &file : resultList) {
ModRepositoryFileInfo temp;
QVariantMap fileInfo = file.toMap();
- temp.uri = fileInfo["uri"].toString();
+ temp.uri = fileInfo["file_name"].toString();
temp.name = fileInfo["name"].toString();
- temp.description = fileInfo["description"].toString();
+ temp.description = fileInfo["changelog_html"].toString();
temp.version = VersionInfo(fileInfo["version"].toString());
temp.categoryID = fileInfo["category_id"].toInt();
- temp.fileID = fileInfo["id"].toInt();
+ temp.fileID = fileInfo["file_id"].toInt();
temp.fileSize = fileInfo["size"].toInt();
fileInfoList.append(temp);
}
@@ -399,10 +401,10 @@ int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID,
}
-int NexusInterface::requestToggleEndorsement(QString gameName, int modID, bool endorse, QObject *receiver, QVariant userData,
+int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game);
+ NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game);
requestInfo.m_Endorse = endorse;
m_RequestQueue.enqueue(requestInfo);
@@ -456,11 +458,11 @@ void NexusInterface::nextRequest()
return;
}
- if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->loggedIn()) {
- if (!getAccessManager()->loginAttempted()) {
+ if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) {
+ if (!getAccessManager()->validateAttempted()) {
emit needLogin();
return;
- } else if (getAccessManager()->loginWaiting()) {
+ } else if (getAccessManager()->validateWaiting()) {
return;
}
}
@@ -469,42 +471,51 @@ void NexusInterface::nextRequest()
info.m_Timeout = new QTimer(this);
info.m_Timeout->setInterval(60000);
+ QJsonObject postObject;
+ QJsonDocument postData(postObject);
+
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);
+ url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID);
} break;
case NXMRequestInfo::TYPE_FILES: {
- url = QString("%1/Files/indexfrommod/%2/").arg(info.m_URL).arg(info.m_ModID);
+ 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/Files/%2/").arg(info.m_URL).arg(info.m_FileID);
+ 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: {
- url = QString("%1/Files/download/%2").arg(info.m_URL).arg(info.m_FileID);
+ 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_TOGGLEENDORSEMENT: {
- url = QString("%1/Mods/toggleendorsement/%2?lvote=%3").arg(info.m_URL).arg(info.m_ModID).arg(!info.m_Endorse);
- hasParams = true;
+ 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_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());
+ 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", "0.5.5");
+ request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8());
- info.m_Reply = m_AccessManager->get(request);
+ if (postData.object().isEmpty())
+ info.m_Reply = m_AccessManager->get(request);
+ else
+ info.m_Reply = m_AccessManager->post(request, postData.toJson());
connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished()));
connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
@@ -627,7 +638,7 @@ void NexusInterface::requestTimeout()
namespace {
QString get_management_url(MOBase::IPluginGame const *game)
{
- return "https://legacy-api.nexusmods.com/" + game->gameNexusName().toLower();
+ return "https://api.nexusmods.com/v1";
}
}
@@ -638,6 +649,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
, MOBase::IPluginGame const *game
)
: m_ModID(modID)
+ , m_ModVersion("0")
, m_FileID(0)
, m_Reply(nullptr)
, m_Type(type)
@@ -648,7 +660,30 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
, m_URL(get_management_url(game))
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
- , m_GameName(game->gameShortName())
+ , m_GameName(game->gameNexusName())
+ , m_Endorse(false)
+{}
+
+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_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->gameNexusName())
, m_Endorse(false)
{}
@@ -659,6 +694,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector<int> modIDList
, MOBase::IPluginGame const *game
)
: m_ModID(-1)
+ , m_ModVersion("0")
, m_ModIDList(modIDList)
, m_FileID(0)
, m_Reply(nullptr)
@@ -670,18 +706,19 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector<int> modIDList
, m_URL(get_management_url(game))
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
- , m_GameName(game->gameShortName())
+ , m_GameName(game->gameNexusName())
, m_Endorse(false)
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
- , int fileID
- , NexusInterface::NXMRequestInfo::Type type
- , QVariant userData
- , const QString &subModule
- , MOBase::IPluginGame const *game
- )
+ , 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)
@@ -692,6 +729,6 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
, m_URL(get_management_url(game))
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
- , m_GameName(game->gameShortName())
+ , m_GameName(game->gameNexusName())
, m_Endorse(false)
{}
diff --git a/src/nexusinterface.h b/src/nexusinterface.h
index defb370f..7b707711 100644
--- a/src/nexusinterface.h
+++ b/src/nexusinterface.h
@@ -96,7 +96,7 @@ public:
* @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);
+ virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData);
public slots:
@@ -255,9 +255,9 @@ public:
* @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)
+ int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule)
{
- return requestToggleEndorsement(gameName, modID, endorse, receiver, userData, subModule, getGame(gameName));
+ return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName));
}
/**
@@ -269,7 +269,7 @@ public:
* @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,
+ int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule,
MOBase::IPluginGame const *game);
/**
@@ -363,6 +363,7 @@ private:
struct NXMRequestInfo {
int m_ModID;
+ QString m_ModVersion;
std::vector<int> m_ModIDList;
int m_FileID;
QNetworkReply *m_Reply;
@@ -385,6 +386,7 @@ private:
int m_Endorse;
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(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);
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index 912eab30..dc1eb2cb 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -1,351 +1,264 @@
-/*
-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(),
- 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::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", qUtf8Printable(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", qUtf8Printable(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);
+ 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;
+ }
+}
+
+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(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.setRawHeader("User-Agent", userAgent().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("Verifying Nexus login"));
+ 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;
+ comments << "Nexus Client v" + m_NMMVersion;
+ if (!subModule.isEmpty()) {
+ comments << "module: " + subModule;
+ }
+
+ return QString("Mod Organizer/%1 (%2)").arg(m_MOVersion, comments.join("; "));
+}
+
+
+QString NXMAccessManager::apiKey() const
+{
+ return m_ApiKey;
+}
+
+
+void NXMAccessManager::validateTimeout()
+{
+ m_ValidateReply->deleteLater();
+ m_ValidateReply = nullptr;
+ m_ValidateAttempted = false; // this usually means we might have success later
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_VALID;
+
+ emit validateFailed(tr("timeout"));
+}
+
+
+void NXMAccessManager::validateError(QNetworkReply::NetworkError)
+{
+ qDebug("login error");
+ if (m_ProgressDialog != nullptr) {
+ m_ProgressDialog->deleteLater();
+ m_ProgressDialog = nullptr;
+ }
+ m_ApiKey.clear();
+ m_ValidateState = VALIDATE_NOT_VALID;
+
+ if (m_ValidateReply != nullptr) {
+ emit validateFailed(m_ValidateReply->errorString());
+ m_ValidateReply->deleteLater();
+ m_ValidateReply = nullptr;
+ } else {
+ emit validateFailed(tr("Unknown error"));
+ }
+}
+
+
+void NXMAccessManager::validateFinished()
+{
+ if (m_ProgressDialog != nullptr) {
+ m_ProgressDialog->deleteLater();
+ m_ProgressDialog = nullptr;
+ }
+
+ if (m_ValidateReply != nullptr) {
+ QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll());
+ QJsonObject credentialsData = jdoc.object();
+ QString test = jdoc.toJson();
+ QString name = credentialsData.value("name").toString();
+ bool premium = credentialsData.value("is_premium?").toBool();
+ emit credentialsReceived(credentialsData.value("name").toString(),
+ credentialsData.value("is_premium?").toBool());
+
+ m_ValidateReply->deleteLater();
+ m_ValidateReply = nullptr;
+
+ m_ValidateState = VALIDATE_VALID;
+ emit validateSuccessful(true);
+ }
+}
diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h
index c58c4cc3..b316ef77 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -43,12 +43,12 @@ public:
void setNMMVersion(const QString &nmmVersion);
- bool loggedIn() const;
+ bool validated() const;
- bool loginAttempted() const;
- bool loginWaiting() const;
+ bool validateAttempted() const;
+ bool validateWaiting() const;
- void login(const QString &username, const QString &password);
+ void apiCheck(const QString &apiKey);
void showCookies() const;
@@ -56,9 +56,11 @@ public:
QString userAgent(const QString &subModule = QString()) const;
- void startLoginCheck();
+ QString apiKey() const;
- void refuseLogin();
+ void startValidationCheck();
+
+ void refuseValidation();
signals:
@@ -74,18 +76,17 @@ signals:
*
* @param necessary true if a login was necessary and succeeded, false if the user is still logged in
**/
- void loginSuccessful(bool necessary);
+ void validateSuccessful(bool necessary);
- void loginFailed(const QString &message);
+ void validateFailed(const QString &message);
void credentialsReceived(const QString &userName, bool premium);
private slots:
- void loginChecked();
- void loginFinished();
- void loginError(QNetworkReply::NetworkError errorCode);
- void loginTimeout();
+ void validateFinished();
+ void validateError(QNetworkReply::NetworkError errorCode);
+ void validateTimeout();
protected:
@@ -95,38 +96,24 @@ protected:
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;
+ QTimer m_ValidateTimeout;
+ QNetworkReply *m_ValidateReply;
QProgressDialog *m_ProgressDialog { nullptr };
QString m_MOVersion;
QString m_NMMVersion;
- QString m_Username;
- QString m_Password;
+ QString m_ApiKey;
- bool m_LoginAttempted;
+ bool m_ValidateAttempted;
enum {
- LOGIN_NOT_CHECKED,
- LOGIN_CHECKING,
- LOGIN_NOT_VALID,
- LOGIN_ATTEMPT_FAILED,
- LOGIN_REFUSED,
- LOGIN_VALID
- } m_LoginState = LOGIN_NOT_CHECKED;
+ VALIDATE_NOT_CHECKED,
+ VALIDATE_CHECKING,
+ VALIDATE_NOT_VALID,
+ VALIDATE_ATTEMPT_FAILED,
+ VALIDATE_REFUSED,
+ VALIDATE_VALID
+ } m_ValidateState = VALIDATE_NOT_CHECKED;
};
diff --git a/src/organizer_en.ts b/src/organizer_en.ts
index 50301bfa..43c01310 100644
--- a/src/organizer_en.ts
+++ b/src/organizer_en.ts
@@ -26,7 +26,7 @@
</message>
<message>
<location filename="aboutdialog.ui" line="160"/>
- <source>Lead Developers/ Maintainers</source>
+ <source>Current Maintainers</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -35,92 +35,67 @@
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="201"/>
- <source>Mo2 devs and Contributors</source>
+ <location filename="aboutdialog.ui" line="206"/>
+ <source>Major Contributors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <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"/>
+ <location filename="aboutdialog.ui" line="227"/>
<source>Translators</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="257"/>
+ <location filename="aboutdialog.ui" line="252"/>
<source>Cyb3r (Dutch)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="267"/>
+ <location filename="aboutdialog.ui" line="262"/>
<source>fruttyx (French)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="282"/>
+ <location filename="aboutdialog.ui" line="277"/>
<source>Yoplala (French)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="287"/>
+ <location filename="aboutdialog.ui" line="282"/>
<source>Faron (German)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="297"/>
+ <location filename="aboutdialog.ui" line="292"/>
<source>Mordan (Greek)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="310"/>
+ <location filename="aboutdialog.ui" line="305"/>
<source>Yoosk (Polish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="315"/>
+ <location filename="aboutdialog.ui" line="310"/>
<source>Brgodfx (Portuguese)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="320"/>
- <source>zDas (Portuguese)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="aboutdialog.ui" line="340"/>
+ <location filename="aboutdialog.ui" line="330"/>
<source>Jax (Swedish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="345"/>
- <source>yohru (Japanese)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="aboutdialog.ui" line="350"/>
+ <location filename="aboutdialog.ui" line="335"/>
<source>...and all other Transifex contributors!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="364"/>
+ <location filename="aboutdialog.ui" line="349"/>
<source>Other Supporters &amp;&amp; Contributors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="374"/>
- <source>Tannin (Original Creator)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="aboutdialog.ui" line="491"/>
+ <location filename="aboutdialog.ui" line="471"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
@@ -309,7 +284,7 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="directoryrefresher.cpp" line="189"/>
+ <location filename="directoryrefresher.cpp" line="179"/>
<source>failed to read mod (%1): %2</source>
<translation type="unfinished"></translation>
</message>
@@ -317,251 +292,435 @@ p, li { white-space: pre-wrap; }
<context>
<name>DownloadList</name>
<message>
- <location filename="downloadlist.cpp" line="71"/>
+ <location filename="downloadlist.cpp" line="64"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="72"/>
- <source>Size</source>
+ <location filename="downloadlist.cpp" line="65"/>
+ <source>Filetime</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="73"/>
- <source>Status</source>
+ <location filename="downloadlist.cpp" line="66"/>
+ <source>Size</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="74"/>
- <source>Filetime</source>
+ <location filename="downloadlist.cpp" line="67"/>
+ <source>Done</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="89"/>
- <source>&lt; game %1 mod %2 file %3 &gt;</source>
+ <location filename="downloadlist.cpp" line="83"/>
+ <source>Information missing, please select &quot;Query Info&quot; from the context menu to re-retrieve.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="downloadlist.cpp" line="90"/>
- <source>Unknown</source>
+ <source>pending download</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>DownloadListWidget</name>
<message>
- <location filename="downloadlist.cpp" line="91"/>
- <source>Pending</source>
+ <location filename="downloadlistwidget.ui" line="17"/>
+ <location filename="downloadlistwidget.ui" line="61"/>
+ <source>Placeholder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="101"/>
- <source>Started</source>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="102"/>
- <source>Canceling</source>
+ <location filename="downloadlistwidget.cpp" line="135"/>
+ <location filename="downloadlistwidget.cpp" line="137"/>
+ <source>Paused - Double Click to resume</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="103"/>
- <source>Pausing</source>
+ <location filename="downloadlistwidget.cpp" line="155"/>
+ <location filename="downloadlistwidget.cpp" line="157"/>
+ <source>Installed - Double Click to re-install</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="104"/>
- <source>Canceled</source>
+ <location filename="downloadlistwidget.cpp" line="162"/>
+ <location filename="downloadlistwidget.cpp" line="164"/>
+ <source>Uninstalled - Double Click to re-install</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>DownloadListWidgetCompact</name>
<message>
- <location filename="downloadlist.cpp" line="105"/>
- <source>Paused</source>
+ <location filename="downloadlistwidgetcompact.ui" line="17"/>
+ <location filename="downloadlistwidgetcompact.ui" line="56"/>
+ <source>Placeholder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="106"/>
- <source>Error</source>
+ <location filename="downloadlistwidgetcompact.ui" line="129"/>
+ <source>Done</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>DownloadListWidgetCompactDelegate</name>
<message>
- <location filename="downloadlist.cpp" line="107"/>
- <source>Fetching Info 1</source>
+ <location filename="downloadlistwidgetcompact.cpp" line="109"/>
+ <source>&lt; game %1 mod %2 file %3 &gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="108"/>
- <source>Fetching Info 2</source>
+ <location filename="downloadlistwidgetcompact.cpp" line="114"/>
+ <source>Pending</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadlistwidgetcompact.cpp" line="141"/>
+ <source>Paused</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadlistwidgetcompact.cpp" line="143"/>
+ <source>Fetching Info 1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="109"/>
- <source>Downloaded</source>
+ <location filename="downloadlistwidgetcompact.cpp" line="145"/>
+ <source>Fetching Info 2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="110"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="150"/>
<source>Installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="111"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="152"/>
<source>Uninstalled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="129"/>
- <source>Pending download</source>
+ <location filename="downloadlistwidgetcompact.cpp" line="154"/>
+ <source>Done</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="133"/>
- <source>Information missing, please select &quot;Query Info&quot; from the context menu to re-retrieve.</source>
+ <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>
-</context>
-<context>
- <name>DownloadListWidget</name>
<message>
- <location filename="downloadlistwidget.cpp" line="201"/>
+ <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>
+ <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>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadlistwidgetcompact.cpp" line="363"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="203"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="365"/>
<source>Query Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="205"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="367"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="206"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="369"/>
<source>Open File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="207"/>
- <location filename="downloadlistwidget.cpp" line="219"/>
- <location filename="downloadlistwidget.cpp" line="224"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="370"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="381"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="385"/>
<source>Show in Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="211"/>
- <location filename="downloadlistwidget.cpp" line="222"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="372"/>
<source>Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="213"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="374"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="215"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="376"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="217"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="379"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="218"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="380"/>
<source>Pause</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="223"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="383"/>
+ <source>Remove</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadlistwidgetcompact.cpp" line="384"/>
<source>Resume</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="229"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="389"/>
<source>Delete Installed...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="230"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="390"/>
<source>Delete Uninstalled...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="231"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="391"/>
<source>Delete All...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="235"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="394"/>
<source>Hide Installed...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="236"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="395"/>
<source>Hide Uninstalled...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="237"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="396"/>
<source>Hide All...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="239"/>
+ <location filename="downloadlistwidgetcompact.cpp" line="400"/>
<source>Un-Hide All...</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>DownloadListWidgetDelegate</name>
<message>
- <location filename="downloadlistwidget.cpp" line="257"/>
- <location filename="downloadlistwidget.cpp" line="312"/>
- <location filename="downloadlistwidget.cpp" line="321"/>
- <location filename="downloadlistwidget.cpp" line="330"/>
+ <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"/>
<source>Delete Files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="258"/>
+ <location filename="downloadlistwidget.cpp" line="249"/>
<source>This will permanently delete the selected download.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="313"/>
+ <location filename="downloadlistwidget.cpp" line="303"/>
<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="322"/>
+ <location filename="downloadlistwidget.cpp" line="312"/>
<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="331"/>
+ <location filename="downloadlistwidget.cpp" line="321"/>
<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="339"/>
- <location filename="downloadlistwidget.cpp" line="348"/>
- <location filename="downloadlistwidget.cpp" line="357"/>
+ <location filename="downloadlistwidget.cpp" line="329"/>
+ <location filename="downloadlistwidget.cpp" line="338"/>
+ <location filename="downloadlistwidget.cpp" line="347"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="340"/>
+ <location filename="downloadlistwidget.cpp" line="330"/>
<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="349"/>
+ <location filename="downloadlistwidget.cpp" line="339"/>
<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="358"/>
+ <location filename="downloadlistwidget.cpp" line="348"/>
<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>
@@ -587,7 +746,7 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="downloadmanager.cpp" line="502"/>
- <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>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
@@ -618,7 +777,7 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="downloadmanager.cpp" line="588"/>
- <location filename="downloadmanager.cpp" line="721"/>
+ <location filename="downloadmanager.cpp" line="725"/>
<source>remove: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
@@ -638,234 +797,234 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="743"/>
+ <location filename="downloadmanager.cpp" line="747"/>
<source>cancel: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="756"/>
+ <location filename="downloadmanager.cpp" line="760"/>
<source>pause: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="776"/>
+ <location filename="downloadmanager.cpp" line="780"/>
<source>resume: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="787"/>
+ <location filename="downloadmanager.cpp" line="791"/>
<source>resume (int): invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="811"/>
+ <location filename="downloadmanager.cpp" line="815"/>
<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="852"/>
+ <location filename="downloadmanager.cpp" line="856"/>
<source>query: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="874"/>
+ <location filename="downloadmanager.cpp" line="878"/>
<source>Please enter the nexus mod id</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="874"/>
+ <location filename="downloadmanager.cpp" line="878"/>
<source>Mod ID:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="885"/>
+ <location filename="downloadmanager.cpp" line="888"/>
<source>Please select the source game code for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="904"/>
+ <location filename="downloadmanager.cpp" line="907"/>
<source>VisitNexus: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="925"/>
+ <location filename="downloadmanager.cpp" line="928"/>
<source>Nexus ID for this Mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="932"/>
+ <location filename="downloadmanager.cpp" line="935"/>
<source>OpenFile: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="949"/>
+ <location filename="downloadmanager.cpp" line="952"/>
<source>OpenFileInDownloadsFolder: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="985"/>
+ <location filename="downloadmanager.cpp" line="988"/>
<source>get pending: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="994"/>
+ <location filename="downloadmanager.cpp" line="997"/>
<source>get path: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1003"/>
+ <location filename="downloadmanager.cpp" line="1006"/>
<source>Main</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1004"/>
+ <location filename="downloadmanager.cpp" line="1007"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1005"/>
+ <location filename="downloadmanager.cpp" line="1008"/>
<source>Optional</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1006"/>
+ <location filename="downloadmanager.cpp" line="1009"/>
<source>Old</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1007"/>
+ <location filename="downloadmanager.cpp" line="1010"/>
<source>Misc</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1008"/>
+ <location filename="downloadmanager.cpp" line="1011"/>
<source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1015"/>
+ <location filename="downloadmanager.cpp" line="1018"/>
<source>display name: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1035"/>
+ <location filename="downloadmanager.cpp" line="1038"/>
<source>file name: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1044"/>
+ <location filename="downloadmanager.cpp" line="1047"/>
<source>file time: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1058"/>
+ <location filename="downloadmanager.cpp" line="1061"/>
<source>file size: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1068"/>
+ <location filename="downloadmanager.cpp" line="1071"/>
<source>progress: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1078"/>
+ <location filename="downloadmanager.cpp" line="1081"/>
<source>state: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1088"/>
+ <location filename="downloadmanager.cpp" line="1091"/>
<source>infocomplete: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1103"/>
- <location filename="downloadmanager.cpp" line="1111"/>
+ <location filename="downloadmanager.cpp" line="1106"/>
+ <location filename="downloadmanager.cpp" line="1114"/>
<source>mod id: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1119"/>
+ <location filename="downloadmanager.cpp" line="1122"/>
<source>ishidden: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1128"/>
+ <location filename="downloadmanager.cpp" line="1131"/>
<source>file info: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1138"/>
+ <location filename="downloadmanager.cpp" line="1141"/>
<source>mark installed: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1183"/>
+ <location filename="downloadmanager.cpp" line="1186"/>
<source>mark uninstalled: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1356"/>
+ <location filename="downloadmanager.cpp" line="1359"/>
<source>Memory allocation error (in processing progress event).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1366"/>
+ <location filename="downloadmanager.cpp" line="1369"/>
<source>Memory allocation error (in processing downloaded data).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1492"/>
+ <location filename="downloadmanager.cpp" line="1495"/>
<source>Information updated</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1494"/>
- <location filename="downloadmanager.cpp" line="1508"/>
+ <location filename="downloadmanager.cpp" line="1497"/>
+ <location filename="downloadmanager.cpp" line="1511"/>
<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="1496"/>
+ <location filename="downloadmanager.cpp" line="1499"/>
<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="1625"/>
+ <location filename="downloadmanager.cpp" line="1617"/>
<source>No download server available. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1668"/>
+ <location filename="downloadmanager.cpp" line="1660"/>
<source>Failed to request file info from nexus: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1695"/>
+ <location filename="downloadmanager.cpp" line="1687"/>
<source>Warning: Content type is: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1700"/>
+ <location filename="downloadmanager.cpp" line="1692"/>
<source>Download header content length: %1 downloaded file size: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1702"/>
+ <location filename="downloadmanager.cpp" line="1694"/>
<source>Download failed: %1 (%2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1724"/>
+ <location filename="downloadmanager.cpp" line="1716"/>
<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="1807"/>
+ <location filename="downloadmanager.cpp" line="1799"/>
<source>failed to re-open %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1848"/>
+ <location filename="downloadmanager.cpp" line="1839"/>
<source>Unable to write download to drive (return %1).
Check the drive&apos;s available storage.
@@ -984,107 +1143,92 @@ 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="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"/>
+ <location filename="editexecutablesdialog.ui" line="197"/>
<source>Use Application&apos;s Icon for shortcuts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="241"/>
+ <location filename="editexecutablesdialog.ui" line="204"/>
<source>(*) This setting is profile-specific</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="250"/>
- <location filename="editexecutablesdialog.ui" line="253"/>
+ <location filename="editexecutablesdialog.ui" line="213"/>
+ <location filename="editexecutablesdialog.ui" line="216"/>
<source>Add an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="256"/>
- <location filename="editexecutablesdialog.cpp" line="247"/>
+ <location filename="editexecutablesdialog.ui" line="219"/>
+ <location filename="editexecutablesdialog.cpp" line="217"/>
<source>Add</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="267"/>
- <location filename="editexecutablesdialog.ui" line="270"/>
+ <location filename="editexecutablesdialog.ui" line="230"/>
+ <location filename="editexecutablesdialog.ui" line="233"/>
<source>Remove the selected executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="273"/>
+ <location filename="editexecutablesdialog.ui" line="236"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="301"/>
+ <location filename="editexecutablesdialog.ui" line="264"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="170"/>
+ <location filename="editexecutablesdialog.cpp" line="146"/>
<source>Select a binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="171"/>
+ <location filename="editexecutablesdialog.cpp" line="147"/>
<source>Executable (%1)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="196"/>
+ <location filename="editexecutablesdialog.cpp" line="172"/>
<source>Java (32-bit) required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="197"/>
+ <location filename="editexecutablesdialog.cpp" line="173"/>
<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="217"/>
+ <location filename="editexecutablesdialog.cpp" line="189"/>
<source>Select a directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="224"/>
+ <location filename="editexecutablesdialog.cpp" line="196"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="224"/>
+ <location filename="editexecutablesdialog.cpp" line="196"/>
<source>Really remove &quot;%1&quot; from executables?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="251"/>
+ <location filename="editexecutablesdialog.cpp" line="221"/>
<source>Modify</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="313"/>
- <location filename="editexecutablesdialog.cpp" line="333"/>
+ <location filename="editexecutablesdialog.cpp" line="267"/>
+ <location filename="editexecutablesdialog.cpp" line="287"/>
<source>Save Changes?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="314"/>
- <location filename="editexecutablesdialog.cpp" line="334"/>
+ <location filename="editexecutablesdialog.cpp" line="268"/>
+ <location filename="editexecutablesdialog.cpp" line="288"/>
<source>You made changes to the current executable, do you want to save them?</source>
<translation type="unfinished"></translation>
</message>
@@ -1137,85 +1281,6 @@ 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"/>
@@ -1280,117 +1345,104 @@ p, li { white-space: pre-wrap; }
<context>
<name>InstallationManager</name>
<message>
- <location filename="installationmanager.cpp" line="120"/>
+ <location filename="installationmanager.cpp" line="119"/>
<source>Password required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="121"/>
+ <location filename="installationmanager.cpp" line="120"/>
<source>Password</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="191"/>
- <location filename="installationmanager.cpp" line="290"/>
+ <location filename="installationmanager.cpp" line="190"/>
+ <location filename="installationmanager.cpp" line="289"/>
<source>Extracting files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="489"/>
+ <location filename="installationmanager.cpp" line="488"/>
<source>failed to create backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="498"/>
+ <location filename="installationmanager.cpp" line="497"/>
<source>Mod Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="498"/>
+ <location filename="installationmanager.cpp" line="497"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="551"/>
+ <location filename="installationmanager.cpp" line="550"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="552"/>
+ <location filename="installationmanager.cpp" line="551"/>
<source>The name you entered is invalid, please enter a different one.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="717"/>
+ <location filename="installationmanager.cpp" line="694"/>
<source>File format &quot;%1&quot; not supported</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="882"/>
+ <location filename="installationmanager.cpp" line="857"/>
<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="895"/>
+ <location filename="installationmanager.cpp" line="868"/>
<source>no error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="898"/>
+ <location filename="installationmanager.cpp" line="871"/>
<source>7z.dll not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="901"/>
+ <location filename="installationmanager.cpp" line="874"/>
<source>7z.dll isn&apos;t valid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="904"/>
+ <location filename="installationmanager.cpp" line="877"/>
<source>archive not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="907"/>
+ <location filename="installationmanager.cpp" line="880"/>
<source>failed to open archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="910"/>
+ <location filename="installationmanager.cpp" line="883"/>
<source>unsupported archive type</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="913"/>
+ <location filename="installationmanager.cpp" line="886"/>
<source>internal library error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="916"/>
+ <location filename="installationmanager.cpp" line="889"/>
<source>archive invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="920"/>
+ <location filename="installationmanager.cpp" line="893"/>
<source>unknown archive error</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>ListDialog</name>
- <message>
- <location filename="listdialog.ui" line="14"/>
- <source>Select an item...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="listdialog.ui" line="42"/>
- <source>Filter</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-<context>
<name>LockedDialog</name>
<message>
<location filename="lockeddialog.ui" line="14"/>
@@ -1447,17 +1499,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="142"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="130"/>
<source>failed to write to %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="178"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="161"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="203"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="186"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
@@ -1482,47 +1534,47 @@ 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="625"/>
+ <location filename="mainwindow.ui" line="590"/>
<source>Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="118"/>
+ <location filename="mainwindow.ui" line="115"/>
<source>Clear</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="137"/>
+ <location filename="mainwindow.ui" line="134"/>
<source>If checked, only mods that match all selected categories are displayed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="140"/>
+ <location filename="mainwindow.ui" line="137"/>
<source>And</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="150"/>
+ <location filename="mainwindow.ui" line="147"/>
<source>If checked, all mods that match at least one of the selected categories are displayed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="153"/>
+ <location filename="mainwindow.ui" line="150"/>
<source>Or</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="192"/>
+ <location filename="mainwindow.ui" line="189"/>
<source>Profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="202"/>
+ <location filename="mainwindow.ui" line="199"/>
<source>Pick a module collection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="205"/>
+ <location filename="mainwindow.ui" line="202"/>
<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; }
@@ -1532,84 +1584,76 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="236"/>
+ <location filename="mainwindow.ui" line="233"/>
<source>Open list options...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="239"/>
+ <location filename="mainwindow.ui" line="236"/>
<source>Refresh list. This is usually not necessary unless you modified data outside the program.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="259"/>
+ <location filename="mainwindow.ui" line="256"/>
<source>Show Open Folders menu...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="273"/>
- <location filename="mainwindow.ui" line="891"/>
+ <location filename="mainwindow.ui" line="270"/>
+ <location filename="mainwindow.ui" line="856"/>
<source>Restore Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="287"/>
- <location filename="mainwindow.ui" line="911"/>
- <location filename="mainwindow.cpp" line="4521"/>
+ <location filename="mainwindow.ui" line="284"/>
+ <location filename="mainwindow.ui" line="876"/>
<source>Create Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="301"/>
- <location filename="mainwindow.ui" line="925"/>
- <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="432"/>
+ <location filename="mainwindow.ui" line="400"/>
<source>List of available mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="435"/>
+ <location filename="mainwindow.ui" line="403"/>
<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="523"/>
- <location filename="mainwindow.ui" line="644"/>
- <location filename="mainwindow.ui" line="1063"/>
- <location filename="mainwindow.ui" line="1404"/>
+ <location filename="mainwindow.ui" line="488"/>
<source>Filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="580"/>
+ <location filename="mainwindow.ui" line="545"/>
<source>Clear all Filters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="620"/>
+ <location filename="mainwindow.ui" line="585"/>
<source>No groups</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="630"/>
+ <location filename="mainwindow.ui" line="595"/>
<source>Nexus IDs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="679"/>
+ <location filename="mainwindow.ui" line="609"/>
+ <location filename="mainwindow.ui" line="999"/>
+ <location filename="mainwindow.ui" line="1316"/>
+ <source>Namefilter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="644"/>
<source>Pick a program to run.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="682"/>
+ <location filename="mainwindow.ui" line="647"/>
<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; }
@@ -1619,12 +1663,12 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="730"/>
+ <location filename="mainwindow.ui" line="695"/>
<source>Run program</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="733"/>
+ <location filename="mainwindow.ui" line="698"/>
<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; }
@@ -1633,17 +1677,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="743"/>
+ <location filename="mainwindow.ui" line="708"/>
<source>Run</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="784"/>
+ <location filename="mainwindow.ui" line="749"/>
<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="787"/>
+ <location filename="mainwindow.ui" line="752"/>
<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; }
@@ -1652,32 +1696,27 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="794"/>
+ <location filename="mainwindow.ui" line="759"/>
<source>Shortcut</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="844"/>
+ <location filename="mainwindow.ui" line="809"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="867"/>
+ <location filename="mainwindow.ui" line="832"/>
<source>Sort</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="938"/>
- <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="1002"/>
<source>List of available esp/esm files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1005"/>
+ <location filename="mainwindow.ui" line="941"/>
<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; }
@@ -1686,27 +1725,27 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1076"/>
+ <location filename="mainwindow.ui" line="1012"/>
<source>Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1096"/>
+ <location filename="mainwindow.ui" line="1032"/>
<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="1099"/>
+ <location filename="mainwindow.ui" line="1035"/>
<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="1114"/>
+ <location filename="mainwindow.ui" line="1050"/>
<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="1117"/>
+ <location filename="mainwindow.ui" line="1053"/>
<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!
@@ -1714,1151 +1753,967 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1151"/>
+ <location filename="mainwindow.ui" line="1082"/>
<source>Data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1169"/>
+ <location filename="mainwindow.ui" line="1100"/>
<source>refresh data-directory overview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1172"/>
+ <location filename="mainwindow.ui" line="1103"/>
<source>Refresh the overview. This may take a moment.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1175"/>
- <location filename="mainwindow.ui" line="1318"/>
- <location filename="mainwindow.cpp" line="4395"/>
- <location filename="mainwindow.cpp" line="5334"/>
+ <location filename="mainwindow.ui" line="1106"/>
+ <location filename="mainwindow.cpp" line="3556"/>
+ <location filename="mainwindow.cpp" line="4375"/>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1191"/>
+ <location filename="mainwindow.ui" line="1122"/>
<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="1204"/>
+ <location filename="mainwindow.ui" line="1132"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1209"/>
+ <location filename="mainwindow.ui" line="1137"/>
<source>Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1221"/>
- <location filename="mainwindow.ui" line="1224"/>
- <source>Filters the above list so that only conflicts are displayed.</source>
+ <location filename="mainwindow.ui" line="1147"/>
+ <location filename="mainwindow.ui" line="1150"/>
+ <source>Filter the above list so that only conflicts are displayed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1227"/>
+ <location filename="mainwindow.ui" line="1153"/>
<source>Show only conflicts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1234"/>
- <location filename="mainwindow.ui" line="1240"/>
- <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="1243"/>
- <source>Show files from Archives</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.ui" line="1253"/>
+ <location filename="mainwindow.ui" line="1161"/>
<source>Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1277"/>
+ <location filename="mainwindow.ui" line="1185"/>
<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;&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&apos;t active now.&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;This is a list of all savegames 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&apos;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;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1297"/>
+ <location filename="mainwindow.ui" line="1205"/>
<source>Downloads</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1315"/>
- <source>Refresh downloads view</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.ui" line="1346"/>
+ <location filename="mainwindow.ui" line="1240"/>
<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="1384"/>
+ <location filename="mainwindow.ui" line="1296"/>
<source>Show Hidden</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1449"/>
+ <location filename="mainwindow.ui" line="1358"/>
<source>Tool Bar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1492"/>
+ <location filename="mainwindow.ui" line="1401"/>
<source>Install Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1495"/>
+ <location filename="mainwindow.ui" line="1404"/>
<source>Install &amp;Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1498"/>
+ <location filename="mainwindow.ui" line="1407"/>
<source>Install a new mod from an archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1501"/>
+ <location filename="mainwindow.ui" line="1410"/>
<source>Ctrl+M</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1510"/>
+ <location filename="mainwindow.ui" line="1419"/>
<source>Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1513"/>
+ <location filename="mainwindow.ui" line="1422"/>
<source>&amp;Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1516"/>
+ <location filename="mainwindow.ui" line="1425"/>
<source>Configure Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1519"/>
+ <location filename="mainwindow.ui" line="1428"/>
<source>Ctrl+P</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1528"/>
+ <location filename="mainwindow.ui" line="1437"/>
<source>Executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1531"/>
+ <location filename="mainwindow.ui" line="1440"/>
<source>&amp;Executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1534"/>
+ <location filename="mainwindow.ui" line="1443"/>
<source>Configure the executables that can be started through Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1537"/>
+ <location filename="mainwindow.ui" line="1446"/>
<source>Ctrl+E</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1546"/>
- <location filename="mainwindow.ui" line="1552"/>
+ <location filename="mainwindow.ui" line="1455"/>
+ <location filename="mainwindow.ui" line="1461"/>
<source>Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1549"/>
+ <location filename="mainwindow.ui" line="1458"/>
<source>&amp;Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1555"/>
+ <location filename="mainwindow.ui" line="1464"/>
<source>Ctrl+I</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1564"/>
+ <location filename="mainwindow.ui" line="1473"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1567"/>
+ <location filename="mainwindow.ui" line="1476"/>
<source>&amp;Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1570"/>
+ <location filename="mainwindow.ui" line="1479"/>
<source>Configure settings and workarounds</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1573"/>
+ <location filename="mainwindow.ui" line="1482"/>
<source>Ctrl+S</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1582"/>
+ <location filename="mainwindow.ui" line="1491"/>
<source>Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1585"/>
+ <location filename="mainwindow.ui" line="1494"/>
<source>Search nexus network for more mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1588"/>
+ <location filename="mainwindow.ui" line="1497"/>
<source>Ctrl+N</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1600"/>
- <location filename="mainwindow.cpp" line="5265"/>
+ <location filename="mainwindow.ui" line="1509"/>
+ <location filename="mainwindow.cpp" line="4316"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1603"/>
+ <location filename="mainwindow.ui" line="1512"/>
<source>Mod Organizer is up-to-date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1615"/>
- <location filename="mainwindow.cpp" line="678"/>
- <source>No Notifications</source>
+ <location filename="mainwindow.ui" line="1524"/>
+ <location filename="mainwindow.cpp" line="636"/>
+ <source>No Problems</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1618"/>
- <source>This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them.</source>
+ <location filename="mainwindow.ui" line="1527"/>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1627"/>
- <location filename="mainwindow.ui" line="1630"/>
+ <location filename="mainwindow.ui" line="1539"/>
+ <location filename="mainwindow.ui" line="1542"/>
<source>Help</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1633"/>
+ <location filename="mainwindow.ui" line="1545"/>
<source>Ctrl+H</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1642"/>
+ <location filename="mainwindow.ui" line="1554"/>
<source>Endorse MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1645"/>
- <location filename="mainwindow.cpp" line="5357"/>
+ <location filename="mainwindow.ui" line="1557"/>
+ <location filename="mainwindow.cpp" line="4398"/>
<source>Endorse Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1650"/>
+ <location filename="mainwindow.ui" line="1562"/>
<source>Copy Log to Clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1659"/>
+ <location filename="mainwindow.ui" line="1571"/>
<source>Change Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1662"/>
+ <location filename="mainwindow.ui" line="1574"/>
<source>Open the Instance selection dialog to manage a different Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="336"/>
+ <location filename="mainwindow.cpp" line="322"/>
<source>Toolbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="337"/>
+ <location filename="mainwindow.cpp" line="323"/>
<source>Desktop</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="338"/>
+ <location filename="mainwindow.cpp" line="324"/>
<source>Start Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="363"/>
+ <location filename="mainwindow.cpp" line="348"/>
<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="498"/>
- <source>Crash on exit</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="499"/>
- <source>MO crashed while exiting. Some settings may not be saved.
-
-Error: %1</source>
+ <location filename="mainwindow.cpp" line="624"/>
+ <source>Problems</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="666"/>
- <source>Notifications</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="667"/>
- <source>There are notifications to read</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="679"/>
- <source>There are no notifications</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="741"/>
- <location filename="mainwindow.cpp" line="4531"/>
- <location filename="mainwindow.cpp" line="4535"/>
- <source>Endorse</source>
+ <location filename="mainwindow.cpp" line="625"/>
+ <source>There are potential problems with your setup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="745"/>
- <source>Won&apos;t Endorse</source>
+ <location filename="mainwindow.cpp" line="637"/>
+ <source>Everything seems to be in order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="760"/>
+ <location filename="mainwindow.cpp" line="699"/>
<source>Help on UI</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="764"/>
- <source>Documentation</source>
+ <location filename="mainwindow.cpp" line="703"/>
+ <source>Documentation Wiki</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="768"/>
- <source>Chat on Discord</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="772"/>
+ <location filename="mainwindow.cpp" line="707"/>
<source>Report Issue</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="776"/>
+ <location filename="mainwindow.cpp" line="711"/>
<source>Tutorials</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="815"/>
+ <location filename="mainwindow.cpp" line="750"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="816"/>
+ <location filename="mainwindow.cpp" line="751"/>
<source>About Qt</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="875"/>
+ <location filename="mainwindow.cpp" line="803"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="876"/>
+ <location filename="mainwindow.cpp" line="804"/>
<source>Please enter a name for the new profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="884"/>
+ <location filename="mainwindow.cpp" line="812"/>
<source>failed to create profile: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="928"/>
+ <location filename="mainwindow.cpp" line="856"/>
<source>Show tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="929"/>
+ <location filename="mainwindow.cpp" line="857"/>
<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="965"/>
+ <location filename="mainwindow.cpp" line="893"/>
<source>Downloads in progress</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="966"/>
+ <location filename="mainwindow.cpp" line="894"/>
<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="1083"/>
+ <location filename="mainwindow.cpp" line="1011"/>
<source>Plugin &quot;%1&quot; failed: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1085"/>
+ <location filename="mainwindow.cpp" line="1013"/>
<source>Plugin &quot;%1&quot; failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1163"/>
+ <location filename="mainwindow.cpp" line="1049"/>
<source>Browse Mod Page</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1370"/>
+ <location filename="mainwindow.cpp" line="1184"/>
<source>Also in: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1381"/>
+ <location filename="mainwindow.cpp" line="1195"/>
<source>No conflict</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1466"/>
+ <location filename="mainwindow.cpp" line="1307"/>
<source>&lt;Edit...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1744"/>
+ <location filename="mainwindow.cpp" line="1575"/>
<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="1806"/>
+ <location filename="mainwindow.cpp" line="1637"/>
<source>Activating Network Proxy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1867"/>
- <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>
+ <location filename="mainwindow.cpp" line="1695"/>
+ <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>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1969"/>
+ <location filename="mainwindow.cpp" line="1784"/>
<source>Choose Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1970"/>
+ <location filename="mainwindow.cpp" line="1785"/>
<source>Mod Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2157"/>
+ <location filename="mainwindow.cpp" line="1947"/>
<source>Start Tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2158"/>
+ <location filename="mainwindow.cpp" line="1948"/>
<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="2331"/>
+ <location filename="mainwindow.cpp" line="2102"/>
<source>failed to spawn notepad.exe: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2371"/>
+ <location filename="mainwindow.cpp" line="2142"/>
<source>failed to change origin name: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2395"/>
+ <location filename="mainwindow.cpp" line="2166"/>
<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="2419"/>
+ <location filename="mainwindow.cpp" line="2190"/>
<source>&lt;Contains %1&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2454"/>
+ <location filename="mainwindow.cpp" line="2225"/>
<source>&lt;Checked&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2455"/>
+ <location filename="mainwindow.cpp" line="2226"/>
<source>&lt;Unchecked&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2456"/>
+ <location filename="mainwindow.cpp" line="2227"/>
<source>&lt;Update&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2457"/>
- <source>&lt;Mod Backup&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2458"/>
+ <location filename="mainwindow.cpp" line="2228"/>
<source>&lt;Managed by MO&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2459"/>
+ <location filename="mainwindow.cpp" line="2229"/>
<source>&lt;Managed outside MO&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2460"/>
+ <location filename="mainwindow.cpp" line="2230"/>
<source>&lt;No category&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2461"/>
+ <location filename="mainwindow.cpp" line="2231"/>
<source>&lt;Conflicted&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2462"/>
+ <location filename="mainwindow.cpp" line="2232"/>
<source>&lt;Not Endorsed&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2508"/>
+ <location filename="mainwindow.cpp" line="2278"/>
<source>failed to rename mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2521"/>
+ <location filename="mainwindow.cpp" line="2291"/>
<source>Overwrite?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2522"/>
+ <location filename="mainwindow.cpp" line="2292"/>
<source>This will replace the existing mod &quot;%1&quot;. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2525"/>
+ <location filename="mainwindow.cpp" line="2295"/>
<source>failed to remove mod &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2529"/>
- <location filename="mainwindow.cpp" line="5091"/>
- <location filename="mainwindow.cpp" line="5115"/>
+ <location filename="mainwindow.cpp" line="2299"/>
+ <location filename="mainwindow.cpp" line="4175"/>
+ <location filename="mainwindow.cpp" line="4199"/>
<source>failed to rename &quot;%1&quot; to &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2606"/>
- <location filename="mainwindow.cpp" line="4098"/>
- <location filename="mainwindow.cpp" line="4106"/>
- <location filename="mainwindow.cpp" line="4644"/>
+ <location filename="mainwindow.cpp" line="2363"/>
+ <location filename="mainwindow.cpp" line="3279"/>
+ <location filename="mainwindow.cpp" line="3287"/>
+ <location filename="mainwindow.cpp" line="3759"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2607"/>
+ <location filename="mainwindow.cpp" line="2364"/>
<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="2622"/>
+ <location filename="mainwindow.cpp" line="2377"/>
<source>failed to remove mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2654"/>
- <location filename="mainwindow.cpp" line="2657"/>
- <location filename="mainwindow.cpp" line="2667"/>
+ <location filename="mainwindow.cpp" line="2409"/>
+ <location filename="mainwindow.cpp" line="2412"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2654"/>
+ <location filename="mainwindow.cpp" line="2409"/>
<source>Installation file no longer exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2658"/>
+ <location filename="mainwindow.cpp" line="2413"/>
<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="2668"/>
- <source>Failed to create backup.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2685"/>
+ <location filename="mainwindow.cpp" line="2430"/>
<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="2701"/>
- <location filename="mainwindow.cpp" line="2727"/>
- <location filename="mainwindow.cpp" line="2761"/>
- <location filename="mainwindow.cpp" line="2787"/>
+ <location filename="mainwindow.cpp" line="2446"/>
+ <location filename="mainwindow.cpp" line="2473"/>
<source>You need to be logged in with Nexus to endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2712"/>
- <location filename="mainwindow.cpp" line="2720"/>
- <source>Endorsing multiple mods will take a while. Please wait...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2772"/>
- <location filename="mainwindow.cpp" line="2780"/>
- <source>Unendorsing multiple mods will take a while. Please wait...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2848"/>
+ <location filename="mainwindow.cpp" line="2521"/>
<source>Failed to display overwrite dialog: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3035"/>
- <source>Opening Nexus Links</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="3036"/>
- <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="3065"/>
+ <location filename="mainwindow.cpp" line="2699"/>
<source>Nexus ID for this Mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3077"/>
- <source>Opening Web Pages</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="3078"/>
- <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="3107"/>
+ <location filename="mainwindow.cpp" line="2709"/>
<source>Web page for this mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3294"/>
- <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="3349"/>
- <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="3381"/>
- <location filename="mainwindow.cpp" line="3519"/>
- <location filename="mainwindow.cpp" line="4457"/>
+ <location filename="mainwindow.cpp" line="2787"/>
+ <location filename="mainwindow.cpp" line="2816"/>
+ <location filename="mainwindow.cpp" line="3586"/>
<source>Create Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3382"/>
+ <location filename="mainwindow.cpp" line="2788"/>
<source>This will create an empty mod.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3391"/>
- <location filename="mainwindow.cpp" line="3529"/>
+ <location filename="mainwindow.cpp" line="2797"/>
+ <location filename="mainwindow.cpp" line="2826"/>
<source>A mod with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3419"/>
- <source>Create Separator...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="3420"/>
- <source>This will create a new separator.
-Please enter a name:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="3427"/>
- <source>A separator with this name already exists</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="3520"/>
+ <location filename="mainwindow.cpp" line="2817"/>
<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="3601"/>
- <source>Move successful.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="3622"/>
- <location filename="mainwindow.cpp" line="5695"/>
+ <location filename="mainwindow.cpp" line="2858"/>
+ <location filename="mainwindow.cpp" line="4715"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3623"/>
+ <location filename="mainwindow.cpp" line="2859"/>
<source>About to recursively delete:
</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3986"/>
+ <location filename="mainwindow.cpp" line="3173"/>
<source>Not logged in, endorsement information will be wrong</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3994"/>
+ <location filename="mainwindow.cpp" line="3181"/>
<source>Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3995"/>
+ <location filename="mainwindow.cpp" line="3182"/>
<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="4015"/>
- <location filename="mainwindow.cpp" line="5233"/>
+ <location filename="mainwindow.cpp" line="3202"/>
+ <location filename="mainwindow.cpp" line="4284"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4016"/>
+ <location filename="mainwindow.cpp" line="3203"/>
<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="4098"/>
+ <location filename="mainwindow.cpp" line="3279"/>
<source>Really enable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4106"/>
+ <location filename="mainwindow.cpp" line="3287"/>
<source>Really disable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4186"/>
+ <location filename="mainwindow.cpp" line="3352"/>
<source>Export to csv</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4189"/>
+ <location filename="mainwindow.cpp" line="3355"/>
<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="4192"/>
+ <location filename="mainwindow.cpp" line="3358"/>
<source>Select what mods you want export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4193"/>
+ <location filename="mainwindow.cpp" line="3359"/>
<source>All installed mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4194"/>
+ <location filename="mainwindow.cpp" line="3360"/>
<source>Only active (checked) mods from your current profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4195"/>
+ <location filename="mainwindow.cpp" line="3361"/>
<source>All currently visible mods in the mod list</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4216"/>
+ <location filename="mainwindow.cpp" line="3382"/>
<source>Choose what Columns to export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4219"/>
+ <location filename="mainwindow.cpp" line="3385"/>
<source>Mod_Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4221"/>
+ <location filename="mainwindow.cpp" line="3387"/>
<source>Mod_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4223"/>
- <source>Notes_column</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4224"/>
+ <location filename="mainwindow.cpp" line="3389"/>
<source>Mod_Status</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4225"/>
+ <location filename="mainwindow.cpp" line="3390"/>
<source>Primary_Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4226"/>
+ <location filename="mainwindow.cpp" line="3391"/>
<source>Nexus_ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4227"/>
+ <location filename="mainwindow.cpp" line="3392"/>
<source>Mod_Nexus_URL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4228"/>
+ <location filename="mainwindow.cpp" line="3393"/>
<source>Mod_Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4229"/>
+ <location filename="mainwindow.cpp" line="3394"/>
<source>Install_Date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4230"/>
+ <location filename="mainwindow.cpp" line="3395"/>
<source>Download_File_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4337"/>
+ <location filename="mainwindow.cpp" line="3497"/>
<source>export failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4356"/>
+ <location filename="mainwindow.cpp" line="3516"/>
<source>Open Game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4358"/>
+ <location filename="mainwindow.cpp" line="3518"/>
<source>Open MyGames folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4360"/>
- <source>Open INIs folder</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4364"/>
+ <location filename="mainwindow.cpp" line="3522"/>
<source>Open Instance folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4366"/>
+ <location filename="mainwindow.cpp" line="3524"/>
<source>Open Mods folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4368"/>
+ <location filename="mainwindow.cpp" line="3526"/>
<source>Open Profile folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4370"/>
+ <location filename="mainwindow.cpp" line="3528"/>
<source>Open Downloads folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4374"/>
+ <location filename="mainwindow.cpp" line="3532"/>
<source>Open MO2 Install folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4376"/>
+ <location filename="mainwindow.cpp" line="3534"/>
<source>Open MO2 Plugins folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4378"/>
+ <location filename="mainwindow.cpp" line="3536"/>
<source>Open MO2 Logs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4386"/>
+ <location filename="mainwindow.cpp" line="3545"/>
<source>Install Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4387"/>
+ <location filename="mainwindow.cpp" line="3547"/>
<source>Create empty mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4388"/>
- <source>Create Separator</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4392"/>
+ <location filename="mainwindow.cpp" line="3551"/>
<source>Enable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4393"/>
+ <location filename="mainwindow.cpp" line="3552"/>
<source>Disable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4394"/>
+ <location filename="mainwindow.cpp" line="3554"/>
<source>Check all for update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4396"/>
+ <location filename="mainwindow.cpp" line="3558"/>
<source>Export to csv...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4405"/>
- <location filename="mainwindow.cpp" line="4421"/>
- <source>Send to</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4406"/>
- <location filename="mainwindow.cpp" line="4422"/>
- <source>Top</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4407"/>
- <location filename="mainwindow.cpp" line="4423"/>
- <source>Bottom</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4408"/>
- <location filename="mainwindow.cpp" line="4424"/>
- <source>Priority...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4409"/>
- <source>Separator...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4448"/>
+ <location filename="mainwindow.cpp" line="3579"/>
<source>All Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4456"/>
+ <location filename="mainwindow.cpp" line="3585"/>
<source>Sync to Mods...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4458"/>
- <source>Move content to Mod...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4459"/>
+ <location filename="mainwindow.cpp" line="3587"/>
<source>Clear Overwrite...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4461"/>
- <location filename="mainwindow.cpp" line="4562"/>
+ <location filename="mainwindow.cpp" line="3589"/>
+ <location filename="mainwindow.cpp" line="3675"/>
<source>Open in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4463"/>
+ <location filename="mainwindow.cpp" line="3591"/>
<source>Restore Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4464"/>
+ <location filename="mainwindow.cpp" line="3592"/>
<source>Remove Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4467"/>
- <location filename="mainwindow.cpp" line="4486"/>
+ <location filename="mainwindow.cpp" line="3596"/>
<source>Change Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4471"/>
- <location filename="mainwindow.cpp" line="4491"/>
+ <location filename="mainwindow.cpp" line="3609"/>
<source>Primary Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4475"/>
- <source>Rename Separator...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4476"/>
- <source>Remove Separator...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4479"/>
- <source>Select Color...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4481"/>
- <source>Reset Color</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4498"/>
+ <location filename="mainwindow.cpp" line="3615"/>
<source>Change versioning scheme</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4502"/>
+ <location filename="mainwindow.cpp" line="3619"/>
<source>Un-ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4506"/>
+ <location filename="mainwindow.cpp" line="3621"/>
<source>Ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4511"/>
- <location filename="mainwindow.cpp" line="5808"/>
+ <location filename="mainwindow.cpp" line="3627"/>
+ <location filename="mainwindow.cpp" line="4819"/>
<source>Enable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4512"/>
- <location filename="mainwindow.cpp" line="5809"/>
+ <location filename="mainwindow.cpp" line="3628"/>
+ <location filename="mainwindow.cpp" line="4820"/>
<source>Disable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4518"/>
+ <location filename="mainwindow.cpp" line="3632"/>
<source>Rename Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4519"/>
+ <location filename="mainwindow.cpp" line="3633"/>
<source>Reinstall Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4520"/>
+ <location filename="mainwindow.cpp" line="3634"/>
<source>Remove Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4528"/>
+ <location filename="mainwindow.cpp" line="3641"/>
<source>Un-Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4532"/>
+ <location filename="mainwindow.cpp" line="3644"/>
+ <location filename="mainwindow.cpp" line="3648"/>
+ <source>Endorse</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="3645"/>
<source>Won&apos;t endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4538"/>
+ <location filename="mainwindow.cpp" line="3651"/>
<source>Endorsement state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4549"/>
+ <location filename="mainwindow.cpp" line="3662"/>
<source>Ignore missing data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4553"/>
+ <location filename="mainwindow.cpp" line="3666"/>
<source>Mark as converted/working</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4557"/>
+ <location filename="mainwindow.cpp" line="3670"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4559"/>
+ <location filename="mainwindow.cpp" line="3672"/>
<source>Visit web page</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4566"/>
+ <location filename="mainwindow.cpp" line="3679"/>
<source>Information...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4573"/>
- <location filename="mainwindow.cpp" line="5861"/>
+ <location filename="mainwindow.cpp" line="3686"/>
+ <location filename="mainwindow.cpp" line="4853"/>
<source>Exception: </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4575"/>
- <location filename="mainwindow.cpp" line="5863"/>
+ <location filename="mainwindow.cpp" line="3688"/>
+ <location filename="mainwindow.cpp" line="4855"/>
<source>Unknown exception</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4604"/>
+ <location filename="mainwindow.cpp" line="3719"/>
<source>&lt;All&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4606"/>
+ <location filename="mainwindow.cpp" line="3721"/>
<source>&lt;Multiple&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4641"/>
+ <location filename="mainwindow.cpp" line="3756"/>
<source>%1 more</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="4645"/>
+ <location filename="mainwindow.cpp" line="3760"/>
<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>
@@ -2866,12 +2721,12 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4690"/>
+ <location filename="mainwindow.cpp" line="3805"/>
<source>Enable Mods...</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="4705"/>
+ <location filename="mainwindow.cpp" line="3820"/>
<source>Delete %n save(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -2879,367 +2734,328 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4747"/>
+ <location filename="mainwindow.cpp" line="3862"/>
<source>failed to remove %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4769"/>
+ <location filename="mainwindow.cpp" line="3884"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4799"/>
- <source>Restarting MO</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4800"/>
- <source>Changing the managed game directory requires restarting MO.
-Any pending downloads will be paused.
-
-Click OK to restart MO now.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4820"/>
+ <location filename="mainwindow.cpp" line="3926"/>
<source>Can&apos;t change download directory while downloads are in progress!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4962"/>
+ <location filename="mainwindow.cpp" line="4047"/>
<source>failed to write to file %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4968"/>
+ <location filename="mainwindow.cpp" line="4053"/>
<source>%1 written</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5010"/>
+ <location filename="mainwindow.cpp" line="4094"/>
<source>Select binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5010"/>
+ <location filename="mainwindow.cpp" line="4094"/>
<source>Binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5036"/>
+ <location filename="mainwindow.cpp" line="4120"/>
<source>Enter Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5037"/>
+ <location filename="mainwindow.cpp" line="4121"/>
<source>Please enter a name for the executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5051"/>
+ <location filename="mainwindow.cpp" line="4135"/>
<source>Not an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5051"/>
+ <location filename="mainwindow.cpp" line="4135"/>
<source>This is not a recognized executable.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5076"/>
- <location filename="mainwindow.cpp" line="5101"/>
+ <location filename="mainwindow.cpp" line="4160"/>
+ <location filename="mainwindow.cpp" line="4185"/>
<source>Replace file?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5076"/>
+ <location filename="mainwindow.cpp" line="4160"/>
<source>There already is a hidden version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5079"/>
- <location filename="mainwindow.cpp" line="5104"/>
+ <location filename="mainwindow.cpp" line="4163"/>
+ <location filename="mainwindow.cpp" line="4188"/>
<source>File operation failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5079"/>
- <location filename="mainwindow.cpp" line="5104"/>
+ <location filename="mainwindow.cpp" line="4163"/>
+ <location filename="mainwindow.cpp" line="4188"/>
<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="5101"/>
+ <location filename="mainwindow.cpp" line="4185"/>
<source>There already is a visible version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5145"/>
- <location filename="mainwindow.cpp" line="6475"/>
- <source>Set Priority</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5145"/>
- <source>Set the priority of the selected plugins</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5200"/>
+ <location filename="mainwindow.cpp" line="4257"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5213"/>
+ <location filename="mainwindow.cpp" line="4270"/>
<source>failed to generate preview for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5233"/>
+ <location filename="mainwindow.cpp" line="4284"/>
<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="5267"/>
+ <location filename="mainwindow.cpp" line="4318"/>
<source>Update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5314"/>
+ <location filename="mainwindow.cpp" line="4355"/>
<source>Open/Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5315"/>
+ <location filename="mainwindow.cpp" line="4356"/>
<source>Add as Executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5319"/>
+ <location filename="mainwindow.cpp" line="4360"/>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5325"/>
+ <location filename="mainwindow.cpp" line="4366"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5327"/>
+ <location filename="mainwindow.cpp" line="4368"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5333"/>
+ <location filename="mainwindow.cpp" line="4374"/>
<source>Write To File...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5358"/>
+ <location filename="mainwindow.cpp" line="4399"/>
<source>Do you want to endorse Mod Organizer on %1 now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5497"/>
+ <location filename="mainwindow.cpp" line="4524"/>
<source>Thank you!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5497"/>
+ <location filename="mainwindow.cpp" line="4524"/>
<source>Thank you for your endorsement!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5532"/>
+ <location filename="mainwindow.cpp" line="4559"/>
<source>Request to Nexus failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5547"/>
- <location filename="mainwindow.cpp" line="5609"/>
+ <location filename="mainwindow.cpp" line="4574"/>
+ <location filename="mainwindow.cpp" line="4636"/>
<source>failed to read %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5559"/>
- <location filename="mainwindow.cpp" line="6051"/>
+ <location filename="mainwindow.cpp" line="4586"/>
+ <location filename="mainwindow.cpp" line="5043"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5559"/>
+ <location filename="mainwindow.cpp" line="4586"/>
<source>failed to extract %1 (errorcode %2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5591"/>
+ <location filename="mainwindow.cpp" line="4618"/>
<source>Extract BSA</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5620"/>
+ <location filename="mainwindow.cpp" line="4647"/>
<source>This archive contains invalid hashes. Some files may be broken.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5666"/>
+ <location filename="mainwindow.cpp" line="4693"/>
<source>Extract...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5696"/>
+ <location filename="mainwindow.cpp" line="4716"/>
<source>This will restart MO, continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5743"/>
+ <location filename="mainwindow.cpp" line="4756"/>
<source>Edit Categories...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5744"/>
+ <location filename="mainwindow.cpp" line="4757"/>
<source>Deselect filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5797"/>
+ <location filename="mainwindow.cpp" line="4808"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5813"/>
+ <location filename="mainwindow.cpp" line="4824"/>
<source>Enable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5814"/>
+ <location filename="mainwindow.cpp" line="4825"/>
<source>Disable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5835"/>
+ <location filename="mainwindow.cpp" line="4844"/>
<source>Unlock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5838"/>
+ <location filename="mainwindow.cpp" line="4847"/>
<source>Lock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5848"/>
- <source>Open Origin in Explorer</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5853"/>
- <source>Open Origin Info...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5997"/>
+ <location filename="mainwindow.cpp" line="4989"/>
<source>depends on missing &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6001"/>
+ <location filename="mainwindow.cpp" line="4993"/>
<source>incompatible with &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6027"/>
+ <location filename="mainwindow.cpp" line="5019"/>
<source>Please wait while LOOT is running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6124"/>
+ <location filename="mainwindow.cpp" line="5116"/>
<source>loot failed. Exit code was: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6146"/>
+ <location filename="mainwindow.cpp" line="5138"/>
<source>failed to start loot</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6149"/>
+ <location filename="mainwindow.cpp" line="5141"/>
<source>failed to run loot: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6153"/>
+ <location filename="mainwindow.cpp" line="5145"/>
<source>Errors occured</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6200"/>
+ <location filename="mainwindow.cpp" line="5192"/>
<source>Backup of load order created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6210"/>
+ <location filename="mainwindow.cpp" line="5202"/>
<source>Choose backup to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6223"/>
+ <location filename="mainwindow.cpp" line="5215"/>
<source>No Backups</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6223"/>
+ <location filename="mainwindow.cpp" line="5215"/>
<source>There are no backups to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6244"/>
- <location filename="mainwindow.cpp" line="6266"/>
+ <location filename="mainwindow.cpp" line="5236"/>
+ <location filename="mainwindow.cpp" line="5258"/>
<source>Restore failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6245"/>
- <location filename="mainwindow.cpp" line="6267"/>
+ <location filename="mainwindow.cpp" line="5237"/>
+ <location filename="mainwindow.cpp" line="5259"/>
<source>Failed to restore the backup. Errorcode: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6256"/>
+ <location filename="mainwindow.cpp" line="5248"/>
<source>Backup of modlist created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6362"/>
+ <location filename="mainwindow.cpp" line="5354"/>
<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="6364"/>
+ <location filename="mainwindow.cpp" line="5356"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6365"/>
+ <location filename="mainwindow.cpp" line="5357"/>
<source>Rename new file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6366"/>
+ <location filename="mainwindow.cpp" line="5358"/>
<source>Ignore file</source>
<translation type="unfinished"></translation>
</message>
- <message>
- <location filename="mainwindow.cpp" line="6475"/>
- <source>Set the priority of the selected mods</source>
- <translation type="unfinished"></translation>
- </message>
</context>
<context>
<name>MessageDialog</name>
<message>
- <location filename="messagedialog.ui" line="32"/>
- <location filename="messagedialog.ui" line="71"/>
+ <location filename="messagedialog.ui" line="150"/>
+ <location filename="messagedialog.ui" line="180"/>
<source>Placeholder</source>
<translation type="unfinished"></translation>
</message>
@@ -3247,82 +3063,72 @@ Click OK to restart MO now.</source>
<context>
<name>ModInfo</name>
<message>
- <location filename="modinfo.cpp" line="99"/>
+ <location filename="modinfo.cpp" line="95"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="100"/>
+ <location filename="modinfo.cpp" line="96"/>
<source>Textures</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="101"/>
+ <location filename="modinfo.cpp" line="97"/>
<source>Meshes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="102"/>
+ <location filename="modinfo.cpp" line="98"/>
<source>Bethesda Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="103"/>
+ <location filename="modinfo.cpp" line="99"/>
<source>UI Changes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="104"/>
+ <location filename="modinfo.cpp" line="100"/>
<source>Sound Effects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="105"/>
+ <location filename="modinfo.cpp" line="101"/>
<source>Scripts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="106"/>
+ <location filename="modinfo.cpp" line="102"/>
<source>Script Extender</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="107"/>
- <source>Script Extender Files</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modinfo.cpp" line="108"/>
+ <location filename="modinfo.cpp" line="103"/>
<source>SkyProc Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="109"/>
+ <location filename="modinfo.cpp" line="104"/>
<source>MCM Data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="110"/>
+ <location filename="modinfo.cpp" line="105"/>
<source>INI files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="111"/>
- <source>ModGroup files</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modinfo.cpp" line="113"/>
- <source>invalid content type: %1</source>
+ <location filename="modinfo.cpp" line="106"/>
+ <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="129"/>
+ <source>invalid mod index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="174"/>
+ <location filename="modinfo.cpp" line="159"/>
<source>remove: invalid mod index %1</source>
<translation type="unfinished"></translation>
</message>
@@ -3592,56 +3398,37 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="741"/>
+ <location filename="modinfodialog.ui" line="729"/>
<source>about:blank</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="767"/>
+ <location filename="modinfodialog.ui" line="755"/>
<source>Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <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"/>
+ <location filename="modinfodialog.ui" line="769"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="801"/>
- <location filename="modinfodialog.ui" line="804"/>
- <location filename="modinfodialog.ui" line="807"/>
- <source>Enter comments about the mod here. These are displayed in the notes column of the mod list.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modinfodialog.ui" line="814"/>
- <location filename="modinfodialog.ui" line="817"/>
- <location filename="modinfodialog.ui" line="823"/>
- <source>Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modinfodialog.ui" line="831"/>
+ <location filename="modinfodialog.ui" line="779"/>
<source>Filetree</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="845"/>
+ <location filename="modinfodialog.ui" line="793"/>
<source>Open Mod in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="870"/>
+ <location filename="modinfodialog.ui" line="818"/>
<source>A directory view of this mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="873"/>
+ <location filename="modinfodialog.ui" line="821"/>
<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; }
@@ -3651,288 +3438,288 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="900"/>
+ <location filename="modinfodialog.ui" line="848"/>
<source>Previous</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="907"/>
+ <location filename="modinfodialog.ui" line="855"/>
<source>Next</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="927"/>
+ <location filename="modinfodialog.ui" line="875"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="234"/>
+ <location filename="modinfodialog.cpp" line="211"/>
<source>&amp;Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="235"/>
+ <location filename="modinfodialog.cpp" line="212"/>
<source>&amp;Rename</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="236"/>
+ <location filename="modinfodialog.cpp" line="213"/>
<source>&amp;Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="237"/>
+ <location filename="modinfodialog.cpp" line="214"/>
<source>&amp;Unhide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="238"/>
+ <location filename="modinfodialog.cpp" line="215"/>
<source>&amp;Open</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="239"/>
+ <location filename="modinfodialog.cpp" line="216"/>
<source>&amp;New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="504"/>
- <location filename="modinfodialog.cpp" line="519"/>
+ <location filename="modinfodialog.cpp" line="481"/>
+ <location filename="modinfodialog.cpp" line="496"/>
<source>Save changes?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="504"/>
- <location filename="modinfodialog.cpp" line="519"/>
+ <location filename="modinfodialog.cpp" line="481"/>
+ <location filename="modinfodialog.cpp" line="496"/>
<source>Save changes to &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="710"/>
+ <location filename="modinfodialog.cpp" line="686"/>
<source>File Exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="710"/>
+ <location filename="modinfodialog.cpp" line="686"/>
<source>A file with that name exists, please enter a new one</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="727"/>
+ <location filename="modinfodialog.cpp" line="703"/>
<source>failed to move file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="752"/>
+ <location filename="modinfodialog.cpp" line="728"/>
<source>failed to create directory &quot;optional&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="796"/>
- <location filename="modinfodialog.cpp" line="1525"/>
+ <location filename="modinfodialog.cpp" line="772"/>
+ <location filename="modinfodialog.cpp" line="1496"/>
<source>Info requested, please wait</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="850"/>
+ <location filename="modinfodialog.cpp" line="826"/>
<source>Main</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="851"/>
+ <location filename="modinfodialog.cpp" line="827"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="852"/>
+ <location filename="modinfodialog.cpp" line="828"/>
<source>Optional</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="853"/>
+ <location filename="modinfodialog.cpp" line="829"/>
<source>Old</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="854"/>
+ <location filename="modinfodialog.cpp" line="830"/>
<source>Misc</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="855"/>
+ <location filename="modinfodialog.cpp" line="831"/>
<source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="866"/>
+ <location filename="modinfodialog.cpp" line="842"/>
<source>Current Version: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="870"/>
+ <location filename="modinfodialog.cpp" line="846"/>
<source>No update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="913"/>
+ <location filename="modinfodialog.cpp" line="889"/>
<source>(description incomplete, please visit nexus)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="928"/>
+ <location filename="modinfodialog.cpp" line="904"/>
<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="1026"/>
+ <location filename="modinfodialog.cpp" line="997"/>
<source>Failed to delete %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1042"/>
- <location filename="modinfodialog.cpp" line="1048"/>
- <location filename="modinfodialog.cpp" line="1067"/>
- <location filename="modinfodialog.cpp" line="1072"/>
+ <location filename="modinfodialog.cpp" line="1013"/>
+ <location filename="modinfodialog.cpp" line="1019"/>
+ <location filename="modinfodialog.cpp" line="1038"/>
+ <location filename="modinfodialog.cpp" line="1043"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1042"/>
- <location filename="modinfodialog.cpp" line="1067"/>
+ <location filename="modinfodialog.cpp" line="1013"/>
+ <location filename="modinfodialog.cpp" line="1038"/>
<source>Are sure you want to delete &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1048"/>
- <location filename="modinfodialog.cpp" line="1072"/>
+ <location filename="modinfodialog.cpp" line="1019"/>
+ <location filename="modinfodialog.cpp" line="1043"/>
<source>Are sure you want to delete the selected files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1146"/>
- <location filename="modinfodialog.cpp" line="1152"/>
+ <location filename="modinfodialog.cpp" line="1117"/>
+ <location filename="modinfodialog.cpp" line="1123"/>
<source>New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1158"/>
+ <location filename="modinfodialog.cpp" line="1129"/>
<source>Failed to create &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1262"/>
- <location filename="modinfodialog.cpp" line="1286"/>
+ <location filename="modinfodialog.cpp" line="1233"/>
+ <location filename="modinfodialog.cpp" line="1257"/>
<source>Replace file?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1262"/>
+ <location filename="modinfodialog.cpp" line="1233"/>
<source>There already is a hidden version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1265"/>
- <location filename="modinfodialog.cpp" line="1289"/>
+ <location filename="modinfodialog.cpp" line="1236"/>
+ <location filename="modinfodialog.cpp" line="1260"/>
<source>File operation failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1265"/>
- <location filename="modinfodialog.cpp" line="1289"/>
+ <location filename="modinfodialog.cpp" line="1236"/>
+ <location filename="modinfodialog.cpp" line="1260"/>
<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="1276"/>
- <location filename="modinfodialog.cpp" line="1299"/>
+ <location filename="modinfodialog.cpp" line="1247"/>
+ <location filename="modinfodialog.cpp" line="1270"/>
<source>failed to rename %1 to %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1286"/>
+ <location filename="modinfodialog.cpp" line="1257"/>
<source>There already is a visible version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1362"/>
+ <location filename="modinfodialog.cpp" line="1333"/>
<source>Select binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1362"/>
+ <location filename="modinfodialog.cpp" line="1333"/>
<source>Binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1434"/>
+ <location filename="modinfodialog.cpp" line="1405"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1447"/>
+ <location filename="modinfodialog.cpp" line="1418"/>
<source>failed to generate preview for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1463"/>
+ <location filename="modinfodialog.cpp" line="1434"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1463"/>
+ <location filename="modinfodialog.cpp" line="1434"/>
<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="1477"/>
+ <location filename="modinfodialog.cpp" line="1448"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1479"/>
+ <location filename="modinfodialog.cpp" line="1450"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1482"/>
- <location filename="modinfodialog.cpp" line="1502"/>
+ <location filename="modinfodialog.cpp" line="1453"/>
+ <location filename="modinfodialog.cpp" line="1473"/>
<source>Open/Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1486"/>
- <location filename="modinfodialog.cpp" line="1506"/>
+ <location filename="modinfodialog.cpp" line="1457"/>
+ <location filename="modinfodialog.cpp" line="1477"/>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1554"/>
+ <location filename="modinfodialog.cpp" line="1525"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1554"/>
+ <location filename="modinfodialog.cpp" line="1525"/>
<source>Please enter a name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1558"/>
- <location filename="modinfodialog.cpp" line="1561"/>
+ <location filename="modinfodialog.cpp" line="1529"/>
+ <location filename="modinfodialog.cpp" line="1532"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1558"/>
+ <location filename="modinfodialog.cpp" line="1529"/>
<source>Invalid name. Must be a valid file name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1561"/>
+ <location filename="modinfodialog.cpp" line="1532"/>
<source>A tweak by that name exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.cpp" line="1575"/>
+ <location filename="modinfodialog.cpp" line="1546"/>
<source>Create Tweak</source>
<translation type="unfinished"></translation>
</message>
@@ -3971,328 +3758,300 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModInfoRegular</name>
<message>
- <location filename="modinforegular.cpp" line="186"/>
- <location filename="modinforegular.cpp" line="189"/>
+ <location filename="modinforegular.cpp" line="171"/>
+ <location filename="modinforegular.cpp" line="174"/>
<source>failed to write %1/meta.ini: error %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinforegular.cpp" line="570"/>
+ <location filename="modinforegular.cpp" line="522"/>
<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="574"/>
+ <location filename="modinforegular.cpp" line="526"/>
<source>Categories: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>ModInfoSeparator</name>
- <message>
- <location filename="modinfoseparator.cpp" line="24"/>
- <source>This is a Separator</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-<context>
<name>ModList</name>
<message>
- <location filename="modlist.cpp" line="68"/>
+ <location filename="modlist.cpp" line="66"/>
<source>Game Plugins (ESP/ESM/ESL)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="69"/>
+ <location filename="modlist.cpp" line="67"/>
<source>Interface</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="70"/>
+ <location filename="modlist.cpp" line="68"/>
<source>Meshes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="71"/>
+ <location filename="modlist.cpp" line="69"/>
<source>Bethesda Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="72"/>
+ <location filename="modlist.cpp" line="70"/>
<source>Scripts (Papyrus)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="73"/>
+ <location filename="modlist.cpp" line="71"/>
<source>Script Extender Plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="74"/>
+ <location filename="modlist.cpp" line="72"/>
<source>SkyProc Patcher</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="75"/>
+ <location filename="modlist.cpp" line="73"/>
<source>Sound or Music</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="76"/>
+ <location filename="modlist.cpp" line="74"/>
<source>Textures</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="77"/>
+ <location filename="modlist.cpp" line="75"/>
<source>MCM Configuration</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="78"/>
+ <location filename="modlist.cpp" line="76"/>
<source>INI files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="79"/>
- <source>ModGroup files</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modlist.cpp" line="140"/>
+ <location filename="modlist.cpp" line="137"/>
<source>This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="149"/>
+ <location filename="modlist.cpp" line="146"/>
<source>Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="150"/>
- <source>Separator</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modlist.cpp" line="151"/>
+ <location filename="modlist.cpp" line="147"/>
<source>No valid game data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="152"/>
+ <location filename="modlist.cpp" line="148"/>
<source>Not endorsed yet</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="161"/>
+ <location filename="modlist.cpp" line="150"/>
<source>Overwrites loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="162"/>
+ <location filename="modlist.cpp" line="151"/>
<source>Overwritten loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="163"/>
+ <location filename="modlist.cpp" line="152"/>
<source>Loose files Overwrites &amp; Overwritten</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="164"/>
+ <location filename="modlist.cpp" line="153"/>
<source>Redundant</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="165"/>
+ <location filename="modlist.cpp" line="154"/>
<source>Overwrites an archive with loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="166"/>
+ <location filename="modlist.cpp" line="155"/>
<source>Archive is overwritten by loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="167"/>
+ <location filename="modlist.cpp" line="156"/>
<source>Overwrites another archive file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="168"/>
+ <location filename="modlist.cpp" line="157"/>
<source>Overwritten by another archive file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="169"/>
+ <location filename="modlist.cpp" line="158"/>
<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>
+ <location filename="modlist.cpp" line="159"/>
+ <source>Alternate game source</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="263"/>
+ <location filename="modlist.cpp" line="245"/>
<source>Non-MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="295"/>
+ <location filename="modlist.cpp" line="275"/>
<source>invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="452"/>
+ <location filename="modlist.cpp" line="417"/>
<source>installed version: &quot;%1&quot;, newest version: &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="454"/>
+ <location filename="modlist.cpp" line="419"/>
<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="462"/>
+ <location filename="modlist.cpp" line="427"/>
<source>Categories: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="493"/>
+ <location filename="modlist.cpp" line="456"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="498"/>
+ <location filename="modlist.cpp" line="461"/>
<source>Name is already in use by another mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1058"/>
+ <location filename="modlist.cpp" line="972"/>
<source>drag&amp;drop failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1137"/>
+ <location filename="modlist.cpp" line="1051"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1138"/>
+ <location filename="modlist.cpp" line="1052"/>
<source>Are you sure you want to remove &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1205"/>
+ <location filename="modlist.cpp" line="1117"/>
<source>Flags</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1206"/>
+ <location filename="modlist.cpp" line="1118"/>
<source>Content</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1207"/>
+ <location filename="modlist.cpp" line="1119"/>
<source>Mod Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1208"/>
+ <location filename="modlist.cpp" line="1120"/>
<source>Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1209"/>
+ <location filename="modlist.cpp" line="1121"/>
<source>Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1210"/>
+ <location filename="modlist.cpp" line="1122"/>
<source>Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1211"/>
+ <location filename="modlist.cpp" line="1123"/>
<source>Source Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1212"/>
+ <location filename="modlist.cpp" line="1124"/>
<source>Nexus ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1213"/>
+ <location filename="modlist.cpp" line="1125"/>
<source>Installation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1214"/>
- <source>Notes</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modlist.cpp" line="1215"/>
- <location filename="modlist.cpp" line="1251"/>
+ <location filename="modlist.cpp" line="1126"/>
+ <location filename="modlist.cpp" line="1159"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1223"/>
+ <location filename="modlist.cpp" line="1134"/>
<source>Name of your mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1224"/>
+ <location filename="modlist.cpp" line="1135"/>
<source>Version of the mod (if available)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1225"/>
+ <location filename="modlist.cpp" line="1136"/>
<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="1227"/>
+ <location filename="modlist.cpp" line="1138"/>
<source>Category of the mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1228"/>
+ <location filename="modlist.cpp" line="1139"/>
<source>The source game which was the origin of this mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1229"/>
+ <location filename="modlist.cpp" line="1140"/>
<source>Id of the mod as used on Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1230"/>
+ <location filename="modlist.cpp" line="1141"/>
<source>Emblemes to highlight things that might require attention.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1231"/>
- <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>
+ <location filename="modlist.cpp" line="1142"/>
+ <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;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1249"/>
+ <location filename="modlist.cpp" line="1158"/>
<source>Time this mod was installed</source>
<translation type="unfinished"></translation>
</message>
- <message>
- <location filename="modlist.cpp" line="1250"/>
- <source>User notes about the mod</source>
- <translation type="unfinished"></translation>
- </message>
</context>
<context>
<name>ModListSortProxy</name>
<message>
- <location filename="modlistsortproxy.cpp" line="506"/>
+ <location filename="modlistsortproxy.cpp" line="398"/>
<source>Drag&amp;Drop is only supported when sorting by priority</source>
<translation type="unfinished"></translation>
</message>
@@ -4326,30 +4085,25 @@ 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="133"/>
+ <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="151"/>
+ <source>Verifying Nexus login</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nxmaccessmanager.cpp" line="277"/>
+ <location filename="nxmaccessmanager.cpp" line="223"/>
<source>timeout</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nxmaccessmanager.cpp" line="297"/>
+ <location filename="nxmaccessmanager.cpp" line="242"/>
<source>Unknown error</source>
<translation type="unfinished"></translation>
</message>
- <message>
- <location filename="nxmaccessmanager.cpp" line="333"/>
- <source>Please check your password</source>
- <translation type="unfinished"></translation>
- </message>
</context>
<context>
<name>NXMUrl</name>
@@ -4362,17 +4116,17 @@ p, li { white-space: pre-wrap; }
<context>
<name>NexusInterface</name>
<message>
- <location filename="nexusinterface.cpp" line="209"/>
+ <location filename="nexusinterface.cpp" line="210"/>
<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="542"/>
<source>empty response</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="572"/>
+ <location filename="nexusinterface.cpp" line="571"/>
<source>invalid response</source>
<translation type="unfinished"></translation>
</message>
@@ -4380,222 +4134,201 @@ 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="701"/>
- <location filename="organizercore.cpp" line="712"/>
+ <location filename="organizercore.cpp" line="671"/>
+ <location filename="organizercore.cpp" line="682"/>
<source>Download started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="715"/>
+ <location filename="organizercore.cpp" line="685"/>
<source>Download failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1006"/>
- <location filename="organizercore.cpp" line="1071"/>
+ <location filename="organizercore.cpp" line="951"/>
+ <location filename="organizercore.cpp" line="1009"/>
<source>Installation successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1014"/>
- <location filename="organizercore.cpp" line="1081"/>
+ <location filename="organizercore.cpp" line="959"/>
+ <location filename="organizercore.cpp" line="1019"/>
<source>Configure Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1015"/>
- <location filename="organizercore.cpp" line="1082"/>
+ <location filename="organizercore.cpp" line="960"/>
+ <location filename="organizercore.cpp" line="1020"/>
<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="1027"/>
- <location filename="organizercore.cpp" line="1092"/>
- <source>mod not found: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="993"/>
+ <location filename="organizercore.cpp" line="972"/>
<location filename="organizercore.cpp" line="1030"/>
- <location filename="organizercore.cpp" line="1041"/>
- <location filename="organizercore.cpp" line="1099"/>
- <source>Installation cancelled</source>
+ <source>mod &quot;%1&quot; not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="994"/>
- <location filename="organizercore.cpp" line="1042"/>
- <source>Another installation is currently in progress.</source>
+ <location filename="organizercore.cpp" line="975"/>
+ <location filename="organizercore.cpp" line="1037"/>
+ <source>Installation cancelled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1031"/>
- <location filename="organizercore.cpp" line="1100"/>
+ <location filename="organizercore.cpp" line="976"/>
+ <location filename="organizercore.cpp" line="1038"/>
<source>The mod was not installed completely.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1318"/>
- <source>Executable not found: %1</source>
+ <location filename="organizercore.cpp" line="1240"/>
+ <source>Executable &quot;%1&quot; not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1346"/>
+ <location filename="organizercore.cpp" line="1267"/>
<source>Start Steam?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1347"/>
+ <location filename="organizercore.cpp" line="1268"/>
<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="1378"/>
+ <location filename="organizercore.cpp" line="1294"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1386"/>
+ <location filename="organizercore.cpp" line="1302"/>
<source>Windows Event Log Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1387"/>
+ <location filename="organizercore.cpp" line="1303"/>
<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="1399"/>
- <source>Blacklisted Executable</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1400"/>
- <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="1495"/>
+ <location filename="organizercore.cpp" line="1391"/>
<source>No profile set</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1791"/>
+ <location filename="organizercore.cpp" line="1677"/>
<source>Failed to refresh list of esps: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1900"/>
+ <location filename="organizercore.cpp" line="1761"/>
<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="1988"/>
+ <location filename="organizercore.cpp" line="1836"/>
<source>Download?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1989"/>
+ <location filename="organizercore.cpp" line="1837"/>
<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="2124"/>
- <location filename="organizercore.cpp" line="2173"/>
+ <location filename="organizercore.cpp" line="1971"/>
<source>failed to update mod list: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2180"/>
- <location filename="organizercore.cpp" line="2197"/>
+ <location filename="organizercore.cpp" line="1978"/>
+ <location filename="organizercore.cpp" line="1995"/>
<source>login successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2204"/>
+ <location filename="organizercore.cpp" line="2002"/>
<source>Login failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2205"/>
+ <location filename="organizercore.cpp" line="2003"/>
<source>Login failed, try again?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2214"/>
+ <location filename="organizercore.cpp" line="2012"/>
<source>login failed: %1. Download will not be associated with an account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2222"/>
+ <location filename="organizercore.cpp" line="2020"/>
<source>login failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2232"/>
+ <location filename="organizercore.cpp" line="2030"/>
<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="2285"/>
+ <location filename="organizercore.cpp" line="2083"/>
<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="2288"/>
- <location filename="organizercore.cpp" line="2304"/>
+ <location filename="organizercore.cpp" line="2086"/>
+ <location filename="organizercore.cpp" line="2102"/>
<source>Description missing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2297"/>
+ <location filename="organizercore.cpp" line="2095"/>
<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="2331"/>
+ <location filename="organizercore.cpp" line="2129"/>
<source>failed to save load order: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2403"/>
+ <location filename="organizercore.cpp" line="2201"/>
<source>The designated write target &quot;%1&quot; is not enabled.</source>
<translation type="unfinished"></translation>
</message>
@@ -4639,7 +4372,7 @@ Continue?</source>
</message>
<message>
<location filename="overwriteinfodialog.cpp" line="113"/>
- <source>mod not found: %1</source>
+ <source>%1 not found</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -4701,135 +4434,135 @@ Continue?</source>
<context>
<name>PluginList</name>
<message>
- <location filename="pluginlist.cpp" line="104"/>
+ <location filename="pluginlist.cpp" line="102"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="105"/>
+ <location filename="pluginlist.cpp" line="103"/>
<source>Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="106"/>
+ <location filename="pluginlist.cpp" line="104"/>
<source>Mod Index</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="107"/>
+ <location filename="pluginlist.cpp" line="105"/>
<source>Flags</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="108"/>
- <location filename="pluginlist.cpp" line="120"/>
+ <location filename="pluginlist.cpp" line="106"/>
+ <location filename="pluginlist.cpp" line="118"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="116"/>
+ <location filename="pluginlist.cpp" line="114"/>
<source>Name of your mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="117"/>
+ <location filename="pluginlist.cpp" line="115"/>
<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="119"/>
+ <location filename="pluginlist.cpp" line="117"/>
<source>The modindex determines the formids of objects originating from this mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="229"/>
+ <location filename="pluginlist.cpp" line="215"/>
<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="302"/>
- <source>Plugin not found: %1</source>
+ <location filename="pluginlist.cpp" line="288"/>
+ <source>esp not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="350"/>
- <location filename="pluginlist.cpp" line="362"/>
+ <location filename="pluginlist.cpp" line="336"/>
+ <location filename="pluginlist.cpp" line="348"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="350"/>
+ <location filename="pluginlist.cpp" line="336"/>
<source>Really enable all plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="362"/>
+ <location filename="pluginlist.cpp" line="348"/>
<source>Really disable all plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="467"/>
+ <location filename="pluginlist.cpp" line="438"/>
<source>The file containing locked plugin indices is broken</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="964"/>
- <location filename="pluginlist.cpp" line="968"/>
+ <location filename="pluginlist.cpp" line="934"/>
+ <location filename="pluginlist.cpp" line="938"/>
<source>&lt;b&gt;Origin&lt;/b&gt;: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="965"/>
+ <location filename="pluginlist.cpp" line="935"/>
<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="970"/>
+ <location filename="pluginlist.cpp" line="940"/>
<source>Author</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="973"/>
+ <location filename="pluginlist.cpp" line="943"/>
<source>Description</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="976"/>
+ <location filename="pluginlist.cpp" line="946"/>
<source>Missing Masters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="983"/>
+ <location filename="pluginlist.cpp" line="953"/>
<source>Enabled Masters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="986"/>
+ <location filename="pluginlist.cpp" line="956"/>
<source>Loads Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="987"/>
+ <location filename="pluginlist.cpp" line="957"/>
<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="992"/>
+ <location filename="pluginlist.cpp" line="962"/>
<source>Loads INI settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="993"/>
+ <location filename="pluginlist.cpp" line="963"/>
<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="997"/>
+ <location filename="pluginlist.cpp" line="967"/>
<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="1172"/>
+ <location filename="pluginlist.cpp" line="1142"/>
<source>failed to restore load order for %1</source>
<translation type="unfinished"></translation>
</message>
@@ -4837,7 +4570,7 @@ Continue?</source>
<context>
<name>PluginListSortProxy</name>
<message>
- <location filename="pluginlistsortproxy.cpp" line="119"/>
+ <location filename="pluginlistsortproxy.cpp" line="120"/>
<source>Drag&amp;Drop is only supported when sorting by priority or mod index</source>
<translation type="unfinished"></translation>
</message>
@@ -4859,7 +4592,7 @@ Continue?</source>
<name>ProblemsDialog</name>
<message>
<location filename="problemsdialog.ui" line="14"/>
- <source>Notifications</source>
+ <source>Problems</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -4892,7 +4625,7 @@ p, li { white-space: pre-wrap; }
<name>Profile</name>
<message>
<location filename="profile.cpp" line="81"/>
- <source>invalid profile name: %1</source>
+ <source>invalid profile name %1</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -4901,82 +4634,57 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="259"/>
+ <location filename="profile.cpp" line="211"/>
<source>failed to write mod list: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="270"/>
+ <location filename="profile.cpp" line="222"/>
<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="290"/>
+ <location filename="profile.cpp" line="242"/>
<source>failed to create tweaked ini: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="315"/>
+ <location filename="profile.cpp" line="267"/>
<source>failed to open %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="374"/>
+ <location filename="profile.cpp" line="326"/>
<source>&quot;%1&quot; is missing or inaccessible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="419"/>
- <location filename="profile.cpp" line="458"/>
- <location filename="profile.cpp" line="555"/>
- <location filename="profile.cpp" line="576"/>
- <location filename="profile.cpp" line="586"/>
- <location filename="profile.cpp" line="605"/>
- <location filename="profile.cpp" line="615"/>
- <source>invalid mod index: %1</source>
+ <location filename="profile.cpp" line="371"/>
+ <location filename="profile.cpp" line="410"/>
+ <location filename="profile.cpp" line="512"/>
+ <location filename="profile.cpp" line="531"/>
+ <location filename="profile.cpp" line="541"/>
+ <source>invalid index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="536"/>
+ <location filename="profile.cpp" line="493"/>
<source>Overwrite directory couldn&apos;t be parsed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="547"/>
+ <location filename="profile.cpp" line="502"/>
<source>invalid priority %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="786"/>
- <source>Delete profile-specific save games?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="profile.cpp" line="787"/>
- <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>
+ <location filename="profile.cpp" line="725"/>
+ <source>Delete savegames?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="817"/>
- <source>Missing profile-specific game INI files!</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="profile.cpp" line="818"/>
- <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.
-
-Missing files:
-</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="profile.cpp" line="834"/>
- <source>Delete profile-specific game INI files?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="profile.cpp" line="835"/>
- <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>
+ <location filename="profile.cpp" line="726"/>
+ <source>Do you want to delete local savegames? (If you select &quot;No&quot;, the save games will show up again if you re-enable local savegames)</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -4994,17 +4702,17 @@ Missing files:
</message>
<message>
<location filename="profileinputdialog.ui" line="30"/>
- <source>If checked, the new profile will use the default game INI settings.</source>
+ <source>If checked, the new profile will use the default game settings.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profileinputdialog.ui" line="33"/>
- <source>If checked, the new profile will use the default game INI settings instead of the &quot;global&quot; settings. Global settings are the settings you configure when running the game launcher directly, without MO.</source>
+ <source>If checked, the new profile will use the default game settings instead of the &quot;global&quot; settings. Global settings are the settings you configure when running the game launcher directly, without MO.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profileinputdialog.ui" line="36"/>
- <source>Default Game INI Settings</source>
+ <source>Default Game Settings</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -5033,41 +4741,27 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="profilesdialog.ui" line="38"/>
- <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"/>
- <source>If checked, save games are local to this profile and will not appear when starting with a different profile.</source>
+ <source>If checked, savegames 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"/>
- <source>Use profile-specific Save Games</source>
+ <source>Local Savegames</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profilesdialog.ui" line="51"/>
- <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>
+ <source>Local Game Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="54"/>
- <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"/>
- <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="61"/>
<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="64"/>
<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; }
@@ -5079,65 +4773,65 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="80"/>
+ <location filename="profilesdialog.ui" line="74"/>
<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="85"/>
+ <location filename="profilesdialog.ui" line="88"/>
<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="91"/>
<source>Create</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="107"/>
+ <location filename="profilesdialog.ui" line="101"/>
<source>Clone the selected profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="110"/>
+ <location filename="profilesdialog.ui" line="104"/>
<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="107"/>
<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="117"/>
+ <location filename="profilesdialog.ui" line="120"/>
<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="123"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="139"/>
+ <location filename="profilesdialog.ui" line="133"/>
<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="143"/>
+ <location filename="profilesdialog.ui" line="146"/>
<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="149"/>
<source>Transfer Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.ui" line="178"/>
+ <location filename="profilesdialog.ui" line="172"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
@@ -5194,7 +4888,7 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="profilesdialog.cpp" line="194"/>
- <source>Are you sure you want to remove this profile (including profile-specific save games, if any)?</source>
+ <source>Are you sure you want to remove this profile (including local savegames if any)?</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -5229,31 +4923,12 @@ 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="739"/>
+ <location filename="organizercore.cpp" line="709"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
@@ -5299,12 +4974,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 category index: %1</source>
+ <source>invalid 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>
@@ -5353,13 +5028,12 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="helper.cpp" line="56"/>
- <location filename="helper.cpp" line="65"/>
+ <location filename="helper.cpp" line="55"/>
<source>helper failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="helper.cpp" line="81"/>
+ <location filename="helper.cpp" line="71"/>
<source>failed to determine account name</source>
<translation type="unfinished"></translation>
</message>
@@ -5370,142 +5044,140 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="88"/>
+ <location filename="installationmanager.cpp" line="87"/>
<source>archive.dll not loaded: &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="82"/>
+ <location filename="instancemanager.cpp" line="81"/>
<source>Deleting folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="83"/>
+ <location filename="instancemanager.cpp" line="82"/>
<source>I&apos;m about to delete the following folder: &quot;%1&quot;. Proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="104"/>
+ <location filename="instancemanager.cpp" line="103"/>
<source>Choose Instance to Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="105"/>
+ <location filename="instancemanager.cpp" line="104"/>
<source>Be Carefull! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="118"/>
+ <location filename="instancemanager.cpp" line="117"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="119"/>
+ <location filename="instancemanager.cpp" line="118"/>
<source>Are you really sure you want to delete the Instance &quot;%1&quot; with all its files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="123"/>
+ <location filename="instancemanager.cpp" line="122"/>
<source>Failed to delete Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="124"/>
+ <location filename="instancemanager.cpp" line="123"/>
<source>Could not delete Instance &quot;%1&quot;.
If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="139"/>
+ <location filename="instancemanager.cpp" line="138"/>
<source>Enter a Name for the new Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="140"/>
- <source>Enter a new name or select one from the suggested list:
-(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>
+ <location filename="instancemanager.cpp" line="139"/>
+ <source>Enter a new name or select one from the suggested list:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="151"/>
- <location filename="instancemanager.cpp" line="222"/>
+ <location filename="instancemanager.cpp" line="148"/>
+ <location filename="instancemanager.cpp" line="219"/>
<source>Canceled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="157"/>
+ <location filename="instancemanager.cpp" line="154"/>
<source>Invalid instance name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="158"/>
+ <location filename="instancemanager.cpp" line="155"/>
<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="173"/>
+ <location filename="instancemanager.cpp" line="170"/>
<source>The instance &quot;%1&quot; already exists.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="174"/>
+ <location filename="instancemanager.cpp" line="171"/>
<source>Please choose a different instance name, like: &quot;%1 1&quot; .</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="193"/>
+ <location filename="instancemanager.cpp" line="190"/>
<source>Choose Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="194"/>
+ <location filename="instancemanager.cpp" line="191"/>
<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="206"/>
+ <location filename="instancemanager.cpp" line="203"/>
<source>New</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="207"/>
+ <location filename="instancemanager.cpp" line="204"/>
<source>Create a new instance.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="211"/>
+ <location filename="instancemanager.cpp" line="208"/>
<source>Portable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="212"/>
+ <location filename="instancemanager.cpp" line="209"/>
<source>Use MO folder for data.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="216"/>
+ <location filename="instancemanager.cpp" line="213"/>
<source>Manage Instances</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="217"/>
+ <location filename="instancemanager.cpp" line="214"/>
<source>Delete an Instance.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="268"/>
- <location filename="profile.cpp" line="69"/>
+ <location filename="instancemanager.cpp" line="265"/>
+ <location filename="profile.cpp" line="66"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="271"/>
+ <location filename="instancemanager.cpp" line="268"/>
<source>Data directory created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="272"/>
+ <location filename="instancemanager.cpp" line="269"/>
<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>
@@ -5517,7 +5189,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>file not found: %1</source>
+ <source>%1 not found</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -5575,7 +5247,7 @@ If the folder was still in use, restart MO and try again.</source>
</message>
<message>
<location filename="main.cpp" line="99"/>
- <location filename="organizercore.cpp" line="740"/>
+ <location filename="organizercore.cpp" line="710"/>
<source>Failed to create &quot;%1&quot;. Your user account probably lacks permission.</source>
<translation type="unfinished"></translation>
</message>
@@ -5592,64 +5264,58 @@ If the folder was still in use, restart MO and try again.</source>
<message>
<location filename="main.cpp" line="357"/>
<location filename="main.cpp" line="375"/>
- <location filename="main.cpp" line="388"/>
<source>Please select the game to manage</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="396"/>
- <source>Canceled finding game in &quot;%1&quot;.</source>
+ <location filename="main.cpp" line="385"/>
+ <source>No game identified in &quot;%1&quot;. The directory is required to contain the game binary and its launcher.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="401"/>
- <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="554"/>
+ <location filename="main.cpp" line="537"/>
<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="591"/>
+ <location filename="main.cpp" line="574"/>
<source>failed to start shortcut: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="613"/>
+ <location filename="main.cpp" line="596"/>
<source>failed to start application: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="699"/>
- <location filename="settings.cpp" line="1126"/>
+ <location filename="main.cpp" line="682"/>
+ <location filename="settings.cpp" line="968"/>
<source>Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="700"/>
+ <location filename="main.cpp" line="683"/>
<source>An instance of Mod Organizer is already running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="715"/>
+ <location filename="main.cpp" line="698"/>
<source>Failed to set up instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="941"/>
+ <location filename="mainwindow.cpp" line="869"/>
<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="1430"/>
- <location filename="mainwindow.cpp" line="4917"/>
+ <location filename="mainwindow.cpp" line="1271"/>
+ <location filename="mainwindow.cpp" line="4004"/>
<source>&lt;Manage...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1442"/>
+ <location filename="mainwindow.cpp" line="1283"/>
<source>failed to parse profile %1: %2</source>
<translation type="unfinished"></translation>
</message>
@@ -5685,12 +5351,12 @@ If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="543"/>
+ <location filename="pluginlist.cpp" line="514"/>
<source>failed to access %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="557"/>
+ <location filename="pluginlist.cpp" line="528"/>
<source>failed to set file time %1</source>
<translation type="unfinished"></translation>
</message>
@@ -5700,39 +5366,37 @@ If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1133"/>
+ <location filename="settings.cpp" line="975"/>
<source>Script Extender</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1140"/>
+ <location filename="settings.cpp" line="982"/>
<source>Proxy DLL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="147"/>
+ <location filename="spawn.cpp" line="143"/>
<source>failed to spawn &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="152"/>
+ <location filename="spawn.cpp" line="150"/>
<source>Elevation required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="153"/>
+ <location filename="spawn.cpp" line="151"/>
<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.
-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>
+Start elevated anyway? (you will be asked if you want to allow ModOrganizer.exe to make changes to the system)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="164"/>
- <location filename="spawn.cpp" line="178"/>
+ <location filename="spawn.cpp" line="166"/>
<source>failed to spawn &quot;%1&quot;: %2</source>
<translation type="unfinished"></translation>
</message>
@@ -5898,12 +5562,12 @@ Select Show Details option to see the full change-log.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="339"/>
+ <location filename="selfupdater.cpp" line="337"/>
<source>Failed to start %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="348"/>
+ <location filename="selfupdater.cpp" line="347"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
@@ -5911,42 +5575,31 @@ 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="521"/>
- <location filename="settings.cpp" line="540"/>
+ <location filename="settings.cpp" line="430"/>
+ <location filename="settings.cpp" line="449"/>
<source>attempt to store setting for unknown plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="904"/>
+ <location filename="settings.cpp" line="759"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="905"/>
+ <location filename="settings.cpp" line="760"/>
<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="1179"/>
- <source>Restart Mod Organizer?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settings.cpp" line="1180"/>
- <source>In order to reset the window geometries, MO must be restarted.
-Restart it now?</source>
- <translation type="unfinished"></translation>
- </message>
</context>
<context>
<name>SettingsDialog</name>
@@ -6020,188 +5673,136 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
</message>
<message>
<location filename="settingsdialog.ui" line="99"/>
- <source>Colors</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="105"/>
- <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="111"/>
- <source>Show mod list separator colors on the scrollbar</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <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="128"/>
- <source>Is overwritten (loose files)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="135"/>
- <source>Is overwriting (loose files)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="142"/>
- <source>Reset Colors</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="149"/>
- <source>Mod Contains selected Plugin</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="156"/>
- <source>Is overwritten (archive files)</source>
+ <source>If checked, the download interface will be more compact.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="163"/>
- <source>Is overwriting (archive files)</source>
+ <location filename="settingsdialog.ui" line="102"/>
+ <source>Compact Download Interface</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>
+ <location filename="settingsdialog.ui" line="109"/>
+ <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="179"/>
- <source>Configure Mod Categories</source>
+ <location filename="settingsdialog.ui" line="112"/>
+ <source>Download Meta Information</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="192"/>
+ <location filename="settingsdialog.ui" line="125"/>
<source>Reset stored information from dialogs.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="195"/>
+ <location filename="settingsdialog.ui" line="128"/>
<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="198"/>
+ <location filename="settingsdialog.ui" line="131"/>
<source>Reset Dialogs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <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="208"/>
- <source>Compact Download Interface</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="228"/>
- <source>If checked, the download list will display meta information instead of file names.</source>
+ <location filename="settingsdialog.ui" line="151"/>
+ <location filename="settingsdialog.ui" line="154"/>
+ <source>Modify the categories available to arrange your mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="231"/>
- <source>Download Meta Information</source>
+ <location filename="settingsdialog.ui" line="157"/>
+ <source>Configure Mod Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="242"/>
+ <location filename="settingsdialog.ui" line="168"/>
<source>Paths</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="250"/>
- <location filename="settingsdialog.ui" line="267"/>
- <location filename="settingsdialog.ui" line="364"/>
- <location filename="settingsdialog.ui" line="422"/>
+ <location filename="settingsdialog.ui" line="176"/>
+ <location filename="settingsdialog.ui" line="193"/>
+ <location filename="settingsdialog.ui" line="290"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="277"/>
+ <location filename="settingsdialog.ui" line="203"/>
<source>Caches</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="284"/>
+ <location filename="settingsdialog.ui" line="210"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="291"/>
- <location filename="settingsdialog.ui" line="294"/>
+ <location filename="settingsdialog.ui" line="217"/>
+ <location filename="settingsdialog.ui" line="220"/>
<source>Directory where downloads are stored.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="311"/>
+ <location filename="settingsdialog.ui" line="237"/>
<source>Downloads</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="331"/>
+ <location filename="settingsdialog.ui" line="257"/>
<source>Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="354"/>
+ <location filename="settingsdialog.ui" line="280"/>
<source>Directory where mods are stored.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="357"/>
+ <location filename="settingsdialog.ui" line="283"/>
<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="381"/>
+ <location filename="settingsdialog.ui" line="307"/>
<source>Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="401"/>
+ <location filename="settingsdialog.ui" line="327"/>
<source>Managed Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="408"/>
+ <location filename="settingsdialog.ui" line="334"/>
<source>Base Directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="415"/>
+ <location filename="settingsdialog.ui" line="341"/>
<source>Use %BASE_DIR% to refer to the Base Directory.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="444"/>
+ <location filename="settingsdialog.ui" line="363"/>
<source>Important: All directories have to be writeable!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="452"/>
- <location filename="settingsdialog.ui" line="468"/>
+ <location filename="settingsdialog.ui" line="371"/>
+ <location filename="settingsdialog.ui" line="387"/>
<source>Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="458"/>
+ <location filename="settingsdialog.ui" line="377"/>
<source>Allows automatic log-in when the Nexus-Page for the game is clicked.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="461"/>
+ <location filename="settingsdialog.ui" line="380"/>
<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; }
@@ -6210,149 +5811,137 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="477"/>
- <source>If checked and if correct credentials are entered below, log-in to Nexus (for browsing and downloading) is automatic.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="480"/>
- <source>Automatically Log-In to Nexus</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="489"/>
- <location filename="settingsdialog.ui" line="689"/>
- <source>Username</source>
+ <location filename="settingsdialog.ui" line="395"/>
+ <source>Connect to Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="503"/>
- <location filename="settingsdialog.ui" line="699"/>
- <source>Password</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="524"/>
+ <location filename="settingsdialog.ui" line="406"/>
<source>Remove cache and cookies. Forces a new login.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="527"/>
+ <location filename="settingsdialog.ui" line="409"/>
<source>Clear Cache</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="558"/>
+ <location filename="settingsdialog.ui" line="438"/>
<source>Disable automatic internet features</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="561"/>
+ <location filename="settingsdialog.ui" line="441"/>
<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="564"/>
+ <location filename="settingsdialog.ui" line="444"/>
<source>Offline Mode</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="571"/>
+ <location filename="settingsdialog.ui" line="451"/>
<source>Use a proxy for network connections.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="574"/>
+ <location filename="settingsdialog.ui" line="454"/>
<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="577"/>
+ <location filename="settingsdialog.ui" line="457"/>
<source>Use HTTP Proxy (Uses System Settings)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="584"/>
- <source>Endorsement Integration</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="598"/>
+ <location filename="settingsdialog.ui" line="466"/>
<source>Associate with &quot;Download with manager&quot; links</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="627"/>
+ <location filename="settingsdialog.ui" line="495"/>
<source>Known Servers (updated on download)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="648"/>
+ <location filename="settingsdialog.ui" line="516"/>
<source>Preferred Servers (Drag &amp; Drop)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="683"/>
+ <location filename="settingsdialog.ui" line="551"/>
<source>Steam</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="729"/>
+ <location filename="settingsdialog.ui" line="557"/>
+ <source>Username</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="567"/>
+ <source>Password</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="597"/>
<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="756"/>
+ <location filename="settingsdialog.ui" line="624"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="781"/>
+ <location filename="settingsdialog.ui" line="646"/>
<source>Author:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="795"/>
+ <location filename="settingsdialog.ui" line="660"/>
<source>Version:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="809"/>
+ <location filename="settingsdialog.ui" line="674"/>
<source>Description:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="847"/>
+ <location filename="settingsdialog.ui" line="712"/>
<source>Key</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="852"/>
+ <location filename="settingsdialog.ui" line="717"/>
<source>Value</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="864"/>
+ <location filename="settingsdialog.ui" line="729"/>
<source>Blacklisted Plugins (use &lt;del&gt; to remove):</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="875"/>
+ <location filename="settingsdialog.ui" line="740"/>
<source>Workarounds</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="883"/>
+ <location filename="settingsdialog.ui" line="748"/>
<source>Steam App ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="903"/>
+ <location filename="settingsdialog.ui" line="768"/>
<source>The Steam AppID for your game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="906"/>
+ <location filename="settingsdialog.ui" line="771"/>
<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; }
@@ -6368,17 +5957,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="937"/>
+ <location filename="settingsdialog.ui" line="802"/>
<source>Load Mechanism</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="957"/>
+ <location filename="settingsdialog.ui" line="822"/>
<source>Select loading mechanism. See help for details.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="960"/>
+ <location filename="settingsdialog.ui" line="825"/>
<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.
@@ -6389,17 +5978,17 @@ 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="977"/>
+ <location filename="settingsdialog.ui" line="842"/>
<source>NMM Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="997"/>
+ <location filename="settingsdialog.ui" line="862"/>
<source>The Version of Nexus Mod Manager to impersonate.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1000"/>
+ <location filename="settingsdialog.ui" line="865"/>
<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.
@@ -6408,178 +5997,127 @@ tl;dr-version: If Nexus-features don&apos;t work, insert the current version num
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1027"/>
+ <location filename="settingsdialog.ui" line="890"/>
<source>Enforces that inactive ESPs and ESMs are never loaded.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1030"/>
+ <location filename="settingsdialog.ui" line="893"/>
<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="1034"/>
+ <location filename="settingsdialog.ui" line="897"/>
<source>Hide inactive ESPs/ESMs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1041"/>
- <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="1044"/>
- <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="1050"/>
- <source>Display mods installed outside MO</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1060"/>
+ <location filename="settingsdialog.ui" line="904"/>
<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="1063"/>
+ <location filename="settingsdialog.ui" line="907"/>
<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="1067"/>
+ <location filename="settingsdialog.ui" line="911"/>
<source>Force-enable game files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1077"/>
- <location filename="settingsdialog.ui" line="1080"/>
- <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="1083"/>
- <source>Lock GUI when running executable</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1093"/>
- <source>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</source>
+ <location filename="settingsdialog.ui" line="921"/>
+ <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="1096"/>
- <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>
+ <location filename="settingsdialog.ui" line="924"/>
+ <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="1099"/>
- <source>Enable parsing of Archives (Experimental Feature)</source>
+ <location filename="settingsdialog.ui" line="930"/>
+ <source>Display mods installed outside MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1111"/>
- <location filename="settingsdialog.ui" line="1115"/>
+ <location filename="settingsdialog.ui" line="940"/>
+ <location filename="settingsdialog.ui" line="944"/>
<source>For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles.
For the other games this is not a sufficient replacement for AI!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1119"/>
+ <location filename="settingsdialog.ui" line="948"/>
<source>Back-date BSAs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1130"/>
- <source>Add executables to the blacklist to prevent them from
-accessing the virtual file system. This is useful to prevent
-unintended programs from being hooked. Hooking unintended
-programs may affect the execution of these programs or the
-programs you are intentionally running.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1137"/>
- <source>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1140"/>
- <source>Configure Executables Blacklist</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1150"/>
- <location filename="settingsdialog.ui" line="1153"/>
- <source>Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.</source>
+ <location filename="settingsdialog.ui" line="972"/>
+ <source>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1156"/>
- <source>Reset Window Geometries</source>
+ <location filename="settingsdialog.ui" line="983"/>
+ <source>Diagnostics</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1179"/>
- <source>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</source>
+ <location filename="settingsdialog.ui" line="991"/>
+ <source>Log Level</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1190"/>
- <source>Diagnostics</source>
+ <location filename="settingsdialog.ui" line="998"/>
+ <source>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1198"/>
- <source>Max Dumps To Keep</source>
+ <location filename="settingsdialog.ui" line="1001"/>
+ <source>
+ 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.
+ </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1218"/>
- <source>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</source>
+ <location filename="settingsdialog.ui" line="1008"/>
+ <source>Debug</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1221"/>
- <source>
- 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.
- </source>
+ <location filename="settingsdialog.ui" line="1013"/>
+ <source>Info (recommended)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1233"/>
- <source>Hint: right click link and copy link location</source>
+ <location filename="settingsdialog.ui" line="1018"/>
+ <source>Warning</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1236"/>
- <source>
- 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.
- </source>
+ <location filename="settingsdialog.ui" line="1023"/>
+ <source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1253"/>
+ <location filename="settingsdialog.ui" line="1048"/>
<source>Crash Dumps</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1260"/>
+ <location filename="settingsdialog.ui" line="1055"/>
<source>Decides which type of crash dumps are collected when injected processes crash.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1263"/>
+ <location filename="settingsdialog.ui" line="1058"/>
<source>
Decides which type of crash dumps are collected when injected processes crash.
&quot;None&quot; Disables the generation of crash dumps by MO.
@@ -6590,131 +6128,105 @@ programs you are intentionally running.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1273"/>
+ <location filename="settingsdialog.ui" line="1068"/>
<source>None</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1278"/>
+ <location filename="settingsdialog.ui" line="1073"/>
<source>Mini (recommended)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1283"/>
+ <location filename="settingsdialog.ui" line="1078"/>
<source>Data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1288"/>
+ <location filename="settingsdialog.ui" line="1083"/>
<source>Full</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1316"/>
- <source>Log Level</source>
+ <location filename="settingsdialog.ui" line="1095"/>
+ <source>Max Dumps To Keep</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1323"/>
- <source>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</source>
+ <location filename="settingsdialog.ui" line="1115"/>
+ <source>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1326"/>
+ <location filename="settingsdialog.ui" line="1118"/>
<source>
- 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.
+ 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.
</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1333"/>
- <source>Debug</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1338"/>
- <source>Info (recommended)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1343"/>
- <source>Warning</source>
+ <location filename="settingsdialog.ui" line="1130"/>
+ <source>Hint: right click link and copy link location</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1348"/>
- <source>Error</source>
+ <location filename="settingsdialog.ui" line="1133"/>
+ <source>
+ 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.
+ </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="99"/>
+ <location filename="settingsdialog.cpp" line="76"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="100"/>
+ <location filename="settingsdialog.cpp" line="77"/>
<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="144"/>
- <source>Executables Blacklist</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.cpp" line="145"/>
- <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.
-
-Example:
- Chrome.exe
- Firefox.exe</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.cpp" line="178"/>
+ <location filename="settingsdialog.cpp" line="125"/>
<source>Select base directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="189"/>
+ <location filename="settingsdialog.cpp" line="136"/>
<source>Select download directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="200"/>
+ <location filename="settingsdialog.cpp" line="147"/>
<source>Select mod directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="211"/>
+ <location filename="settingsdialog.cpp" line="158"/>
<source>Select cache directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="222"/>
+ <location filename="settingsdialog.cpp" line="169"/>
<source>Select profiles directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="233"/>
+ <location filename="settingsdialog.cpp" line="180"/>
<source>Select overwrite directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="243"/>
- <source>Select game executable</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.cpp" line="322"/>
+ <location filename="settingsdialog.cpp" line="188"/>
<source>Confirm?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="323"/>
+ <location filename="settingsdialog.cpp" line="189"/>
<source>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box. Continue?</source>
<translation type="unfinished"></translation>
</message>
@@ -6826,7 +6338,7 @@ Example:
<name>TransferSavesDialog</name>
<message>
<location filename="transfersavesdialog.ui" line="14"/>
- <source>Transfer Save Games</source>
+ <source>Transfer Savegames</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -6918,7 +6430,7 @@ On Windows XP:
<context>
<name>UsvfsConnector</name>
<message>
- <location filename="usvfsconnector.cpp" line="163"/>
+ <location filename="usvfsconnector.cpp" line="157"/>
<source>Preparing vfs</source>
<translation type="unfinished"></translation>
</message>
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index a668de6b..dca855c7 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -1,2485 +1,2461 @@
-#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 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_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(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::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", qUtf8Printable(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", qUtf8Printable(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();
-}
-
-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());
-}
-
-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.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()));
- }
- }
- } else {
- qWarning("directory not found: %1", qUtf8Printable(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 {
- qWarning("file not found: %1", qUtf8Printable(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;
- 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)) {
- 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));
- 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)
-{
- 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);
- }
- }
-
- 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 (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", 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;
-}
+#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 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());
+ 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);
+}
+
+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()
+{
+ 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(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 (!retry && 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());
+}
+
+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.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()));
+ }
+ }
+ } else {
+ qWarning("directory not found: %1", qUtf8Printable(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 {
+ qWarning("file not found: %1", qUtf8Printable(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;
+ 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)) {
+ 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));
+ 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)
+{
+ 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);
+ }
+ }
+
+ 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 68d4e7e4..0a4cff6c 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -245,7 +245,7 @@ public slots:
void requestDownload(const QUrl &url, QNetworkReply *reply);
void downloadRequestedNXM(const QString &url);
- bool nexusLogin(bool retry = false);
+ bool nexusApi(bool retry = false);
signals:
@@ -267,7 +267,7 @@ 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);
@@ -341,7 +341,6 @@ private:
QThread m_RefresherThread;
- bool m_AskForNexusPW;
bool m_DirectoryUpdate;
bool m_ArchivesInit;
bool m_ArchiveParsing{ m_Settings.archiveParsing() };
diff --git a/src/settings.cpp b/src/settings.cpp
index 0ce647fd..7a1d6737 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -332,15 +332,12 @@ QString Settings::getNMMVersion() const
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 {
+ if (m_Settings.value("Settings/nexus_api_key", "").toString().isEmpty())
return false;
- }
+ apiKey = deObfuscate(m_Settings.value("Settings/nexus_api_key", "").toString());
+ return true;
}
bool Settings::getSteamLogin(QString &username, QString &password) const
@@ -431,11 +428,9 @@ QString Settings::executablesBlacklist() const
).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));
+ m_Settings.setValue("Settings/nexus_api_key", obfuscate(apiKey));
}
void Settings::setSteamLogin(QString username, QString password)
@@ -674,11 +669,29 @@ void Settings::resetDialogs()
QuestionBoxMemory::resetDialogs();
}
+void Settings::processApiKey(const QString &apiKey)
+{
+ m_Settings.setValue("Settings/nexus_api_key", obfuscate(apiKey));
+}
+
+void Settings::checkApiKey(QPushButton *nexusButton)
+{
+ if (m_Settings.value("Settings/nexus_api_key", "").toString().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 *)));
std::vector<std::unique_ptr<SettingsTab>> tabs;
@@ -960,9 +973,7 @@ 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"))
@@ -970,10 +981,9 @@ Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog)
dialog.findChild<QListWidget *>("preferredServersList"))
, m_endorsementBox(dialog.findChild<QCheckBox *>("endorsementBox"))
{
- 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 (!m_Settings.value("Settings/nexus_api_key", "").toString().isEmpty()) {
+ m_nexusConnect->setText("Nexus API Key Stored");
+ m_nexusConnect->setDisabled(true);
}
m_offlineBox->setChecked(parent->offlineMode());
@@ -1009,6 +1019,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());
@@ -1018,6 +1029,7 @@ 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());
diff --git a/src/settings.h b/src/settings.h
index 0286d4db..49e5a5b6 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -25,6 +25,7 @@ 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>
@@ -188,7 +189,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
@@ -249,7 +250,7 @@ public:
* @param username username
* @param password password
*/
- void setNexusLogin(QString username, QString password);
+ void setNexusApiKey(QString apiKey);
/**
* @brief set the steam login information
@@ -475,13 +476,10 @@ 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;
@@ -537,6 +535,8 @@ private:
private slots:
void resetDialogs();
+ void processApiKey(const QString &);
+ void checkApiKey(QPushButton *nexusButton);
signals:
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 1bbc256f..4843dea5 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>
@@ -46,6 +51,7 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, QWidget *parent
: TutorableDialog("SettingsDialog", parent)
, ui(new Ui::SettingsDialog)
, m_PluginContainer(pluginContainer)
+ , m_nexusLogin(new QWebSocket)
{
ui->setupUi(this);
ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}");
@@ -53,6 +59,9 @@ 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(textMessageReceived(const QString &)), this, SLOT(receiveApiKey(const QString &)));
+ connect(m_nexusLogin, SIGNAL(disconnected()), this, SLOT(completeApiConnection()));
}
SettingsDialog::~SettingsDialog()
@@ -326,6 +335,39 @@ void SettingsDialog::on_resetDialogsButton_clicked()
}
}
+void SettingsDialog::on_nexusConnect_clicked()
+{
+ ui->nexusConnect->setText("Connecting the API...");
+ ui->nexusConnect->setDisabled(true);
+ QUrl url = QUrl("wss://sso.nexusmods.com:8443");
+ m_nexusLogin->open(url);
+}
+
+void SettingsDialog::dispatchLogin()
+{
+ QJsonObject login;
+ boost::uuids::random_generator generator;
+ boost::uuids::uuid sessionId = generator();
+ login.insert(QString("id"), QJsonValue(QString(boost::uuids::to_string(sessionId).c_str())));
+ login.insert(QString("appid"), QJsonValue(QString("MO2")));
+ QJsonDocument loginDoc(login);
+ QString finalMessage(loginDoc.toJson());
+ m_nexusLogin->sendTextMessage(finalMessage);
+ QDesktopServices::openUrl(QUrl(QString("https://www.nexusmods.com/sso?id=") + QString(boost::uuids::to_string(sessionId).c_str())));
+}
+
+void SettingsDialog::receiveApiKey(const QString &apiKey)
+{
+ emit processApiKey(apiKey);
+ m_nexusLogin->close();
+ ui->nexusConnect->setText("Nexus API Key Stored");
+}
+
+void SettingsDialog::completeApiConnection()
+{
+ emit closeApiConnection(ui->nexusConnect);
+}
+
void SettingsDialog::storeSettings(QListWidgetItem *pluginItem)
{
if (pluginItem != nullptr) {
diff --git a/src/settingsdialog.h b/src/settingsdialog.h
index 6357f064..90b60c91 100644
--- a/src/settingsdialog.h
+++ b/src/settingsdialog.h
@@ -1,163 +1,177 @@
-/*
-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;
-
- void setButtonColor(QPushButton *button, QColor &color);
-
-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 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();
-
- 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_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();
-
-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;
-
- 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>
+
+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 *);
+
+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();
+
+ 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_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 receiveApiKey(const QString &apiKey);
+
+ void completeApiConnection();
+
+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;
+
+ QString m_ExecutableBlacklist;
+
+ QWebSocket *m_nexusLogin;
+};
+
+
+
+#endif // SETTINGSDIALOG_H
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index c865744e..d3628ab3 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -469,48 +469,11 @@ p, li { white-space: pre-wrap; }
</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">
+ <widget class="QPushButton" name="nexusConnect">
<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>
+ <string>Connect to Nexus</string>
</property>
</widget>
</item>
@@ -1387,9 +1350,6 @@ programs you are intentionally running.</string>
<tabstop>browseProfilesDirBtn</tabstop>
<tabstop>overwriteDirEdit</tabstop>
<tabstop>browseOverwriteDirBtn</tabstop>
- <tabstop>loginCheckBox</tabstop>
- <tabstop>usernameEdit</tabstop>
- <tabstop>passwordEdit</tabstop>
<tabstop>clearCacheButton</tabstop>
<tabstop>associateButton</tabstop>
<tabstop>knownServersList</tabstop>