summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/aboutdialog.cpp248
-rw-r--r--src/aboutdialog.h166
-rw-r--r--src/activatemodsdialog.cpp192
-rw-r--r--src/activatemodsdialog.h152
-rw-r--r--src/bbcode.cpp584
-rw-r--r--src/bbcode.h80
-rw-r--r--src/browserdialog.cpp594
-rw-r--r--src/browserdialog.h252
-rw-r--r--src/browserview.cpp152
-rw-r--r--src/browserview.h162
-rw-r--r--src/categories.cpp838
-rw-r--r--src/categories.h434
-rw-r--r--src/categoriesdialog.cpp500
-rw-r--r--src/categoriesdialog.h150
-rw-r--r--src/credentialsdialog.cpp86
-rw-r--r--src/credentialsdialog.h88
-rw-r--r--src/csvbuilder.cpp544
-rw-r--r--src/csvbuilder.h186
-rw-r--r--src/directoryrefresher.cpp1020
-rw-r--r--src/directoryrefresher.h422
-rw-r--r--src/downloadlist.cpp582
-rw-r--r--src/downloadlist.h208
-rw-r--r--src/downloadlistview.cpp894
-rw-r--r--src/downloadlistview.h256
-rw-r--r--src/editexecutablesdialog.h486
-rw-r--r--src/executableslist.cpp982
-rw-r--r--src/executableslist.h456
-rw-r--r--src/filedialogmemory.cpp164
-rw-r--r--src/filedialogmemory.h98
-rw-r--r--src/genericicondelegate.cpp58
-rw-r--r--src/genericicondelegate.h70
-rw-r--r--src/icondelegate.cpp174
-rw-r--r--src/icondelegate.h114
-rw-r--r--src/iuserinterface.h66
-rw-r--r--src/loghighlighter.cpp102
-rw-r--r--src/loghighlighter.h90
-rw-r--r--src/loglist.cpp794
-rw-r--r--src/loglist.h188
-rw-r--r--src/messagedialog.cpp216
-rw-r--r--src/messagedialog.h134
-rw-r--r--src/moapplication.cpp1326
-rw-r--r--src/moapplication.h222
-rw-r--r--src/modflagicondelegate.cpp186
-rw-r--r--src/modflagicondelegate.h70
-rw-r--r--src/modidlineedit.cpp2
-rw-r--r--src/modidlineedit.h2
-rw-r--r--src/modinfodialog.h520
-rw-r--r--src/modlist.h814
-rw-r--r--src/modlistsortproxy.cpp1410
-rw-r--r--src/modlistsortproxy.h344
-rw-r--r--src/modlistview.cpp2930
-rw-r--r--src/modlistview.h688
-rw-r--r--src/motddialog.cpp102
-rw-r--r--src/motddialog.h92
-rw-r--r--src/multiprocess.cpp204
-rw-r--r--src/multiprocess.h140
-rw-r--r--src/noeditdelegate.cpp20
-rw-r--r--src/noeditdelegate.h24
-rw-r--r--src/organizercore.h1004
-rw-r--r--src/organizerproxy.cpp750
-rw-r--r--src/organizerproxy.h260
-rw-r--r--src/overwriteinfodialog.cpp550
-rw-r--r--src/overwriteinfodialog.h256
-rw-r--r--src/persistentcookiejar.cpp148
-rw-r--r--src/persistentcookiejar.h60
-rw-r--r--src/plugincontainer.cpp2380
-rw-r--r--src/plugincontainer.h996
-rw-r--r--src/pluginlist.cpp3442
-rw-r--r--src/pluginlist.h838
-rw-r--r--src/pluginlistsortproxy.cpp306
-rw-r--r--src/pluginlistsortproxy.h136
-rw-r--r--src/previewdialog.cpp128
-rw-r--r--src/previewdialog.h80
-rw-r--r--src/previewgenerator.cpp116
-rw-r--r--src/previewgenerator.h94
-rw-r--r--src/problemsdialog.cpp226
-rw-r--r--src/problemsdialog.h90
-rw-r--r--src/profileinputdialog.cpp96
-rw-r--r--src/profileinputdialog.h88
-rw-r--r--src/profilesdialog.cpp806
-rw-r--r--src/profilesdialog.h264
-rw-r--r--src/qtgroupingproxy.cpp2076
-rw-r--r--src/qtgroupingproxy.h306
-rw-r--r--src/queryoverwritedialog.cpp134
-rw-r--r--src/queryoverwritedialog.h122
-rw-r--r--src/savetextasdialog.cpp100
-rw-r--r--src/savetextasdialog.h62
-rw-r--r--src/selectiondialog.cpp218
-rw-r--r--src/selectiondialog.h148
-rw-r--r--src/selfupdater.cpp722
-rw-r--r--src/selfupdater.h306
-rw-r--r--src/serverinfo.cpp356
-rw-r--r--src/serverinfo.h140
-rw-r--r--src/shared/appconfig.cpp66
-rw-r--r--src/shared/appconfig.h80
-rw-r--r--src/shared/directoryentry.cpp1956
-rw-r--r--src/shared/directoryentry.h642
-rw-r--r--src/shared/util.cpp888
-rw-r--r--src/shared/util.h178
-rw-r--r--src/shared/windows_error.cpp98
-rw-r--r--src/shared/windows_error.h88
-rw-r--r--src/spawn.cpp2164
-rw-r--r--src/spawn.h242
-rw-r--r--src/syncoverwritedialog.cpp342
-rw-r--r--src/syncoverwritedialog.h112
-rw-r--r--src/transfersavesdialog.cpp566
-rw-r--r--src/transfersavesdialog.h206
-rw-r--r--src/viewmarkingscrollbar.cpp110
-rw-r--r--src/viewmarkingscrollbar.h52
109 files changed, 23836 insertions, 23836 deletions
diff --git a/src/aboutdialog.cpp b/src/aboutdialog.cpp
index 98743e05..a8b8d81d 100644
--- a/src/aboutdialog.cpp
+++ b/src/aboutdialog.cpp
@@ -1,124 +1,124 @@
-/*
-Copyright (C) 2014 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 "aboutdialog.h"
-#include "ui_aboutdialog.h"
-#include "shared/util.h"
-#include <utility.h>
-
-#include <QApplication>
-#include <QLabel>
-#include <QListWidget>
-#include <QListWidgetItem>
-#include <QTextBrowser>
-#include <QVariant>
-#include <Qt>
-#include <QFontDatabase>
-
-AboutDialog::AboutDialog(const QString &version, QWidget *parent)
- : QDialog(parent)
- , ui(new Ui::AboutDialog)
-{
- ui->setupUi(this);
-
- m_LicenseFiles[LICENSE_LGPL3] = "LGPL-v3.0.txt";
- m_LicenseFiles[LICENSE_LGPL21] = "GNU-LGPL-v2.1.txt";
- m_LicenseFiles[LICENSE_GPL3] = "GPL-v3.0.txt";
- m_LicenseFiles[LICENSE_GPL2] = "GPL-v2.0.txt";
- m_LicenseFiles[LICENSE_BOOST] = "boost.txt";
- m_LicenseFiles[LICENSE_7ZIP] = "7zip.txt";
- m_LicenseFiles[LICENSE_CCBY3] = "BY-SA-v3.0.txt";
- m_LicenseFiles[LICENSE_ZLIB] = "zlib.txt";
- m_LicenseFiles[LICENSE_PYTHON] = "python.txt";
- m_LicenseFiles[LICENSE_SSL] = "openssl.txt";
- m_LicenseFiles[LICENSE_CPPTOML] = "cpptoml.txt";
- m_LicenseFiles[LICENSE_UDIS] = "udis86.txt";
- m_LicenseFiles[LICENSE_SPDLOG] = "spdlog.txt";
- m_LicenseFiles[LICENSE_FMT] = "fmt.txt";
- m_LicenseFiles[LICENSE_SIP] = "sip.txt";
- m_LicenseFiles[LICENSE_CASTLE] = "Castle.txt";
- m_LicenseFiles[LICENSE_ANTLR] = "AntlrBuildTask.txt";
- m_LicenseFiles[LICENSE_DXTEX] = "DXTex.txt";
-
- addLicense("Qt", LICENSE_LGPL3);
- addLicense("Qt Json", LICENSE_GPL3);
- addLicense("Boost Library", LICENSE_BOOST);
- addLicense("7-zip", LICENSE_7ZIP);
- addLicense("ZLib", LICENSE_NONE);
- addLicense("Tango Icon Theme", LICENSE_NONE);
- addLicense("RRZE Icon Set", LICENSE_CCBY3);
- addLicense("Icons by Lorc, Delapouite and sbed available on http://game-icons.net", LICENSE_CCBY3);
- addLicense("Castle Core", LICENSE_CASTLE);
- addLicense("ANTLR", LICENSE_ANTLR);
- addLicense("LOOT", LICENSE_GPL3);
- addLicense("Python", LICENSE_PYTHON);
- addLicense("OpenSSL", LICENSE_SSL);
- addLicense("cpptoml", LICENSE_CPPTOML);
- addLicense("Udis86", LICENSE_UDIS);
- addLicense("spdlog", LICENSE_SPDLOG);
- addLicense("{fmt}", LICENSE_FMT);
- addLicense("SIP", LICENSE_SIP);
- addLicense("DXTex Headers", LICENSE_DXTEX);
-
- ui->nameLabel->setText(QString("<span style=\"font-size:12pt; font-weight:600;\">%1 %2</span>").arg(ui->nameLabel->text()).arg(version));
-#if defined(HGID)
- ui->revisionLabel->setText(ui->revisionLabel->text() + " " + HGID);
-#elif defined(GITID)
- ui->revisionLabel->setText(ui->revisionLabel->text() + " " + GITID);
-#else
- ui->revisionLabel->setText(ui->revisionLabel->text() + " unknown");
-#endif
-
-
- ui->usvfsLabel->setText(ui->usvfsLabel->text() + " " + MOShared::getUsvfsVersionString());
- ui->licenseText->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
-}
-
-
-AboutDialog::~AboutDialog()
-{
- delete ui;
-}
-
-
-void AboutDialog::addLicense(const QString &name, Licenses license)
-{
- QListWidgetItem *item = new QListWidgetItem(name);
- item->setData(Qt::UserRole, license);
- ui->creditsList->addItem(item);
-}
-
-
-void AboutDialog::on_creditsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem*)
-{
- auto iter = m_LicenseFiles.find(current->data(Qt::UserRole).toInt());
- if (iter != m_LicenseFiles.end()) {
- QString filePath = qApp->applicationDirPath() + "/licenses/" + iter->second;
- QString text = MOBase::readFileText(filePath);
- ui->licenseText->setText(text);
- } else {
- ui->licenseText->setText(tr("No license"));
- }
-}
-
-void AboutDialog::on_sourceText_linkActivated(const QString &link)
-{
- MOBase::shell::Open(QUrl(link));
-}
+/*
+Copyright (C) 2014 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 "aboutdialog.h"
+#include "ui_aboutdialog.h"
+#include "shared/util.h"
+#include <utility.h>
+
+#include <QApplication>
+#include <QLabel>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QTextBrowser>
+#include <QVariant>
+#include <Qt>
+#include <QFontDatabase>
+
+AboutDialog::AboutDialog(const QString &version, QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::AboutDialog)
+{
+ ui->setupUi(this);
+
+ m_LicenseFiles[LICENSE_LGPL3] = "LGPL-v3.0.txt";
+ m_LicenseFiles[LICENSE_LGPL21] = "GNU-LGPL-v2.1.txt";
+ m_LicenseFiles[LICENSE_GPL3] = "GPL-v3.0.txt";
+ m_LicenseFiles[LICENSE_GPL2] = "GPL-v2.0.txt";
+ m_LicenseFiles[LICENSE_BOOST] = "boost.txt";
+ m_LicenseFiles[LICENSE_7ZIP] = "7zip.txt";
+ m_LicenseFiles[LICENSE_CCBY3] = "BY-SA-v3.0.txt";
+ m_LicenseFiles[LICENSE_ZLIB] = "zlib.txt";
+ m_LicenseFiles[LICENSE_PYTHON] = "python.txt";
+ m_LicenseFiles[LICENSE_SSL] = "openssl.txt";
+ m_LicenseFiles[LICENSE_CPPTOML] = "cpptoml.txt";
+ m_LicenseFiles[LICENSE_UDIS] = "udis86.txt";
+ m_LicenseFiles[LICENSE_SPDLOG] = "spdlog.txt";
+ m_LicenseFiles[LICENSE_FMT] = "fmt.txt";
+ m_LicenseFiles[LICENSE_SIP] = "sip.txt";
+ m_LicenseFiles[LICENSE_CASTLE] = "Castle.txt";
+ m_LicenseFiles[LICENSE_ANTLR] = "AntlrBuildTask.txt";
+ m_LicenseFiles[LICENSE_DXTEX] = "DXTex.txt";
+
+ addLicense("Qt", LICENSE_LGPL3);
+ addLicense("Qt Json", LICENSE_GPL3);
+ addLicense("Boost Library", LICENSE_BOOST);
+ addLicense("7-zip", LICENSE_7ZIP);
+ addLicense("ZLib", LICENSE_NONE);
+ addLicense("Tango Icon Theme", LICENSE_NONE);
+ addLicense("RRZE Icon Set", LICENSE_CCBY3);
+ addLicense("Icons by Lorc, Delapouite and sbed available on http://game-icons.net", LICENSE_CCBY3);
+ addLicense("Castle Core", LICENSE_CASTLE);
+ addLicense("ANTLR", LICENSE_ANTLR);
+ addLicense("LOOT", LICENSE_GPL3);
+ addLicense("Python", LICENSE_PYTHON);
+ addLicense("OpenSSL", LICENSE_SSL);
+ addLicense("cpptoml", LICENSE_CPPTOML);
+ addLicense("Udis86", LICENSE_UDIS);
+ addLicense("spdlog", LICENSE_SPDLOG);
+ addLicense("{fmt}", LICENSE_FMT);
+ addLicense("SIP", LICENSE_SIP);
+ addLicense("DXTex Headers", LICENSE_DXTEX);
+
+ ui->nameLabel->setText(QString("<span style=\"font-size:12pt; font-weight:600;\">%1 %2</span>").arg(ui->nameLabel->text()).arg(version));
+#if defined(HGID)
+ ui->revisionLabel->setText(ui->revisionLabel->text() + " " + HGID);
+#elif defined(GITID)
+ ui->revisionLabel->setText(ui->revisionLabel->text() + " " + GITID);
+#else
+ ui->revisionLabel->setText(ui->revisionLabel->text() + " unknown");
+#endif
+
+
+ ui->usvfsLabel->setText(ui->usvfsLabel->text() + " " + MOShared::getUsvfsVersionString());
+ ui->licenseText->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
+}
+
+
+AboutDialog::~AboutDialog()
+{
+ delete ui;
+}
+
+
+void AboutDialog::addLicense(const QString &name, Licenses license)
+{
+ QListWidgetItem *item = new QListWidgetItem(name);
+ item->setData(Qt::UserRole, license);
+ ui->creditsList->addItem(item);
+}
+
+
+void AboutDialog::on_creditsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem*)
+{
+ auto iter = m_LicenseFiles.find(current->data(Qt::UserRole).toInt());
+ if (iter != m_LicenseFiles.end()) {
+ QString filePath = qApp->applicationDirPath() + "/licenses/" + iter->second;
+ QString text = MOBase::readFileText(filePath);
+ ui->licenseText->setText(text);
+ } else {
+ ui->licenseText->setText(tr("No license"));
+ }
+}
+
+void AboutDialog::on_sourceText_linkActivated(const QString &link)
+{
+ MOBase::shell::Open(QUrl(link));
+}
diff --git a/src/aboutdialog.h b/src/aboutdialog.h
index 02d840ec..f029f931 100644
--- a/src/aboutdialog.h
+++ b/src/aboutdialog.h
@@ -1,83 +1,83 @@
-#ifndef ABOUTDIALOG_H
-/*
-Copyright (C) 2014 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/>.
-*/
-
-
-#define ABOUTDIALOG_H
-
-#include <QDialog>
-class QListWidgetItem;
-#include <QObject>
-#include <QString>
-
-#include <map>
-
-namespace Ui {
- class AboutDialog;
-}
-
-class AboutDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit AboutDialog(const QString &version, QWidget *parent = 0);
- ~AboutDialog();
-
-private:
-
- enum Licenses {
- LICENSE_NONE,
- LICENSE_LGPL3,
- LICENSE_LGPL21,
- LICENSE_GPL3,
- LICENSE_GPL2,
- LICENSE_BOOST,
- LICENSE_CCBY3,
- LICENSE_PYTHON,
- LICENSE_SSL,
- LICENSE_CPPTOML,
- LICENSE_7ZIP,
- LICENSE_ZLIB,
- LICENSE_UDIS,
- LICENSE_SPDLOG,
- LICENSE_FMT,
- LICENSE_SIP,
- LICENSE_CASTLE,
- LICENSE_ANTLR,
- LICENSE_DXTEX
- };
-
-private:
-
- void addLicense(const QString &name, Licenses license);
-
-private slots:
- void on_creditsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_sourceText_linkActivated(const QString &link);
-
-private:
-
- Ui::AboutDialog *ui;
-
- std::map<int, QString> m_LicenseFiles;
-
-};
-
-#endif // ABOUTDIALOG_H
+#ifndef ABOUTDIALOG_H
+/*
+Copyright (C) 2014 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/>.
+*/
+
+
+#define ABOUTDIALOG_H
+
+#include <QDialog>
+class QListWidgetItem;
+#include <QObject>
+#include <QString>
+
+#include <map>
+
+namespace Ui {
+ class AboutDialog;
+}
+
+class AboutDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit AboutDialog(const QString &version, QWidget *parent = 0);
+ ~AboutDialog();
+
+private:
+
+ enum Licenses {
+ LICENSE_NONE,
+ LICENSE_LGPL3,
+ LICENSE_LGPL21,
+ LICENSE_GPL3,
+ LICENSE_GPL2,
+ LICENSE_BOOST,
+ LICENSE_CCBY3,
+ LICENSE_PYTHON,
+ LICENSE_SSL,
+ LICENSE_CPPTOML,
+ LICENSE_7ZIP,
+ LICENSE_ZLIB,
+ LICENSE_UDIS,
+ LICENSE_SPDLOG,
+ LICENSE_FMT,
+ LICENSE_SIP,
+ LICENSE_CASTLE,
+ LICENSE_ANTLR,
+ LICENSE_DXTEX
+ };
+
+private:
+
+ void addLicense(const QString &name, Licenses license);
+
+private slots:
+ void on_creditsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
+ void on_sourceText_linkActivated(const QString &link);
+
+private:
+
+ Ui::AboutDialog *ui;
+
+ std::map<int, QString> m_LicenseFiles;
+
+};
+
+#endif // ABOUTDIALOG_H
diff --git a/src/activatemodsdialog.cpp b/src/activatemodsdialog.cpp
index c7e3dca2..4870d62b 100644
--- a/src/activatemodsdialog.cpp
+++ b/src/activatemodsdialog.cpp
@@ -1,96 +1,96 @@
-/*
-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 "activatemodsdialog.h"
-#include "ui_activatemodsdialog.h"
-
-#include <QComboBox>
-#include <QHeaderView>
-#include <QLabel>
-#include <QString>
-#include <QTableWidget>
-
-#include <QtGlobal>
-
-ActivateModsDialog::ActivateModsDialog(SaveGameInfo::MissingAssets const &missingAssets, QWidget *parent)
- : TutorableDialog("ActivateMods", parent), ui(new Ui::ActivateModsDialog)
-{
- ui->setupUi(this);
-
- QTableWidget *modsTable = findChild<QTableWidget*>("modsTable");
- QHeaderView *headerView = modsTable->horizontalHeader();
- headerView->setSectionResizeMode(0, QHeaderView::Stretch);
- headerView->setSectionResizeMode(1, QHeaderView::Interactive);
-
- int row = 0;
-
- modsTable->setRowCount(missingAssets.size());
-
- for (SaveGameInfo::MissingAssets::const_iterator espIter = missingAssets.begin();
- espIter != missingAssets.end(); ++espIter, ++row) {
- modsTable->setCellWidget(row, 0, new QLabel(espIter.key()));
- if (espIter->size() == 0) {
- modsTable->setCellWidget(row, 1, new QLabel(tr("not found")));
- } else {
- QComboBox* combo = new QComboBox();
- for (QString const &mod : espIter.value()) {
- combo->addItem(mod);
- }
- modsTable->setCellWidget(row, 1, combo);
- }
- }
-}
-
-
-ActivateModsDialog::~ActivateModsDialog()
-{
- delete ui;
-}
-
-
-std::set<QString> ActivateModsDialog::getModsToActivate()
-{
- std::set<QString> result;
- QTableWidget *modsTable = findChild<QTableWidget*>("modsTable");
-
- for (int row = 0; row < modsTable->rowCount(); ++row) {
- QComboBox *comboBox = dynamic_cast<QComboBox*>(modsTable->cellWidget(row, 1));
- if (comboBox != nullptr) {
- result.insert(comboBox->currentText());
- }
- }
- return result;
-}
-
-
-std::set<QString> ActivateModsDialog::getESPsToActivate()
-{
- std::set<QString> result;
- QTableWidget *modsTable = findChild<QTableWidget*>("modsTable");
-
- for (int row = 0; row < modsTable->rowCount(); ++row) {
- QComboBox *comboBox = dynamic_cast<QComboBox*>(modsTable->cellWidget(row, 1));
- if (comboBox != nullptr) {
- QLabel *espName = dynamic_cast<QLabel*>(modsTable->cellWidget(row, 0));
-
- result.insert(espName->text());
- }
- }
- return result;
-}
+/*
+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 "activatemodsdialog.h"
+#include "ui_activatemodsdialog.h"
+
+#include <QComboBox>
+#include <QHeaderView>
+#include <QLabel>
+#include <QString>
+#include <QTableWidget>
+
+#include <QtGlobal>
+
+ActivateModsDialog::ActivateModsDialog(SaveGameInfo::MissingAssets const &missingAssets, QWidget *parent)
+ : TutorableDialog("ActivateMods", parent), ui(new Ui::ActivateModsDialog)
+{
+ ui->setupUi(this);
+
+ QTableWidget *modsTable = findChild<QTableWidget*>("modsTable");
+ QHeaderView *headerView = modsTable->horizontalHeader();
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+ headerView->setSectionResizeMode(1, QHeaderView::Interactive);
+
+ int row = 0;
+
+ modsTable->setRowCount(missingAssets.size());
+
+ for (SaveGameInfo::MissingAssets::const_iterator espIter = missingAssets.begin();
+ espIter != missingAssets.end(); ++espIter, ++row) {
+ modsTable->setCellWidget(row, 0, new QLabel(espIter.key()));
+ if (espIter->size() == 0) {
+ modsTable->setCellWidget(row, 1, new QLabel(tr("not found")));
+ } else {
+ QComboBox* combo = new QComboBox();
+ for (QString const &mod : espIter.value()) {
+ combo->addItem(mod);
+ }
+ modsTable->setCellWidget(row, 1, combo);
+ }
+ }
+}
+
+
+ActivateModsDialog::~ActivateModsDialog()
+{
+ delete ui;
+}
+
+
+std::set<QString> ActivateModsDialog::getModsToActivate()
+{
+ std::set<QString> result;
+ QTableWidget *modsTable = findChild<QTableWidget*>("modsTable");
+
+ for (int row = 0; row < modsTable->rowCount(); ++row) {
+ QComboBox *comboBox = dynamic_cast<QComboBox*>(modsTable->cellWidget(row, 1));
+ if (comboBox != nullptr) {
+ result.insert(comboBox->currentText());
+ }
+ }
+ return result;
+}
+
+
+std::set<QString> ActivateModsDialog::getESPsToActivate()
+{
+ std::set<QString> result;
+ QTableWidget *modsTable = findChild<QTableWidget*>("modsTable");
+
+ for (int row = 0; row < modsTable->rowCount(); ++row) {
+ QComboBox *comboBox = dynamic_cast<QComboBox*>(modsTable->cellWidget(row, 1));
+ if (comboBox != nullptr) {
+ QLabel *espName = dynamic_cast<QLabel*>(modsTable->cellWidget(row, 0));
+
+ result.insert(espName->text());
+ }
+ }
+ return result;
+}
diff --git a/src/activatemodsdialog.h b/src/activatemodsdialog.h
index 56dda237..0c94d1d1 100644
--- a/src/activatemodsdialog.h
+++ b/src/activatemodsdialog.h
@@ -1,76 +1,76 @@
-/*
-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 ACTIVATEMODSDIALOG_H
-#define ACTIVATEMODSDIALOG_H
-
-#include "savegameinfo.h"
-#include "tutorabledialog.h"
-
-#include <QObject>
-
-class QString;
-class QWidget;
-
-#include <set>
-
-namespace Ui {
- class ActivateModsDialog;
-}
-
-/**
- * @brief Dialog that is used to batch activate/deactivate mods and plugins
- **/
-class ActivateModsDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT
-
-public:
- /**
- * @brief constructor
- *
- * @param missingPlugins a map containing missing plugins that need to be activated
- * @param parent ... Defaults to 0.
- **/
- explicit ActivateModsDialog(SaveGameInfo::MissingAssets const &missingAssets, QWidget *parent = 0);
- ~ActivateModsDialog();
-
- /**
- * @brief get a list of mods that the user chose to activate
- *
- * @note This can of ocurse only be called after the dialog has been displayed
- *
- * @return set< QString > the mods to activate
- **/
- std::set<QString> getModsToActivate();
-
- /**
- * @brief get a list of plugins that should be activated
- *
- * @return set< QString > the plugins to activate. This contains only plugins that become available after enabling the mods retrieved with getModsToActivate
- **/
- std::set<QString> getESPsToActivate();
-
-private slots:
-
-private:
- Ui::ActivateModsDialog *ui;
-};
-
-#endif // ACTIVATEMODSDIALOG_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 ACTIVATEMODSDIALOG_H
+#define ACTIVATEMODSDIALOG_H
+
+#include "savegameinfo.h"
+#include "tutorabledialog.h"
+
+#include <QObject>
+
+class QString;
+class QWidget;
+
+#include <set>
+
+namespace Ui {
+ class ActivateModsDialog;
+}
+
+/**
+ * @brief Dialog that is used to batch activate/deactivate mods and plugins
+ **/
+class ActivateModsDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ *
+ * @param missingPlugins a map containing missing plugins that need to be activated
+ * @param parent ... Defaults to 0.
+ **/
+ explicit ActivateModsDialog(SaveGameInfo::MissingAssets const &missingAssets, QWidget *parent = 0);
+ ~ActivateModsDialog();
+
+ /**
+ * @brief get a list of mods that the user chose to activate
+ *
+ * @note This can of ocurse only be called after the dialog has been displayed
+ *
+ * @return set< QString > the mods to activate
+ **/
+ std::set<QString> getModsToActivate();
+
+ /**
+ * @brief get a list of plugins that should be activated
+ *
+ * @return set< QString > the plugins to activate. This contains only plugins that become available after enabling the mods retrieved with getModsToActivate
+ **/
+ std::set<QString> getESPsToActivate();
+
+private slots:
+
+private:
+ Ui::ActivateModsDialog *ui;
+};
+
+#endif // ACTIVATEMODSDIALOG_H
diff --git a/src/bbcode.cpp b/src/bbcode.cpp
index 36b60a66..e143909f 100644
--- a/src/bbcode.cpp
+++ b/src/bbcode.cpp
@@ -1,292 +1,292 @@
-/*
-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 "bbcode.h"
-#include <log.h>
-#include <QRegularExpression>
-#include <map>
-
-namespace BBCode {
-
-namespace log = MOBase::log;
-
-class BBCodeMap {
-
- typedef std::map<QString, std::pair<QRegularExpression, QString> > TagMap;
-
-public:
-
- static BBCodeMap &instance() {
- static BBCodeMap s_Instance;
- return s_Instance;
- }
-
- QString convertTag(QString input, int &length)
- {
- // extract the tag name
- auto match = m_TagNameExp.match(input, 1, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption);
- QString tagName = match.captured(0).toLower();
- TagMap::iterator tagIter = m_TagMap.find(tagName);
- if (tagIter != m_TagMap.end()) {
- // recognized tag
- if (tagName.endsWith('=')) {
- tagName.chop(1);
- }
-
- int closeTagPos = 0;
- int nextTagPos = 0;
- int nextTagSearchIndex = input.indexOf("]");
- int closeTagLength = 0;
- if (tagName == "*") {
- // ends at the next bullet point
- closeTagPos = input.indexOf(QRegularExpression("(\\[\\*\\]|</ul>)", QRegularExpression::CaseInsensitiveOption), 3);
- // leave closeTagLength at 0 because we don't want to "eat" the next bullet point
- } else if (tagName == "line") {
- // ends immediately after the tag
- closeTagPos = 6;
- // leave closeTagLength at 0 because there is no close tag to skip over
- } else {
- QRegularExpression nextTag(QString("\\[%1[=\\]]?").arg(tagName), QRegularExpression::CaseInsensitiveOption);
- QString closeTag = QString("[/%1]").arg(tagName);
- closeTagPos = input.indexOf(closeTag, 0, Qt::CaseInsensitive);
- nextTagPos = nextTag.match(input, nextTagSearchIndex).capturedStart(0);
- while (nextTagPos != -1 && closeTagPos != -1 && nextTagPos < closeTagPos) {
- closeTagPos = input.indexOf(closeTag, closeTagPos + closeTag.size(), Qt::CaseInsensitive);
- nextTagSearchIndex = input.indexOf("]", nextTagPos);
- nextTagPos = nextTag.match(input, nextTagSearchIndex).capturedStart(0);
- }
- if (closeTagPos == -1) {
- // workaround to improve compatibility: add fake closing tag
- input.append(closeTag);
- closeTagPos = input.size() - closeTag.size();
- }
- closeTagLength = closeTag.size();
- }
-
- if (closeTagPos > -1) {
- length = closeTagPos + closeTagLength;
- QString temp = input.mid(0, length);
- tagIter->second.first.setPatternOptions(QRegularExpression::PatternOption::DotMatchesEverythingOption);
- auto match = tagIter->second.first.match(temp);
- if (match.hasMatch()) {
- if (tagIter->second.second.isEmpty()) {
- if (tagName == "color") {
- QString color = match.captured(1);
- QString content = match.captured(2);
- if (color.at(0) == '#') {
- return temp.replace(tagIter->second.first, QString("<font style=\"color: %1;\">%2</font>").arg(color, content));
- } else {
- auto colIter = m_ColorMap.find(color.toLower());
- if (colIter != m_ColorMap.end()) {
- color = colIter->second;
- }
- return temp.replace(tagIter->second.first, QString("<font style=\"color: #%1;\">%2</font>").arg(color, content));
- }
- } else {
- log::warn("don't know how to deal with tag {}", tagName);
- }
- } else {
- if (tagName == "*") {
- temp.remove(QRegularExpression("(\\[/\\*\\])?(<br/>)?$"));
- }
- return temp.replace(tagIter->second.first, tagIter->second.second);
- }
- } else {
- // expression doesn't match. either the input string is invalid
- // or the expression is
- log::warn("{} doesn't match the expression for {}", temp, tagName);
- length = 0;
- return QString();
- }
- }
- }
-
- // not a recognized tag or tag invalid
- length = 0;
- return QString();
- }
-
-private:
- BBCodeMap()
- : m_TagNameExp("[a-zA-Z*]*=?")
- {
- m_TagMap["b"] = std::make_pair(QRegularExpression("\\[b\\](.*)\\[/b\\]"),
- "<b>\\1</b>");
- m_TagMap["i"] = std::make_pair(QRegularExpression("\\[i\\](.*)\\[/i\\]"),
- "<i>\\1</i>");
- m_TagMap["u"] = std::make_pair(QRegularExpression("\\[u\\](.*)\\[/u\\]"),
- "<u>\\1</u>");
- m_TagMap["s"] = std::make_pair(QRegularExpression("\\[s\\](.*)\\[/s\\]"),
- "<s>\\1</s>");
- m_TagMap["sub"] = std::make_pair(QRegularExpression("\\[sub\\](.*)\\[/sub\\]"),
- "<sub>\\1</sub>");
- m_TagMap["sup"] = std::make_pair(QRegularExpression("\\[sup\\](.*)\\[/sup\\]"),
- "<sup>\\1</sup>");
- m_TagMap["size="] = std::make_pair(QRegularExpression("\\[size=([^\\]]*)\\](.*)\\[/size\\]"),
- "<font size=\"\\1\">\\2</font>");
- m_TagMap["color="] = std::make_pair(QRegularExpression("\\[color=([^\\]]*)\\](.*)\\[/color\\]"),
- "");
- m_TagMap["font="] = std::make_pair(QRegularExpression("\\[font=([^\\]]*)\\](.*)\\[/font\\]"),
- "<font style=\"font-family: \\1;\">\\2</font>");
- m_TagMap["center"] = std::make_pair(QRegularExpression("\\[center\\](.*)\\[/center\\]"),
- "<div align=\"center\">\\1</div>");
- m_TagMap["right"] = std::make_pair(QRegularExpression("\\[right\\](.*)\\[/right\\]"),
- "<div align=\"right\">\\1</div>");
- m_TagMap["quote"] = std::make_pair(QRegularExpression("\\[quote\\](.*)\\[/quote\\]"),
- "<figure class=\"quote\"><blockquote>\\1</blockquote></figure>");
- m_TagMap["quote="] = std::make_pair(QRegularExpression("\\[quote=([^\\]]*)\\](.*)\\[/quote\\]"),
- "<figure class=\"quote\"><blockquote>\\2</blockquote></figure>");
- m_TagMap["spoiler"] = std::make_pair(QRegularExpression("\\[spoiler\\](.*)\\[/spoiler\\]"),
- "<details><summary>Spoiler: <div class=\"bbc_spoiler_show\">Show</div></summary><div class=\"spoiler_content\">\\1</div></details>");
- m_TagMap["code"] = std::make_pair(QRegularExpression("\\[code\\](.*)\\[/code\\]"),
- "<code>\\1</code>");
- m_TagMap["heading"]= std::make_pair(QRegularExpression("\\[heading\\](.*)\\[/heading\\]"),
- "<h2><strong>\\1</strong></h2>");
- m_TagMap["line"] = std::make_pair(QRegularExpression("\\[line\\]"),
- "<hr>");
-
- // lists
- m_TagMap["list"] = std::make_pair(QRegularExpression("\\[list\\](.*)\\[/list\\]"),
- "<ul>\\1</ul>");
- m_TagMap["list="] = std::make_pair(QRegularExpression("\\[list.*\\](.*)\\[/list\\]"),
- "<ol>\\1</ol>");
- m_TagMap["ul"] = std::make_pair(QRegularExpression("\\[ul\\](.*)\\[/ul\\]"),
- "<ul>\\1</ul>");
- m_TagMap["ol"] = std::make_pair(QRegularExpression("\\[ol\\](.*)\\[/ol\\]"),
- "<ol>\\1</ol>");
- m_TagMap["li"] = std::make_pair(QRegularExpression("\\[li\\](.*)\\[/li\\]"),
- "<li>\\1</li>");
-
- // tables
- m_TagMap["table"] = std::make_pair(QRegularExpression("\\[table\\](.*)\\[/table\\]"),
- "<table>\\1</table>");
- m_TagMap["tr"] = std::make_pair(QRegularExpression("\\[tr\\](.*)\\[/tr\\]"),
- "<tr>\\1</tr>");
- m_TagMap["th"] = std::make_pair(QRegularExpression("\\[th\\](.*)\\[/th\\]"),
- "<th>\\1</th>");
- m_TagMap["td"] = std::make_pair(QRegularExpression("\\[td\\](.*)\\[/td\\]"),
- "<td>\\1</td>");
-
- // web content
- m_TagMap["url"] = std::make_pair(QRegularExpression("\\[url\\](.*)\\[/url\\]"),
- "<a href=\"\\1\">\\1</a>");
- m_TagMap["url="] = std::make_pair(QRegularExpression("\\[url=([^\\]]*)\\](.*)\\[/url\\]"),
- "<a href=\"\\1\">\\2</a>");
- m_TagMap["img"] = std::make_pair(QRegularExpression("\\[img(?:\\s*width=\\d+\\s*,?\\s*height=\\d+)?\\](.*)\\[/img\\]"),
- "<img src=\"\\1\">");
- m_TagMap["img="] = std::make_pair(QRegularExpression("\\[img=([^\\]]*)\\](.*)\\[/img\\]"),
- "<img src=\"\\2\" alt=\"\\1\">");
- m_TagMap["email="] = std::make_pair(QRegularExpression("\\[email=\"?([^\\]]*)\"?\\](.*)\\[/email\\]"),
- "<a href=\"mailto:\\1\">\\2</a>");
- m_TagMap["youtube"] = std::make_pair(QRegularExpression("\\[youtube\\](.*)\\[/youtube\\]"),
- "<a href=\"https://www.youtube.com/watch?v=\\1\">https://www.youtube.com/watch?v=\\1</a>");
-
-
- // make all patterns non-greedy and case-insensitive
- for (TagMap::iterator iter = m_TagMap.begin(); iter != m_TagMap.end(); ++iter) {
- iter->second.first.setPatternOptions(QRegularExpression::CaseInsensitiveOption | QRegularExpression::InvertedGreedinessOption);
- }
-
- // this tag is in fact greedy
- m_TagMap["*"] = std::make_pair(QRegularExpression("\\[\\*\\](.*)"),
- "<li>\\1</li>");
-
- m_ColorMap.insert(std::make_pair<QString, QString>("red", "FF0000"));
- m_ColorMap.insert(std::make_pair<QString, QString>("green", "00FF00"));
- m_ColorMap.insert(std::make_pair<QString, QString>("blue", "0000FF"));
- m_ColorMap.insert(std::make_pair<QString, QString>("black", "000000"));
- m_ColorMap.insert(std::make_pair<QString, QString>("gray", "7F7F7F"));
- m_ColorMap.insert(std::make_pair<QString, QString>("white", "FFFFFF"));
- m_ColorMap.insert(std::make_pair<QString, QString>("yellow", "FFFF00"));
- m_ColorMap.insert(std::make_pair<QString, QString>("cyan", "00FFFF"));
- m_ColorMap.insert(std::make_pair<QString, QString>("magenta", "FF00FF"));
- m_ColorMap.insert(std::make_pair<QString, QString>("brown", "A52A2A"));
- m_ColorMap.insert(std::make_pair<QString, QString>("orange", "FFA500"));
- m_ColorMap.insert(std::make_pair<QString, QString>("gold", "FFD700"));
- m_ColorMap.insert(std::make_pair<QString, QString>("deepskyblue", "00BFFF"));
- m_ColorMap.insert(std::make_pair<QString, QString>("salmon", "FA8072"));
- m_ColorMap.insert(std::make_pair<QString, QString>("dodgerblue", "1E90FF"));
- m_ColorMap.insert(std::make_pair<QString, QString>("greenyellow", "ADFF2F"));
- m_ColorMap.insert(std::make_pair<QString, QString>("peru", "CD853F"));
- }
-
-private:
-
- QRegularExpression m_TagNameExp;
- TagMap m_TagMap;
- std::map<QString, QString> m_ColorMap;
-};
-
-
-QString convertToHTML(const QString &inputParam)
-{
- // this code goes over the input string once and replaces all bbtags
- // it encounters. This function is called recursively for every replaced
- // string to convert nested tags.
- //
- // This could be implemented simpler by applying a set of regular expressions
- // for each recognized bb-tag one after the other but that would probably be
- // very inefficient (O(n^2)).
-
- QString input = inputParam.mid(0).replace("\r\n", "<br/>");
- input.replace("\\\"", "\"").replace("\\'", "'");
- QString result;
- int lastBlock = 0;
- int pos = 0;
-
- // iterate over the input buffer
- while ((pos = input.indexOf('[', lastBlock)) != -1) {
- // append everything between the previous tag-block and the current one
- result.append(input.mid(lastBlock, pos - lastBlock));
-
- if ((pos < (input.size() - 1)) && (input.at(pos + 1) == '/')) {
- // skip invalid end tag
- int tagEnd = input.indexOf(']', pos) + 1;
- if (tagEnd == 0) {
- //no closing tag found
- //move the pos up one so that the opening bracket is ignored next iteration
- pos++;
- }
- else {
- pos = tagEnd;
- }
- } else {
- // convert the tag and content if necessary
- int length = -1;
- QString replacement = BBCodeMap::instance().convertTag(input.mid(pos), length);
- if (length != 0) {
- result.append(convertToHTML(replacement));
- // length contains the number of characters in the original tag
- pos += length;
- } else {
- // nothing replaced
- result.append('[');
- ++pos;
- }
- }
- lastBlock = pos;
- }
-
- // append the remainder (everything after the last tag)
- result.append(input.mid(lastBlock));
- return result;
-}
-
-} // namespace BBCode
-
+/*
+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 "bbcode.h"
+#include <log.h>
+#include <QRegularExpression>
+#include <map>
+
+namespace BBCode {
+
+namespace log = MOBase::log;
+
+class BBCodeMap {
+
+ typedef std::map<QString, std::pair<QRegularExpression, QString> > TagMap;
+
+public:
+
+ static BBCodeMap &instance() {
+ static BBCodeMap s_Instance;
+ return s_Instance;
+ }
+
+ QString convertTag(QString input, int &length)
+ {
+ // extract the tag name
+ auto match = m_TagNameExp.match(input, 1, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption);
+ QString tagName = match.captured(0).toLower();
+ TagMap::iterator tagIter = m_TagMap.find(tagName);
+ if (tagIter != m_TagMap.end()) {
+ // recognized tag
+ if (tagName.endsWith('=')) {
+ tagName.chop(1);
+ }
+
+ int closeTagPos = 0;
+ int nextTagPos = 0;
+ int nextTagSearchIndex = input.indexOf("]");
+ int closeTagLength = 0;
+ if (tagName == "*") {
+ // ends at the next bullet point
+ closeTagPos = input.indexOf(QRegularExpression("(\\[\\*\\]|</ul>)", QRegularExpression::CaseInsensitiveOption), 3);
+ // leave closeTagLength at 0 because we don't want to "eat" the next bullet point
+ } else if (tagName == "line") {
+ // ends immediately after the tag
+ closeTagPos = 6;
+ // leave closeTagLength at 0 because there is no close tag to skip over
+ } else {
+ QRegularExpression nextTag(QString("\\[%1[=\\]]?").arg(tagName), QRegularExpression::CaseInsensitiveOption);
+ QString closeTag = QString("[/%1]").arg(tagName);
+ closeTagPos = input.indexOf(closeTag, 0, Qt::CaseInsensitive);
+ nextTagPos = nextTag.match(input, nextTagSearchIndex).capturedStart(0);
+ while (nextTagPos != -1 && closeTagPos != -1 && nextTagPos < closeTagPos) {
+ closeTagPos = input.indexOf(closeTag, closeTagPos + closeTag.size(), Qt::CaseInsensitive);
+ nextTagSearchIndex = input.indexOf("]", nextTagPos);
+ nextTagPos = nextTag.match(input, nextTagSearchIndex).capturedStart(0);
+ }
+ if (closeTagPos == -1) {
+ // workaround to improve compatibility: add fake closing tag
+ input.append(closeTag);
+ closeTagPos = input.size() - closeTag.size();
+ }
+ closeTagLength = closeTag.size();
+ }
+
+ if (closeTagPos > -1) {
+ length = closeTagPos + closeTagLength;
+ QString temp = input.mid(0, length);
+ tagIter->second.first.setPatternOptions(QRegularExpression::PatternOption::DotMatchesEverythingOption);
+ auto match = tagIter->second.first.match(temp);
+ if (match.hasMatch()) {
+ if (tagIter->second.second.isEmpty()) {
+ if (tagName == "color") {
+ QString color = match.captured(1);
+ QString content = match.captured(2);
+ if (color.at(0) == '#') {
+ return temp.replace(tagIter->second.first, QString("<font style=\"color: %1;\">%2</font>").arg(color, content));
+ } else {
+ auto colIter = m_ColorMap.find(color.toLower());
+ if (colIter != m_ColorMap.end()) {
+ color = colIter->second;
+ }
+ return temp.replace(tagIter->second.first, QString("<font style=\"color: #%1;\">%2</font>").arg(color, content));
+ }
+ } else {
+ log::warn("don't know how to deal with tag {}", tagName);
+ }
+ } else {
+ if (tagName == "*") {
+ temp.remove(QRegularExpression("(\\[/\\*\\])?(<br/>)?$"));
+ }
+ return temp.replace(tagIter->second.first, tagIter->second.second);
+ }
+ } else {
+ // expression doesn't match. either the input string is invalid
+ // or the expression is
+ log::warn("{} doesn't match the expression for {}", temp, tagName);
+ length = 0;
+ return QString();
+ }
+ }
+ }
+
+ // not a recognized tag or tag invalid
+ length = 0;
+ return QString();
+ }
+
+private:
+ BBCodeMap()
+ : m_TagNameExp("[a-zA-Z*]*=?")
+ {
+ m_TagMap["b"] = std::make_pair(QRegularExpression("\\[b\\](.*)\\[/b\\]"),
+ "<b>\\1</b>");
+ m_TagMap["i"] = std::make_pair(QRegularExpression("\\[i\\](.*)\\[/i\\]"),
+ "<i>\\1</i>");
+ m_TagMap["u"] = std::make_pair(QRegularExpression("\\[u\\](.*)\\[/u\\]"),
+ "<u>\\1</u>");
+ m_TagMap["s"] = std::make_pair(QRegularExpression("\\[s\\](.*)\\[/s\\]"),
+ "<s>\\1</s>");
+ m_TagMap["sub"] = std::make_pair(QRegularExpression("\\[sub\\](.*)\\[/sub\\]"),
+ "<sub>\\1</sub>");
+ m_TagMap["sup"] = std::make_pair(QRegularExpression("\\[sup\\](.*)\\[/sup\\]"),
+ "<sup>\\1</sup>");
+ m_TagMap["size="] = std::make_pair(QRegularExpression("\\[size=([^\\]]*)\\](.*)\\[/size\\]"),
+ "<font size=\"\\1\">\\2</font>");
+ m_TagMap["color="] = std::make_pair(QRegularExpression("\\[color=([^\\]]*)\\](.*)\\[/color\\]"),
+ "");
+ m_TagMap["font="] = std::make_pair(QRegularExpression("\\[font=([^\\]]*)\\](.*)\\[/font\\]"),
+ "<font style=\"font-family: \\1;\">\\2</font>");
+ m_TagMap["center"] = std::make_pair(QRegularExpression("\\[center\\](.*)\\[/center\\]"),
+ "<div align=\"center\">\\1</div>");
+ m_TagMap["right"] = std::make_pair(QRegularExpression("\\[right\\](.*)\\[/right\\]"),
+ "<div align=\"right\">\\1</div>");
+ m_TagMap["quote"] = std::make_pair(QRegularExpression("\\[quote\\](.*)\\[/quote\\]"),
+ "<figure class=\"quote\"><blockquote>\\1</blockquote></figure>");
+ m_TagMap["quote="] = std::make_pair(QRegularExpression("\\[quote=([^\\]]*)\\](.*)\\[/quote\\]"),
+ "<figure class=\"quote\"><blockquote>\\2</blockquote></figure>");
+ m_TagMap["spoiler"] = std::make_pair(QRegularExpression("\\[spoiler\\](.*)\\[/spoiler\\]"),
+ "<details><summary>Spoiler: <div class=\"bbc_spoiler_show\">Show</div></summary><div class=\"spoiler_content\">\\1</div></details>");
+ m_TagMap["code"] = std::make_pair(QRegularExpression("\\[code\\](.*)\\[/code\\]"),
+ "<code>\\1</code>");
+ m_TagMap["heading"]= std::make_pair(QRegularExpression("\\[heading\\](.*)\\[/heading\\]"),
+ "<h2><strong>\\1</strong></h2>");
+ m_TagMap["line"] = std::make_pair(QRegularExpression("\\[line\\]"),
+ "<hr>");
+
+ // lists
+ m_TagMap["list"] = std::make_pair(QRegularExpression("\\[list\\](.*)\\[/list\\]"),
+ "<ul>\\1</ul>");
+ m_TagMap["list="] = std::make_pair(QRegularExpression("\\[list.*\\](.*)\\[/list\\]"),
+ "<ol>\\1</ol>");
+ m_TagMap["ul"] = std::make_pair(QRegularExpression("\\[ul\\](.*)\\[/ul\\]"),
+ "<ul>\\1</ul>");
+ m_TagMap["ol"] = std::make_pair(QRegularExpression("\\[ol\\](.*)\\[/ol\\]"),
+ "<ol>\\1</ol>");
+ m_TagMap["li"] = std::make_pair(QRegularExpression("\\[li\\](.*)\\[/li\\]"),
+ "<li>\\1</li>");
+
+ // tables
+ m_TagMap["table"] = std::make_pair(QRegularExpression("\\[table\\](.*)\\[/table\\]"),
+ "<table>\\1</table>");
+ m_TagMap["tr"] = std::make_pair(QRegularExpression("\\[tr\\](.*)\\[/tr\\]"),
+ "<tr>\\1</tr>");
+ m_TagMap["th"] = std::make_pair(QRegularExpression("\\[th\\](.*)\\[/th\\]"),
+ "<th>\\1</th>");
+ m_TagMap["td"] = std::make_pair(QRegularExpression("\\[td\\](.*)\\[/td\\]"),
+ "<td>\\1</td>");
+
+ // web content
+ m_TagMap["url"] = std::make_pair(QRegularExpression("\\[url\\](.*)\\[/url\\]"),
+ "<a href=\"\\1\">\\1</a>");
+ m_TagMap["url="] = std::make_pair(QRegularExpression("\\[url=([^\\]]*)\\](.*)\\[/url\\]"),
+ "<a href=\"\\1\">\\2</a>");
+ m_TagMap["img"] = std::make_pair(QRegularExpression("\\[img(?:\\s*width=\\d+\\s*,?\\s*height=\\d+)?\\](.*)\\[/img\\]"),
+ "<img src=\"\\1\">");
+ m_TagMap["img="] = std::make_pair(QRegularExpression("\\[img=([^\\]]*)\\](.*)\\[/img\\]"),
+ "<img src=\"\\2\" alt=\"\\1\">");
+ m_TagMap["email="] = std::make_pair(QRegularExpression("\\[email=\"?([^\\]]*)\"?\\](.*)\\[/email\\]"),
+ "<a href=\"mailto:\\1\">\\2</a>");
+ m_TagMap["youtube"] = std::make_pair(QRegularExpression("\\[youtube\\](.*)\\[/youtube\\]"),
+ "<a href=\"https://www.youtube.com/watch?v=\\1\">https://www.youtube.com/watch?v=\\1</a>");
+
+
+ // make all patterns non-greedy and case-insensitive
+ for (TagMap::iterator iter = m_TagMap.begin(); iter != m_TagMap.end(); ++iter) {
+ iter->second.first.setPatternOptions(QRegularExpression::CaseInsensitiveOption | QRegularExpression::InvertedGreedinessOption);
+ }
+
+ // this tag is in fact greedy
+ m_TagMap["*"] = std::make_pair(QRegularExpression("\\[\\*\\](.*)"),
+ "<li>\\1</li>");
+
+ m_ColorMap.insert(std::make_pair<QString, QString>("red", "FF0000"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("green", "00FF00"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("blue", "0000FF"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("black", "000000"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("gray", "7F7F7F"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("white", "FFFFFF"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("yellow", "FFFF00"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("cyan", "00FFFF"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("magenta", "FF00FF"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("brown", "A52A2A"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("orange", "FFA500"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("gold", "FFD700"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("deepskyblue", "00BFFF"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("salmon", "FA8072"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("dodgerblue", "1E90FF"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("greenyellow", "ADFF2F"));
+ m_ColorMap.insert(std::make_pair<QString, QString>("peru", "CD853F"));
+ }
+
+private:
+
+ QRegularExpression m_TagNameExp;
+ TagMap m_TagMap;
+ std::map<QString, QString> m_ColorMap;
+};
+
+
+QString convertToHTML(const QString &inputParam)
+{
+ // this code goes over the input string once and replaces all bbtags
+ // it encounters. This function is called recursively for every replaced
+ // string to convert nested tags.
+ //
+ // This could be implemented simpler by applying a set of regular expressions
+ // for each recognized bb-tag one after the other but that would probably be
+ // very inefficient (O(n^2)).
+
+ QString input = inputParam.mid(0).replace("\r\n", "<br/>");
+ input.replace("\\\"", "\"").replace("\\'", "'");
+ QString result;
+ int lastBlock = 0;
+ int pos = 0;
+
+ // iterate over the input buffer
+ while ((pos = input.indexOf('[', lastBlock)) != -1) {
+ // append everything between the previous tag-block and the current one
+ result.append(input.mid(lastBlock, pos - lastBlock));
+
+ if ((pos < (input.size() - 1)) && (input.at(pos + 1) == '/')) {
+ // skip invalid end tag
+ int tagEnd = input.indexOf(']', pos) + 1;
+ if (tagEnd == 0) {
+ //no closing tag found
+ //move the pos up one so that the opening bracket is ignored next iteration
+ pos++;
+ }
+ else {
+ pos = tagEnd;
+ }
+ } else {
+ // convert the tag and content if necessary
+ int length = -1;
+ QString replacement = BBCodeMap::instance().convertTag(input.mid(pos), length);
+ if (length != 0) {
+ result.append(convertToHTML(replacement));
+ // length contains the number of characters in the original tag
+ pos += length;
+ } else {
+ // nothing replaced
+ result.append('[');
+ ++pos;
+ }
+ }
+ lastBlock = pos;
+ }
+
+ // append the remainder (everything after the last tag)
+ result.append(input.mid(lastBlock));
+ return result;
+}
+
+} // namespace BBCode
+
diff --git a/src/bbcode.h b/src/bbcode.h
index 708dfe71..9aea9d9c 100644
--- a/src/bbcode.h
+++ b/src/bbcode.h
@@ -1,40 +1,40 @@
-/*
-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 BBCODE_H
-#define BBCODE_H
-
-
-#include <QString>
-
-
-namespace BBCode {
-
-/**
- * @brief convert a string with BB Code-Tags to HTML
- * @param input the input string with BB tags
- * @param replaceOccured if not nullptr, this parameter will be set to true if any bb tags were replaced
- * @return the same string in html representation
- **/
-QString convertToHTML(const QString &input);
-
-}
-
-
-#endif // BBCODE_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 BBCODE_H
+#define BBCODE_H
+
+
+#include <QString>
+
+
+namespace BBCode {
+
+/**
+ * @brief convert a string with BB Code-Tags to HTML
+ * @param input the input string with BB tags
+ * @param replaceOccured if not nullptr, this parameter will be set to true if any bb tags were replaced
+ * @return the same string in html representation
+ **/
+QString convertToHTML(const QString &input);
+
+}
+
+
+#endif // BBCODE_H
diff --git a/src/browserdialog.cpp b/src/browserdialog.cpp
index 8e341363..4f16d352 100644
--- a/src/browserdialog.cpp
+++ b/src/browserdialog.cpp
@@ -1,297 +1,297 @@
-/*
-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 "browserdialog.h"
-
-#include "ui_browserdialog.h"
-#include "browserview.h"
-#include "messagedialog.h"
-#include "report.h"
-#include "persistentcookiejar.h"
-#include "settings.h"
-
-#include <utility.h>
-#include <log.h>
-
-#include <QWebEngineSettings>
-#include <QNetworkCookieJar>
-#include <QNetworkCookie>
-#include <QMenu>
-#include <QInputDialog>
-#include <QWebEngineHistory>
-#include <QDir>
-#include <QKeyEvent>
-
-using namespace MOBase;
-
-
-BrowserDialog::BrowserDialog(QWidget *parent)
- : QDialog(parent)
- , ui(new Ui::BrowserDialog)
- , m_AccessManager(new QNetworkAccessManager(this))
-{
- ui->setupUi(this);
-
- m_AccessManager->setCookieJar(new PersistentCookieJar(
- QDir::fromNativeSeparators(Settings::instance().paths().cache() + "/cookies.dat")));
-
- Qt::WindowFlags flags = windowFlags() | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint;
- Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint;
- flags = flags & (~helpFlag);
- setWindowFlags(flags);
-
- m_Tabs = this->findChild<QTabWidget*>("browserTabWidget");
-
- installEventFilter(this);
-
- connect(m_Tabs, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int)));
-
- ui->urlEdit->setVisible(false);
-}
-
-BrowserDialog::~BrowserDialog()
-{
- delete ui;
-}
-
-void BrowserDialog::closeEvent(QCloseEvent *event)
-{
- Settings::instance().geometry().saveGeometry(this);
- QDialog::closeEvent(event);
-}
-
-void BrowserDialog::initTab(BrowserView *newView)
-{
- //newView->page()->setNetworkAccessManager(m_AccessManager);
- //newView->page()->setForwardUnsupportedContent(true);
-
- connect(newView, SIGNAL(loadProgress(int)), this, SLOT(progress(int)));
- connect(newView, SIGNAL(titleChanged(QString)), this, SLOT(titleChanged(QString)));
- connect(newView, SIGNAL(initTab(BrowserView*)), this, SLOT(initTab(BrowserView*)));
- connect(newView, SIGNAL(startFind()), this, SLOT(startSearch()));
- connect(newView, SIGNAL(urlChanged(QUrl)), this, SLOT(urlChanged(QUrl)));
- connect(newView, SIGNAL(openUrlInNewTab(QUrl)), this, SLOT(openInNewTab(QUrl)));
- connect(newView, SIGNAL(downloadRequested(QNetworkRequest)), this, SLOT(downloadRequested(QNetworkRequest)));
- connect(newView, SIGNAL(unsupportedContent(QNetworkReply*)), this, SLOT(unsupportedContent(QNetworkReply*)));
-
- ui->backBtn->setEnabled(false);
- ui->fwdBtn->setEnabled(false);
- m_Tabs->addTab(newView, tr("new"));
- newView->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
- newView->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, true);
-}
-
-
-void BrowserDialog::openInNewTab(const QUrl &url)
-{
- BrowserView *newView = new BrowserView(this);
- initTab(newView);
- newView->setUrl(url);
-}
-
-
-BrowserView *BrowserDialog::getCurrentView()
-{
- return qobject_cast<BrowserView*>(m_Tabs->currentWidget());
-}
-
-
-void BrowserDialog::urlChanged(const QUrl &url)
-{
- BrowserView *currentView = getCurrentView();
- if (currentView != nullptr) {
- ui->backBtn->setEnabled(currentView->history()->canGoBack());
- ui->fwdBtn->setEnabled(currentView->history()->canGoForward());
- }
- ui->urlEdit->setText(url.toString());
-}
-
-
-void BrowserDialog::openUrl(const QUrl &url)
-{
- if (isHidden()) {
- Settings::instance().geometry().restoreGeometry(this);
- show();
- }
- openInNewTab(url);
-}
-
-
-void BrowserDialog::maximizeWidth()
-{
- int viewportWidth = getCurrentView()->page()->contentsSize ().width();
- int frameWidth = width() - viewportWidth;
-
- int contentWidth = getCurrentView()->page()->contentsSize().width();
-
- QScreen* screen = this->window()->windowHandle()->screen();
- int screenWidth = screen->geometry().size().width();
-
- int targetWidth = std::min<int>(std::max<int>(viewportWidth, contentWidth) + frameWidth, screenWidth);
- this->resize(targetWidth, height());
-}
-
-
-void BrowserDialog::progress(int value)
-{
- ui->loadProgress->setValue(value);
- if (value == 100) {
- maximizeWidth();
- ui->loadProgress->setVisible(false);
- } else {
- ui->loadProgress->setVisible(true);
- }
-}
-
-
-void BrowserDialog::titleChanged(const QString &title)
-{
- BrowserView *view = qobject_cast<BrowserView*>(sender());
- for (int i = 0; i < m_Tabs->count(); ++i) {
- if (m_Tabs->widget(i) == view) {
- m_Tabs->setTabText(i, title.mid(0, 15));
- m_Tabs->setTabToolTip(i, title);
- }
- }
-}
-
-
-QString BrowserDialog::guessFileName(const QString &url)
-{
- QRegularExpression uploadsExp(QString("https://.+/uploads/([^/]+)$"));
- auto match = uploadsExp.match(url);
- if (match.hasMatch()) {
- // these seem to be premium downloads
- return match.captured(1);
- }
-
- QRegularExpression filesExp(QString("https://.+\\?file=([^&]+)"));
- match = filesExp.match(url);
- if (match.hasMatch()) {
- // a regular manual download?
- return match.captured(1);
- }
- return "unknown";
-}
-
-void BrowserDialog::unsupportedContent(QNetworkReply *reply)
-{
- try {
- QWebEnginePage *page = qobject_cast<QWebEnginePage*>(sender());
- if (page == nullptr) {
- log::error("sender not a page");
- return;
- }
- /*browserview *view = qobject_cast<browserview*>(page->view());
- if (view == nullptr) {
- log::error("no view?");
- return;
- }*/
-
- emit requestDownload(page->url(), reply);
- } catch (const std::exception &e) {
- if (isVisible()) {
- MessageDialog::showMessage(tr("failed to start download"), this);
- }
- log::error("exception downloading unsupported content: {}", e.what());
- }
-}
-
-
-void BrowserDialog::downloadRequested(const QNetworkRequest &request)
-{
- log::error("download request {} ignored", request.url().toString());
-}
-
-
-void BrowserDialog::tabCloseRequested(int index)
-{
- if (m_Tabs->count() == 1) {
- this->close();
- } else {
- m_Tabs->widget(index)->deleteLater();
- m_Tabs->removeTab(index);
- }
-}
-
-void BrowserDialog::on_backBtn_clicked()
-{
- BrowserView *currentView = getCurrentView();
- if (currentView != nullptr) {
- currentView->back();
- }
-}
-
-void BrowserDialog::on_fwdBtn_clicked()
-{
- BrowserView *currentView = getCurrentView();
- if (currentView != nullptr) {
- currentView->forward();
- }
-}
-
-
-void BrowserDialog::startSearch()
-{
- ui->searchEdit->setFocus();
-}
-
-
-void BrowserDialog::on_searchEdit_returnPressed()
-{
-// BrowserView *currentView = getCurrentView();
-// if (currentView != nullptr) {
-// currentView->findText(ui->searchEdit->text(), QWebEnginePage::FindWrapsAroundDocument);
-// }
-}
-
-void BrowserDialog::on_refreshBtn_clicked()
-{
- getCurrentView()->reload();
-}
-
-void BrowserDialog::on_browserTabWidget_currentChanged(int index)
-{
- BrowserView *currentView = qobject_cast<BrowserView*>(ui->browserTabWidget->widget(index));
- if (currentView != nullptr) {
- ui->backBtn->setEnabled(currentView->history()->canGoBack());
- ui->fwdBtn->setEnabled(currentView->history()->canGoForward());
- }
-}
-
-void BrowserDialog::on_urlEdit_returnPressed()
-{
- QWebEngineView *currentView = getCurrentView();
- if (currentView != nullptr) {
- currentView->setUrl(QUrl(ui->urlEdit->text()));
- }
-}
-
-bool BrowserDialog::eventFilter(QObject *object, QEvent *event)
-{
- if (event->type() == QEvent::KeyPress) {
- QKeyEvent *keyEvent = reinterpret_cast<QKeyEvent*>(event);
- if ((keyEvent->modifiers() & Qt::ControlModifier)
- && (keyEvent->key() == Qt::Key_U)) {
- ui->urlEdit->setVisible(!ui->urlEdit->isVisible());
- return true;
- }
- }
- return QDialog::eventFilter(object, event);
-}
+/*
+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 "browserdialog.h"
+
+#include "ui_browserdialog.h"
+#include "browserview.h"
+#include "messagedialog.h"
+#include "report.h"
+#include "persistentcookiejar.h"
+#include "settings.h"
+
+#include <utility.h>
+#include <log.h>
+
+#include <QWebEngineSettings>
+#include <QNetworkCookieJar>
+#include <QNetworkCookie>
+#include <QMenu>
+#include <QInputDialog>
+#include <QWebEngineHistory>
+#include <QDir>
+#include <QKeyEvent>
+
+using namespace MOBase;
+
+
+BrowserDialog::BrowserDialog(QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::BrowserDialog)
+ , m_AccessManager(new QNetworkAccessManager(this))
+{
+ ui->setupUi(this);
+
+ m_AccessManager->setCookieJar(new PersistentCookieJar(
+ QDir::fromNativeSeparators(Settings::instance().paths().cache() + "/cookies.dat")));
+
+ Qt::WindowFlags flags = windowFlags() | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint;
+ Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint;
+ flags = flags & (~helpFlag);
+ setWindowFlags(flags);
+
+ m_Tabs = this->findChild<QTabWidget*>("browserTabWidget");
+
+ installEventFilter(this);
+
+ connect(m_Tabs, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int)));
+
+ ui->urlEdit->setVisible(false);
+}
+
+BrowserDialog::~BrowserDialog()
+{
+ delete ui;
+}
+
+void BrowserDialog::closeEvent(QCloseEvent *event)
+{
+ Settings::instance().geometry().saveGeometry(this);
+ QDialog::closeEvent(event);
+}
+
+void BrowserDialog::initTab(BrowserView *newView)
+{
+ //newView->page()->setNetworkAccessManager(m_AccessManager);
+ //newView->page()->setForwardUnsupportedContent(true);
+
+ connect(newView, SIGNAL(loadProgress(int)), this, SLOT(progress(int)));
+ connect(newView, SIGNAL(titleChanged(QString)), this, SLOT(titleChanged(QString)));
+ connect(newView, SIGNAL(initTab(BrowserView*)), this, SLOT(initTab(BrowserView*)));
+ connect(newView, SIGNAL(startFind()), this, SLOT(startSearch()));
+ connect(newView, SIGNAL(urlChanged(QUrl)), this, SLOT(urlChanged(QUrl)));
+ connect(newView, SIGNAL(openUrlInNewTab(QUrl)), this, SLOT(openInNewTab(QUrl)));
+ connect(newView, SIGNAL(downloadRequested(QNetworkRequest)), this, SLOT(downloadRequested(QNetworkRequest)));
+ connect(newView, SIGNAL(unsupportedContent(QNetworkReply*)), this, SLOT(unsupportedContent(QNetworkReply*)));
+
+ ui->backBtn->setEnabled(false);
+ ui->fwdBtn->setEnabled(false);
+ m_Tabs->addTab(newView, tr("new"));
+ newView->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
+ newView->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, true);
+}
+
+
+void BrowserDialog::openInNewTab(const QUrl &url)
+{
+ BrowserView *newView = new BrowserView(this);
+ initTab(newView);
+ newView->setUrl(url);
+}
+
+
+BrowserView *BrowserDialog::getCurrentView()
+{
+ return qobject_cast<BrowserView*>(m_Tabs->currentWidget());
+}
+
+
+void BrowserDialog::urlChanged(const QUrl &url)
+{
+ BrowserView *currentView = getCurrentView();
+ if (currentView != nullptr) {
+ ui->backBtn->setEnabled(currentView->history()->canGoBack());
+ ui->fwdBtn->setEnabled(currentView->history()->canGoForward());
+ }
+ ui->urlEdit->setText(url.toString());
+}
+
+
+void BrowserDialog::openUrl(const QUrl &url)
+{
+ if (isHidden()) {
+ Settings::instance().geometry().restoreGeometry(this);
+ show();
+ }
+ openInNewTab(url);
+}
+
+
+void BrowserDialog::maximizeWidth()
+{
+ int viewportWidth = getCurrentView()->page()->contentsSize ().width();
+ int frameWidth = width() - viewportWidth;
+
+ int contentWidth = getCurrentView()->page()->contentsSize().width();
+
+ QScreen* screen = this->window()->windowHandle()->screen();
+ int screenWidth = screen->geometry().size().width();
+
+ int targetWidth = std::min<int>(std::max<int>(viewportWidth, contentWidth) + frameWidth, screenWidth);
+ this->resize(targetWidth, height());
+}
+
+
+void BrowserDialog::progress(int value)
+{
+ ui->loadProgress->setValue(value);
+ if (value == 100) {
+ maximizeWidth();
+ ui->loadProgress->setVisible(false);
+ } else {
+ ui->loadProgress->setVisible(true);
+ }
+}
+
+
+void BrowserDialog::titleChanged(const QString &title)
+{
+ BrowserView *view = qobject_cast<BrowserView*>(sender());
+ for (int i = 0; i < m_Tabs->count(); ++i) {
+ if (m_Tabs->widget(i) == view) {
+ m_Tabs->setTabText(i, title.mid(0, 15));
+ m_Tabs->setTabToolTip(i, title);
+ }
+ }
+}
+
+
+QString BrowserDialog::guessFileName(const QString &url)
+{
+ QRegularExpression uploadsExp(QString("https://.+/uploads/([^/]+)$"));
+ auto match = uploadsExp.match(url);
+ if (match.hasMatch()) {
+ // these seem to be premium downloads
+ return match.captured(1);
+ }
+
+ QRegularExpression filesExp(QString("https://.+\\?file=([^&]+)"));
+ match = filesExp.match(url);
+ if (match.hasMatch()) {
+ // a regular manual download?
+ return match.captured(1);
+ }
+ return "unknown";
+}
+
+void BrowserDialog::unsupportedContent(QNetworkReply *reply)
+{
+ try {
+ QWebEnginePage *page = qobject_cast<QWebEnginePage*>(sender());
+ if (page == nullptr) {
+ log::error("sender not a page");
+ return;
+ }
+ /*browserview *view = qobject_cast<browserview*>(page->view());
+ if (view == nullptr) {
+ log::error("no view?");
+ return;
+ }*/
+
+ emit requestDownload(page->url(), reply);
+ } catch (const std::exception &e) {
+ if (isVisible()) {
+ MessageDialog::showMessage(tr("failed to start download"), this);
+ }
+ log::error("exception downloading unsupported content: {}", e.what());
+ }
+}
+
+
+void BrowserDialog::downloadRequested(const QNetworkRequest &request)
+{
+ log::error("download request {} ignored", request.url().toString());
+}
+
+
+void BrowserDialog::tabCloseRequested(int index)
+{
+ if (m_Tabs->count() == 1) {
+ this->close();
+ } else {
+ m_Tabs->widget(index)->deleteLater();
+ m_Tabs->removeTab(index);
+ }
+}
+
+void BrowserDialog::on_backBtn_clicked()
+{
+ BrowserView *currentView = getCurrentView();
+ if (currentView != nullptr) {
+ currentView->back();
+ }
+}
+
+void BrowserDialog::on_fwdBtn_clicked()
+{
+ BrowserView *currentView = getCurrentView();
+ if (currentView != nullptr) {
+ currentView->forward();
+ }
+}
+
+
+void BrowserDialog::startSearch()
+{
+ ui->searchEdit->setFocus();
+}
+
+
+void BrowserDialog::on_searchEdit_returnPressed()
+{
+// BrowserView *currentView = getCurrentView();
+// if (currentView != nullptr) {
+// currentView->findText(ui->searchEdit->text(), QWebEnginePage::FindWrapsAroundDocument);
+// }
+}
+
+void BrowserDialog::on_refreshBtn_clicked()
+{
+ getCurrentView()->reload();
+}
+
+void BrowserDialog::on_browserTabWidget_currentChanged(int index)
+{
+ BrowserView *currentView = qobject_cast<BrowserView*>(ui->browserTabWidget->widget(index));
+ if (currentView != nullptr) {
+ ui->backBtn->setEnabled(currentView->history()->canGoBack());
+ ui->fwdBtn->setEnabled(currentView->history()->canGoForward());
+ }
+}
+
+void BrowserDialog::on_urlEdit_returnPressed()
+{
+ QWebEngineView *currentView = getCurrentView();
+ if (currentView != nullptr) {
+ currentView->setUrl(QUrl(ui->urlEdit->text()));
+ }
+}
+
+bool BrowserDialog::eventFilter(QObject *object, QEvent *event)
+{
+ if (event->type() == QEvent::KeyPress) {
+ QKeyEvent *keyEvent = reinterpret_cast<QKeyEvent*>(event);
+ if ((keyEvent->modifiers() & Qt::ControlModifier)
+ && (keyEvent->key() == Qt::Key_U)) {
+ ui->urlEdit->setVisible(!ui->urlEdit->isVisible());
+ return true;
+ }
+ }
+ return QDialog::eventFilter(object, event);
+}
diff --git a/src/browserdialog.h b/src/browserdialog.h
index 354a377b..31b86fff 100644
--- a/src/browserdialog.h
+++ b/src/browserdialog.h
@@ -1,126 +1,126 @@
-/*
-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 BROWSERDIALOG_H
-#define BROWSERDIALOG_H
-
-#include <QDialog>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QTimer>
-#include <QWebEngineView>
-#include <QQueue>
-#include <QTabWidget>
-#include <QAtomicInt>
-
-
-namespace Ui {
- class BrowserDialog;
-}
-
-class BrowserView;
-
-/**
- * @brief a dialog containing a webbrowser that is intended to browse the nexus network
- **/
-class BrowserDialog : public QDialog
-{
- Q_OBJECT
-
-public:
-
- /**
- * @brief constructor
- *
- * @param accessManager the access manager to use for network requests
- * @param parent parent widget
- **/
- explicit BrowserDialog(QWidget *parent = 0);
- ~BrowserDialog();
-
- /**
- * @brief set the url to open. If automatic login is enabled, the url is opened after login
- *
- * @param url the url to open
- **/
- void openUrl(const QUrl &url);
-
- virtual bool eventFilter(QObject *object, QEvent *event);
-signals:
-
- /**
- * @brief emitted when the user starts a download
- * @param pageUrl url of the current web site from which the download was started
- * @param reply network reply of the started download
- */
- void requestDownload(const QUrl &pageUrl, QNetworkReply *reply);
-
-protected:
-
- virtual void closeEvent(QCloseEvent *);
-
-private slots:
-
- void initTab(BrowserView *newView);
- void openInNewTab(const QUrl &url);
-
- void progress(int value);
-
- void titleChanged(const QString &title);
- void unsupportedContent(QNetworkReply *reply);
- void downloadRequested(const QNetworkRequest &request);
-
- void tabCloseRequested(int index);
-
- void urlChanged(const QUrl &url);
-
- void on_backBtn_clicked();
-
- void on_fwdBtn_clicked();
-
- void on_searchEdit_returnPressed();
-
- void startSearch();
-
- void on_refreshBtn_clicked();
-
- void on_browserTabWidget_currentChanged(int index);
-
- void on_urlEdit_returnPressed();
-
-private:
-
- QString guessFileName(const QString &url);
-
- BrowserView *getCurrentView();
-
- void maximizeWidth();
-
-private:
-
- Ui::BrowserDialog *ui;
-
- QNetworkAccessManager *m_AccessManager;
-
- QTabWidget *m_Tabs;
-
-
-};
-
-#endif // BROWSERDIALOG_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 BROWSERDIALOG_H
+#define BROWSERDIALOG_H
+
+#include <QDialog>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QTimer>
+#include <QWebEngineView>
+#include <QQueue>
+#include <QTabWidget>
+#include <QAtomicInt>
+
+
+namespace Ui {
+ class BrowserDialog;
+}
+
+class BrowserView;
+
+/**
+ * @brief a dialog containing a webbrowser that is intended to browse the nexus network
+ **/
+class BrowserDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+
+ /**
+ * @brief constructor
+ *
+ * @param accessManager the access manager to use for network requests
+ * @param parent parent widget
+ **/
+ explicit BrowserDialog(QWidget *parent = 0);
+ ~BrowserDialog();
+
+ /**
+ * @brief set the url to open. If automatic login is enabled, the url is opened after login
+ *
+ * @param url the url to open
+ **/
+ void openUrl(const QUrl &url);
+
+ virtual bool eventFilter(QObject *object, QEvent *event);
+signals:
+
+ /**
+ * @brief emitted when the user starts a download
+ * @param pageUrl url of the current web site from which the download was started
+ * @param reply network reply of the started download
+ */
+ void requestDownload(const QUrl &pageUrl, QNetworkReply *reply);
+
+protected:
+
+ virtual void closeEvent(QCloseEvent *);
+
+private slots:
+
+ void initTab(BrowserView *newView);
+ void openInNewTab(const QUrl &url);
+
+ void progress(int value);
+
+ void titleChanged(const QString &title);
+ void unsupportedContent(QNetworkReply *reply);
+ void downloadRequested(const QNetworkRequest &request);
+
+ void tabCloseRequested(int index);
+
+ void urlChanged(const QUrl &url);
+
+ void on_backBtn_clicked();
+
+ void on_fwdBtn_clicked();
+
+ void on_searchEdit_returnPressed();
+
+ void startSearch();
+
+ void on_refreshBtn_clicked();
+
+ void on_browserTabWidget_currentChanged(int index);
+
+ void on_urlEdit_returnPressed();
+
+private:
+
+ QString guessFileName(const QString &url);
+
+ BrowserView *getCurrentView();
+
+ void maximizeWidth();
+
+private:
+
+ Ui::BrowserDialog *ui;
+
+ QNetworkAccessManager *m_AccessManager;
+
+ QTabWidget *m_Tabs;
+
+
+};
+
+#endif // BROWSERDIALOG_H
diff --git a/src/browserview.cpp b/src/browserview.cpp
index 81fd8a74..c45bf928 100644
--- a/src/browserview.cpp
+++ b/src/browserview.cpp
@@ -1,76 +1,76 @@
-/*
-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 "browserview.h"
-
-#include <QEvent>
-#include <QKeyEvent>
-#include <QNetworkDiskCache>
-#include <QWebEngineContextMenuRequest>
-#include <QWebEngineSettings>
-#include <QMenu>
-#include <Shlwapi.h>
-#include "utility.h"
-
-
-BrowserView::BrowserView(QWidget *parent)
- : QWebEngineView(parent)
-{
- installEventFilter(this);
-
- //page()->settings()->setMaximumPagesInCache(10);
-}
-
-QWebEngineView *BrowserView::createWindow(QWebEnginePage::WebWindowType)
-{
- BrowserView *newView = new BrowserView(parentWidget());
- emit initTab(newView);
- return newView;
-}
-
-bool BrowserView::eventFilter(QObject *obj, QEvent *event)
-{
- if (event->type() == QEvent::ShortcutOverride) {
- QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
- if (keyEvent->matches(QKeySequence::Find)) {
- emit startFind();
- } else if (keyEvent->matches(QKeySequence::FindNext)) {
- emit findAgain();
- }
- } else if (event->type() == QEvent::MouseButtonPress) {
- QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
- if (mouseEvent->button() == Qt::MouseButton::MiddleButton) {
- mouseEvent->ignore();
- return true;
- }
-// TODO This is due to that QTWebEnginePage doesn't support QWebFrame anymore
-// } else if (event->type() == QEvent::MouseButtonRelease) {
-// QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
-// if (mouseEvent->button() == Qt::MidButton) {
-// QWebEngineContextMenuData hitTest = page()->hitTestContent(mouseEvent->pos());
-// if (hitTest.linkUrl().isValid()) {
-// emit openUrlInNewTab(hitTest.linkUrl());
-// }
-// mouseEvent->ignore();
-//
-// return true;
-// }
- }
- return QWebEngineView::eventFilter(obj, event);
-}
+/*
+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 "browserview.h"
+
+#include <QEvent>
+#include <QKeyEvent>
+#include <QNetworkDiskCache>
+#include <QWebEngineContextMenuRequest>
+#include <QWebEngineSettings>
+#include <QMenu>
+#include <Shlwapi.h>
+#include "utility.h"
+
+
+BrowserView::BrowserView(QWidget *parent)
+ : QWebEngineView(parent)
+{
+ installEventFilter(this);
+
+ //page()->settings()->setMaximumPagesInCache(10);
+}
+
+QWebEngineView *BrowserView::createWindow(QWebEnginePage::WebWindowType)
+{
+ BrowserView *newView = new BrowserView(parentWidget());
+ emit initTab(newView);
+ return newView;
+}
+
+bool BrowserView::eventFilter(QObject *obj, QEvent *event)
+{
+ if (event->type() == QEvent::ShortcutOverride) {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->matches(QKeySequence::Find)) {
+ emit startFind();
+ } else if (keyEvent->matches(QKeySequence::FindNext)) {
+ emit findAgain();
+ }
+ } else if (event->type() == QEvent::MouseButtonPress) {
+ QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
+ if (mouseEvent->button() == Qt::MouseButton::MiddleButton) {
+ mouseEvent->ignore();
+ return true;
+ }
+// TODO This is due to that QTWebEnginePage doesn't support QWebFrame anymore
+// } else if (event->type() == QEvent::MouseButtonRelease) {
+// QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
+// if (mouseEvent->button() == Qt::MidButton) {
+// QWebEngineContextMenuData hitTest = page()->hitTestContent(mouseEvent->pos());
+// if (hitTest.linkUrl().isValid()) {
+// emit openUrlInNewTab(hitTest.linkUrl());
+// }
+// mouseEvent->ignore();
+//
+// return true;
+// }
+ }
+ return QWebEngineView::eventFilter(obj, event);
+}
diff --git a/src/browserview.h b/src/browserview.h
index 24be21c1..c0248263 100644
--- a/src/browserview.h
+++ b/src/browserview.h
@@ -1,81 +1,81 @@
-/*
-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 NEXUSVIEW_H
-#define NEXUSVIEW_H
-
-
-class QEvent;
-class QUrl;
-class QWidget;
-#include <QWebEngineView>
-#include <QWebEnginePage>
-
-/**
- * @brief web view used to display a nexus page
- **/
-class BrowserView : public QWebEngineView
-{
- Q_OBJECT
-
-public:
-
- explicit BrowserView(QWidget *parent = 0);
-
-signals:
-
- /**
- * @brief emitted when the user opens a new window to be displayed in another tab
- *
- * @param newView the view for the newly opened window
- **/
- void initTab(BrowserView *newView);
-
- /**
- * @brief emitted when the user requests a link to be opened in a new tab by middle-clicking
- *
- * @param url the url to open
- */
- void openUrlInNewTab(const QUrl &url);
-
- /**
- * @brief Ctrl-f was clicked. The containing dialog should activate its find-facility
- */
- void startFind();
-
- /**
- * @brief F3 was pressed. The containing dialog should search again
- */
- void findAgain();
-
-protected:
-
- virtual QWebEngineView *createWindow(QWebEnginePage::WebWindowType type);
-
- virtual bool eventFilter(QObject *obj, QEvent *event);
-
-
-private:
-
- QString m_FindPattern;
- bool m_MiddleClick;
-
-};
-
-#endif // NEXUSVIEW_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 NEXUSVIEW_H
+#define NEXUSVIEW_H
+
+
+class QEvent;
+class QUrl;
+class QWidget;
+#include <QWebEngineView>
+#include <QWebEnginePage>
+
+/**
+ * @brief web view used to display a nexus page
+ **/
+class BrowserView : public QWebEngineView
+{
+ Q_OBJECT
+
+public:
+
+ explicit BrowserView(QWidget *parent = 0);
+
+signals:
+
+ /**
+ * @brief emitted when the user opens a new window to be displayed in another tab
+ *
+ * @param newView the view for the newly opened window
+ **/
+ void initTab(BrowserView *newView);
+
+ /**
+ * @brief emitted when the user requests a link to be opened in a new tab by middle-clicking
+ *
+ * @param url the url to open
+ */
+ void openUrlInNewTab(const QUrl &url);
+
+ /**
+ * @brief Ctrl-f was clicked. The containing dialog should activate its find-facility
+ */
+ void startFind();
+
+ /**
+ * @brief F3 was pressed. The containing dialog should search again
+ */
+ void findAgain();
+
+protected:
+
+ virtual QWebEngineView *createWindow(QWebEnginePage::WebWindowType type);
+
+ virtual bool eventFilter(QObject *obj, QEvent *event);
+
+
+private:
+
+ QString m_FindPattern;
+ bool m_MiddleClick;
+
+};
+
+#endif // NEXUSVIEW_H
diff --git a/src/categories.cpp b/src/categories.cpp
index 01383031..6071cc43 100644
--- a/src/categories.cpp
+++ b/src/categories.cpp
@@ -1,419 +1,419 @@
-/*
-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 "categories.h"
-
-#include <utility.h>
-#include <report.h>
-#include <log.h>
-
-#include <QObject>
-#include <QFile>
-#include <QDir>
-#include <QList>
-#include <QCoreApplication>
-
-
-using namespace MOBase;
-
-
-CategoryFactory* CategoryFactory::s_Instance = nullptr;
-
-
-QString CategoryFactory::categoriesFilePath()
-{
- return qApp->property("dataPath").toString() + "/categories.dat";
-}
-
-
-CategoryFactory::CategoryFactory()
-{
- atexit(&cleanup);
-}
-
-void CategoryFactory::loadCategories()
-{
- reset();
-
- QFile categoryFile(categoriesFilePath());
-
- if (!categoryFile.open(QIODevice::ReadOnly)) {
- loadDefaultCategories();
- } else {
- int lineNum = 0;
- while (!categoryFile.atEnd()) {
- QByteArray line = categoryFile.readLine();
- ++lineNum;
- QList<QByteArray> cells = line.split('|');
- if (cells.count() != 4) {
- log::error(
- "invalid category line {}: {} ({} cells)",
- lineNum, line.constData(), cells.count());
- } else {
- std::vector<int> nexusIDs;
- if (cells[2].length() > 0) {
- QList<QByteArray> nexusIDStrings = cells[2].split(',');
- for (QList<QByteArray>::iterator iter = nexusIDStrings.begin();
- iter != nexusIDStrings.end(); ++iter) {
- bool ok = false;
- int temp = iter->toInt(&ok);
- if (!ok) {
- log::error("invalid category id {}", iter->constData());
- }
- nexusIDs.push_back(temp);
- }
- }
- bool cell0Ok = true;
- bool cell3Ok = true;
- int id = cells[0].toInt(&cell0Ok);
- int parentID = cells[3].trimmed().toInt(&cell3Ok);
- if (!cell0Ok || !cell3Ok) {
- log::error("invalid category line {}: {}", lineNum, line.constData());
- }
- addCategory(id, QString::fromUtf8(cells[1].constData()), nexusIDs, parentID);
- }
- }
- categoryFile.close();
- }
- std::sort(m_Categories.begin(), m_Categories.end());
- setParents();
-}
-
-
-CategoryFactory &CategoryFactory::instance()
-{
- if (s_Instance == nullptr) {
- s_Instance = new CategoryFactory;
- }
- return *s_Instance;
-}
-
-
-void CategoryFactory::reset()
-{
- m_Categories.clear();
- m_IDMap.clear();
- // 28 =
- // 43 = Savegames (makes no sense to install them through MO)
- // 45 = Videos and trailers
- // 87 = Miscelanous
- addCategory(0, "None", { 4, 28, 43, 45, 87 }, 0);
-}
-
-
-void CategoryFactory::setParents()
-{
- for (std::vector<Category>::iterator iter = m_Categories.begin();
- iter != m_Categories.end(); ++iter) {
- iter->m_HasChildren = false;
- }
-
- for (std::vector<Category>::const_iterator categoryIter = m_Categories.begin();
- categoryIter != m_Categories.end(); ++categoryIter) {
- if (categoryIter->m_ParentID != 0) {
- std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(categoryIter->m_ParentID);
- if (iter != m_IDMap.end()) {
- m_Categories[iter->second].m_HasChildren = true;
- }
- }
- }
-}
-
-void CategoryFactory::cleanup()
-{
- delete s_Instance;
- s_Instance = nullptr;
-}
-
-
-void CategoryFactory::saveCategories()
-{
- QFile categoryFile(categoriesFilePath());
-
- if (!categoryFile.open(QIODevice::WriteOnly)) {
- reportError(QObject::tr("Failed to save custom categories"));
- return;
- }
-
- categoryFile.resize(0);
- for (std::vector<Category>::const_iterator iter = m_Categories.begin();
- iter != m_Categories.end(); ++iter) {
- if (iter->m_ID == 0) {
- continue;
- }
- QByteArray line;
- line.append(QByteArray::number(iter->m_ID)).append("|")
- .append(iter->m_Name.toUtf8()).append("|")
- .append(VectorJoin(iter->m_NexusIDs, ",").toUtf8()).append("|")
- .append(QByteArray::number(iter->m_ParentID)).append("\n");
- categoryFile.write(line);
- }
- categoryFile.close();
-}
-
-
-unsigned int CategoryFactory::countCategories(std::function<bool (const Category &category)> filter)
-{
- unsigned int result = 0;
- for (const Category &cat : m_Categories) {
- if (filter(cat)) {
- ++result;
- }
- }
- return result;
-}
-
-int CategoryFactory::addCategory(const QString &name, const std::vector<int> &nexusIDs, int parentID)
-{
- int id = 1;
- while (m_IDMap.find(id) != m_IDMap.end()) {
- ++id;
- }
- addCategory(id, name, nexusIDs, parentID);
-
- saveCategories();
- return id;
-}
-
-void CategoryFactory::addCategory(int id, const QString &name, const std::vector<int> &nexusIDs, int parentID)
-{
- int index = static_cast<int>(m_Categories.size());
- m_Categories.push_back(Category(index, id, name, nexusIDs, parentID));
- for (int nexusID : nexusIDs) {
- m_NexusMap[nexusID] = index;
- }
- m_IDMap[id] = index;
-}
-
-
-void CategoryFactory::loadDefaultCategories()
-{
- // the order here is relevant as it defines the order in which the
- // mods appear in the combo box
- addCategory(1, "Animations", { 2, 4, 51 }, 0);
- addCategory(52, "Poses", { 1, 29 }, 1);
- addCategory(2, "Armour", { 2, 5, 54 }, 0);
- addCategory(53, "Power Armor", { 1, 53 }, 2);
- addCategory(3, "Audio", { 3, 33, 35, 106 }, 0);
- addCategory(38, "Music", { 2, 34, 61 }, 0);
- addCategory(39, "Voice", { 2, 36, 107 }, 0);
- addCategory(5, "Clothing", { 2, 9, 60 }, 0);
- addCategory(41, "Jewelry", { 1, 102 }, 5);
- addCategory(42, "Backpacks", { 1, 49 }, 5);
- addCategory(6, "Collectables", { 2, 10, 92 }, 0);
- addCategory(28, "Companions", { 3, 11, 66, 96 }, 0);
- addCategory(7, "Creatures, Mounts, & Vehicles", { 4, 12, 65, 83, 101 }, 0);
- addCategory(8, "Factions", { 2, 16, 25 }, 0);
- addCategory(9, "Gameplay", { 2, 15, 24 }, 0);
- addCategory(27, "Combat", { 1, 77 }, 9);
- addCategory(43, "Crafting", { 2, 50, 100 }, 9);
- addCategory(48, "Overhauls", { 2, 24, 79 }, 9);
- addCategory(49, "Perks", { 1, 27 }, 9);
- addCategory(54, "Radio", { 1, 31 }, 9);
- addCategory(55, "Shouts", { 1, 104 }, 9);
- addCategory(22, "Skills & Levelling", { 2, 46, 73 }, 9);
- addCategory(58, "Weather & Lighting", { 1, 56 }, 9);
- addCategory(44, "Equipment", { 1, 44 }, 43);
- addCategory(45, "Home/Settlement", { 1, 45 }, 43);
- addCategory(10, "Body, Face, & Hair", { 2, 17, 26 }, 0);
- addCategory(56, "Tattoos", { 1, 57 }, 10);
- addCategory(40, "Character Presets", { 1, 58 }, 0);
- addCategory(11, "Items", { 2, 27, 85 }, 0);
- addCategory(32, "Mercantile", { 2, 23, 69 }, 0);
- addCategory(37, "Ammo", { 1, 3 }, 11);
- addCategory(19, "Weapons", { 2, 41, 55 }, 11);
- addCategory(36, "Weapon & Armour Sets", { 1, 42 }, 11);
- addCategory(23, "Player Homes", { 2, 28, 67 }, 0);
- addCategory(25, "Castles & Mansions", { 1, 68 }, 23);
- addCategory(51, "Settlements", { 1, 48 }, 23);
- addCategory(12, "Locations", { 10, 20, 21, 22, 30, 47, 70, 88, 89, 90, 91 }, 0);
- addCategory(4, "Cities", { 1, 53 }, 12);
- addCategory(31, "Landscape Changes", { 1, 58 }, 0);
- addCategory(29, "Environment", { 2, 14, 74 }, 0);
- addCategory(30, "Immersion", { 2, 51, 78 }, 0);
- addCategory(20, "Magic", { 3, 75, 93, 94 }, 0);
- addCategory(21, "Models & Textures", { 2, 19, 29 }, 0);
- addCategory(33, "Modders resources", { 2, 18, 82 }, 0);
- addCategory(13, "NPCs", { 3, 22, 33, 99 }, 0);
- addCategory(24, "Bugfixes", { 2, 6, 95 }, 0);
- addCategory(14, "Patches", { 2, 25, 84 }, 24);
- addCategory(35, "Utilities", { 2, 38, 39 }, 0);
- addCategory(26, "Cheats", { 1, 8 }, 0);
- addCategory(15, "Quests", { 2, 30, 35 }, 0);
- addCategory(16, "Races & Classes", { 1, 34 }, 0);
- addCategory(34, "Stealth", { 1, 76 }, 0);
- addCategory(17, "UI", { 2, 37, 42 }, 0);
- addCategory(18, "Visuals", { 2, 40, 62 }, 0);
- addCategory(50, "Pip-Boy", { 1, 52 }, 18);
- addCategory(46, "Shader Presets", { 3, 13, 97, 105 }, 0);
- addCategory(47, "Miscellaneous", { 2, 2, 28 }, 0);
-}
-
-
-int CategoryFactory::getParentID(unsigned int index) const
-{
- if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid category index: %1").arg(index));
- }
-
- return m_Categories[index].m_ParentID;
-}
-
-
-bool CategoryFactory::categoryExists(int id) const
-{
- return m_IDMap.find(id) != m_IDMap.end();
-}
-
-
-bool CategoryFactory::isDescendantOf(int id, int parentID) const
-{
- // handles cycles
- std::set<int> seen;
- return isDescendantOfImpl(id, parentID, seen);
-}
-
-bool CategoryFactory::isDescendantOfImpl(
- int id, int parentID, std::set<int>& seen) const
-{
- if (!seen.insert(id).second) {
- log::error("cycle in category: {}", id);
- return false;
- }
-
- std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(id);
-
- if (iter != m_IDMap.end()) {
- unsigned int index = iter->second;
- if (m_Categories[index].m_ParentID == 0) {
- return false;
- } else if (m_Categories[index].m_ParentID == parentID) {
- return true;
- } else {
- return isDescendantOfImpl(m_Categories[index].m_ParentID, parentID, seen);
- }
- } else {
- log::warn("{} is no valid category id", id);
- return false;
- }
-}
-
-
-bool CategoryFactory::hasChildren(unsigned int index) const
-{
- if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid category index: %1").arg(index));
- }
-
- return m_Categories[index].m_HasChildren;
-}
-
-
-QString CategoryFactory::getCategoryName(unsigned int index) const
-{
- if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid category index: %1").arg(index));
- }
-
- return m_Categories[index].m_Name;
-}
-
-QString CategoryFactory::getSpecialCategoryName(SpecialCategories type) const
-{
- QString label;
- switch (type)
- {
- case Checked: label = QObject::tr("Active"); break;
- case UpdateAvailable: label = QObject::tr("Update available"); break;
- case HasCategory: label = QObject::tr("Has category"); break;
- case Conflict: label = QObject::tr("Conflicted"); break;
- case HasHiddenFiles: label = QObject::tr("Has hidden files"); break;
- case Endorsed: label = QObject::tr("Endorsed"); break;
- case Backup: label = QObject::tr("Has backup"); break;
- case Managed: label = QObject::tr("Managed"); break;
- case HasGameData: label = QObject::tr("Has valid game data"); break;
- case HasNexusID: label = QObject::tr("Has Nexus ID"); break;
- case Tracked: label = QObject::tr("Tracked on Nexus"); break;
- default: return {};
- }
- return QString("<%1>").arg(label);
-}
-
-QString CategoryFactory::getCategoryNameByID(int id) const
-{
- auto itor = m_IDMap.find(id);
-
- if (itor == m_IDMap.end()) {
- return getSpecialCategoryName(static_cast<SpecialCategories>(id));
- } else {
- const auto index = itor->second;
- if (index >= m_Categories.size()) {
- return {};
- }
-
- return m_Categories[index].m_Name;
- }
-}
-
-int CategoryFactory::getCategoryID(unsigned int index) const
-{
- if (index >= m_Categories.size()) {
- throw MyException(QObject::tr("invalid category index: %1").arg(index));
- }
-
- return m_Categories[index].m_ID;
-}
-
-
-int CategoryFactory::getCategoryIndex(int ID) const
-{
- std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(ID);
- if (iter == m_IDMap.end()) {
- throw MyException(QObject::tr("invalid category id: %1").arg(ID));
- }
- return iter->second;
-}
-
-
-int CategoryFactory::getCategoryID(const QString &name) const
-{
- auto iter = std::find_if(m_Categories.begin(), m_Categories.end(),
- [name] (const Category &cat) -> bool {
- return cat.m_Name == name;
- });
-
- if (iter != m_Categories.end()) {
- return iter->m_ID;
- } else {
- return -1;
- }
-}
-
-
-unsigned int CategoryFactory::resolveNexusID(int nexusID) const
-{
- std::map<int, unsigned int>::const_iterator iter = m_NexusMap.find(nexusID);
- if (iter != m_NexusMap.end()) {
- log::debug("nexus category id {} maps to internal {}", nexusID, iter->second);
- return iter->second;
- } else {
- log::debug("nexus category id {} not mapped", nexusID);
- return 0U;
- }
-}
+/*
+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 "categories.h"
+
+#include <utility.h>
+#include <report.h>
+#include <log.h>
+
+#include <QObject>
+#include <QFile>
+#include <QDir>
+#include <QList>
+#include <QCoreApplication>
+
+
+using namespace MOBase;
+
+
+CategoryFactory* CategoryFactory::s_Instance = nullptr;
+
+
+QString CategoryFactory::categoriesFilePath()
+{
+ return qApp->property("dataPath").toString() + "/categories.dat";
+}
+
+
+CategoryFactory::CategoryFactory()
+{
+ atexit(&cleanup);
+}
+
+void CategoryFactory::loadCategories()
+{
+ reset();
+
+ QFile categoryFile(categoriesFilePath());
+
+ if (!categoryFile.open(QIODevice::ReadOnly)) {
+ loadDefaultCategories();
+ } else {
+ int lineNum = 0;
+ while (!categoryFile.atEnd()) {
+ QByteArray line = categoryFile.readLine();
+ ++lineNum;
+ QList<QByteArray> cells = line.split('|');
+ if (cells.count() != 4) {
+ log::error(
+ "invalid category line {}: {} ({} cells)",
+ lineNum, line.constData(), cells.count());
+ } else {
+ std::vector<int> nexusIDs;
+ if (cells[2].length() > 0) {
+ QList<QByteArray> nexusIDStrings = cells[2].split(',');
+ for (QList<QByteArray>::iterator iter = nexusIDStrings.begin();
+ iter != nexusIDStrings.end(); ++iter) {
+ bool ok = false;
+ int temp = iter->toInt(&ok);
+ if (!ok) {
+ log::error("invalid category id {}", iter->constData());
+ }
+ nexusIDs.push_back(temp);
+ }
+ }
+ bool cell0Ok = true;
+ bool cell3Ok = true;
+ int id = cells[0].toInt(&cell0Ok);
+ int parentID = cells[3].trimmed().toInt(&cell3Ok);
+ if (!cell0Ok || !cell3Ok) {
+ log::error("invalid category line {}: {}", lineNum, line.constData());
+ }
+ addCategory(id, QString::fromUtf8(cells[1].constData()), nexusIDs, parentID);
+ }
+ }
+ categoryFile.close();
+ }
+ std::sort(m_Categories.begin(), m_Categories.end());
+ setParents();
+}
+
+
+CategoryFactory &CategoryFactory::instance()
+{
+ if (s_Instance == nullptr) {
+ s_Instance = new CategoryFactory;
+ }
+ return *s_Instance;
+}
+
+
+void CategoryFactory::reset()
+{
+ m_Categories.clear();
+ m_IDMap.clear();
+ // 28 =
+ // 43 = Savegames (makes no sense to install them through MO)
+ // 45 = Videos and trailers
+ // 87 = Miscelanous
+ addCategory(0, "None", { 4, 28, 43, 45, 87 }, 0);
+}
+
+
+void CategoryFactory::setParents()
+{
+ for (std::vector<Category>::iterator iter = m_Categories.begin();
+ iter != m_Categories.end(); ++iter) {
+ iter->m_HasChildren = false;
+ }
+
+ for (std::vector<Category>::const_iterator categoryIter = m_Categories.begin();
+ categoryIter != m_Categories.end(); ++categoryIter) {
+ if (categoryIter->m_ParentID != 0) {
+ std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(categoryIter->m_ParentID);
+ if (iter != m_IDMap.end()) {
+ m_Categories[iter->second].m_HasChildren = true;
+ }
+ }
+ }
+}
+
+void CategoryFactory::cleanup()
+{
+ delete s_Instance;
+ s_Instance = nullptr;
+}
+
+
+void CategoryFactory::saveCategories()
+{
+ QFile categoryFile(categoriesFilePath());
+
+ if (!categoryFile.open(QIODevice::WriteOnly)) {
+ reportError(QObject::tr("Failed to save custom categories"));
+ return;
+ }
+
+ categoryFile.resize(0);
+ for (std::vector<Category>::const_iterator iter = m_Categories.begin();
+ iter != m_Categories.end(); ++iter) {
+ if (iter->m_ID == 0) {
+ continue;
+ }
+ QByteArray line;
+ line.append(QByteArray::number(iter->m_ID)).append("|")
+ .append(iter->m_Name.toUtf8()).append("|")
+ .append(VectorJoin(iter->m_NexusIDs, ",").toUtf8()).append("|")
+ .append(QByteArray::number(iter->m_ParentID)).append("\n");
+ categoryFile.write(line);
+ }
+ categoryFile.close();
+}
+
+
+unsigned int CategoryFactory::countCategories(std::function<bool (const Category &category)> filter)
+{
+ unsigned int result = 0;
+ for (const Category &cat : m_Categories) {
+ if (filter(cat)) {
+ ++result;
+ }
+ }
+ return result;
+}
+
+int CategoryFactory::addCategory(const QString &name, const std::vector<int> &nexusIDs, int parentID)
+{
+ int id = 1;
+ while (m_IDMap.find(id) != m_IDMap.end()) {
+ ++id;
+ }
+ addCategory(id, name, nexusIDs, parentID);
+
+ saveCategories();
+ return id;
+}
+
+void CategoryFactory::addCategory(int id, const QString &name, const std::vector<int> &nexusIDs, int parentID)
+{
+ int index = static_cast<int>(m_Categories.size());
+ m_Categories.push_back(Category(index, id, name, nexusIDs, parentID));
+ for (int nexusID : nexusIDs) {
+ m_NexusMap[nexusID] = index;
+ }
+ m_IDMap[id] = index;
+}
+
+
+void CategoryFactory::loadDefaultCategories()
+{
+ // the order here is relevant as it defines the order in which the
+ // mods appear in the combo box
+ addCategory(1, "Animations", { 2, 4, 51 }, 0);
+ addCategory(52, "Poses", { 1, 29 }, 1);
+ addCategory(2, "Armour", { 2, 5, 54 }, 0);
+ addCategory(53, "Power Armor", { 1, 53 }, 2);
+ addCategory(3, "Audio", { 3, 33, 35, 106 }, 0);
+ addCategory(38, "Music", { 2, 34, 61 }, 0);
+ addCategory(39, "Voice", { 2, 36, 107 }, 0);
+ addCategory(5, "Clothing", { 2, 9, 60 }, 0);
+ addCategory(41, "Jewelry", { 1, 102 }, 5);
+ addCategory(42, "Backpacks", { 1, 49 }, 5);
+ addCategory(6, "Collectables", { 2, 10, 92 }, 0);
+ addCategory(28, "Companions", { 3, 11, 66, 96 }, 0);
+ addCategory(7, "Creatures, Mounts, & Vehicles", { 4, 12, 65, 83, 101 }, 0);
+ addCategory(8, "Factions", { 2, 16, 25 }, 0);
+ addCategory(9, "Gameplay", { 2, 15, 24 }, 0);
+ addCategory(27, "Combat", { 1, 77 }, 9);
+ addCategory(43, "Crafting", { 2, 50, 100 }, 9);
+ addCategory(48, "Overhauls", { 2, 24, 79 }, 9);
+ addCategory(49, "Perks", { 1, 27 }, 9);
+ addCategory(54, "Radio", { 1, 31 }, 9);
+ addCategory(55, "Shouts", { 1, 104 }, 9);
+ addCategory(22, "Skills & Levelling", { 2, 46, 73 }, 9);
+ addCategory(58, "Weather & Lighting", { 1, 56 }, 9);
+ addCategory(44, "Equipment", { 1, 44 }, 43);
+ addCategory(45, "Home/Settlement", { 1, 45 }, 43);
+ addCategory(10, "Body, Face, & Hair", { 2, 17, 26 }, 0);
+ addCategory(56, "Tattoos", { 1, 57 }, 10);
+ addCategory(40, "Character Presets", { 1, 58 }, 0);
+ addCategory(11, "Items", { 2, 27, 85 }, 0);
+ addCategory(32, "Mercantile", { 2, 23, 69 }, 0);
+ addCategory(37, "Ammo", { 1, 3 }, 11);
+ addCategory(19, "Weapons", { 2, 41, 55 }, 11);
+ addCategory(36, "Weapon & Armour Sets", { 1, 42 }, 11);
+ addCategory(23, "Player Homes", { 2, 28, 67 }, 0);
+ addCategory(25, "Castles & Mansions", { 1, 68 }, 23);
+ addCategory(51, "Settlements", { 1, 48 }, 23);
+ addCategory(12, "Locations", { 10, 20, 21, 22, 30, 47, 70, 88, 89, 90, 91 }, 0);
+ addCategory(4, "Cities", { 1, 53 }, 12);
+ addCategory(31, "Landscape Changes", { 1, 58 }, 0);
+ addCategory(29, "Environment", { 2, 14, 74 }, 0);
+ addCategory(30, "Immersion", { 2, 51, 78 }, 0);
+ addCategory(20, "Magic", { 3, 75, 93, 94 }, 0);
+ addCategory(21, "Models & Textures", { 2, 19, 29 }, 0);
+ addCategory(33, "Modders resources", { 2, 18, 82 }, 0);
+ addCategory(13, "NPCs", { 3, 22, 33, 99 }, 0);
+ addCategory(24, "Bugfixes", { 2, 6, 95 }, 0);
+ addCategory(14, "Patches", { 2, 25, 84 }, 24);
+ addCategory(35, "Utilities", { 2, 38, 39 }, 0);
+ addCategory(26, "Cheats", { 1, 8 }, 0);
+ addCategory(15, "Quests", { 2, 30, 35 }, 0);
+ addCategory(16, "Races & Classes", { 1, 34 }, 0);
+ addCategory(34, "Stealth", { 1, 76 }, 0);
+ addCategory(17, "UI", { 2, 37, 42 }, 0);
+ addCategory(18, "Visuals", { 2, 40, 62 }, 0);
+ addCategory(50, "Pip-Boy", { 1, 52 }, 18);
+ addCategory(46, "Shader Presets", { 3, 13, 97, 105 }, 0);
+ addCategory(47, "Miscellaneous", { 2, 2, 28 }, 0);
+}
+
+
+int CategoryFactory::getParentID(unsigned int index) const
+{
+ if (index >= m_Categories.size()) {
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
+ }
+
+ return m_Categories[index].m_ParentID;
+}
+
+
+bool CategoryFactory::categoryExists(int id) const
+{
+ return m_IDMap.find(id) != m_IDMap.end();
+}
+
+
+bool CategoryFactory::isDescendantOf(int id, int parentID) const
+{
+ // handles cycles
+ std::set<int> seen;
+ return isDescendantOfImpl(id, parentID, seen);
+}
+
+bool CategoryFactory::isDescendantOfImpl(
+ int id, int parentID, std::set<int>& seen) const
+{
+ if (!seen.insert(id).second) {
+ log::error("cycle in category: {}", id);
+ return false;
+ }
+
+ std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(id);
+
+ if (iter != m_IDMap.end()) {
+ unsigned int index = iter->second;
+ if (m_Categories[index].m_ParentID == 0) {
+ return false;
+ } else if (m_Categories[index].m_ParentID == parentID) {
+ return true;
+ } else {
+ return isDescendantOfImpl(m_Categories[index].m_ParentID, parentID, seen);
+ }
+ } else {
+ log::warn("{} is no valid category id", id);
+ return false;
+ }
+}
+
+
+bool CategoryFactory::hasChildren(unsigned int index) const
+{
+ if (index >= m_Categories.size()) {
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
+ }
+
+ return m_Categories[index].m_HasChildren;
+}
+
+
+QString CategoryFactory::getCategoryName(unsigned int index) const
+{
+ if (index >= m_Categories.size()) {
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
+ }
+
+ return m_Categories[index].m_Name;
+}
+
+QString CategoryFactory::getSpecialCategoryName(SpecialCategories type) const
+{
+ QString label;
+ switch (type)
+ {
+ case Checked: label = QObject::tr("Active"); break;
+ case UpdateAvailable: label = QObject::tr("Update available"); break;
+ case HasCategory: label = QObject::tr("Has category"); break;
+ case Conflict: label = QObject::tr("Conflicted"); break;
+ case HasHiddenFiles: label = QObject::tr("Has hidden files"); break;
+ case Endorsed: label = QObject::tr("Endorsed"); break;
+ case Backup: label = QObject::tr("Has backup"); break;
+ case Managed: label = QObject::tr("Managed"); break;
+ case HasGameData: label = QObject::tr("Has valid game data"); break;
+ case HasNexusID: label = QObject::tr("Has Nexus ID"); break;
+ case Tracked: label = QObject::tr("Tracked on Nexus"); break;
+ default: return {};
+ }
+ return QString("<%1>").arg(label);
+}
+
+QString CategoryFactory::getCategoryNameByID(int id) const
+{
+ auto itor = m_IDMap.find(id);
+
+ if (itor == m_IDMap.end()) {
+ return getSpecialCategoryName(static_cast<SpecialCategories>(id));
+ } else {
+ const auto index = itor->second;
+ if (index >= m_Categories.size()) {
+ return {};
+ }
+
+ return m_Categories[index].m_Name;
+ }
+}
+
+int CategoryFactory::getCategoryID(unsigned int index) const
+{
+ if (index >= m_Categories.size()) {
+ throw MyException(QObject::tr("invalid category index: %1").arg(index));
+ }
+
+ return m_Categories[index].m_ID;
+}
+
+
+int CategoryFactory::getCategoryIndex(int ID) const
+{
+ std::map<int, unsigned int>::const_iterator iter = m_IDMap.find(ID);
+ if (iter == m_IDMap.end()) {
+ throw MyException(QObject::tr("invalid category id: %1").arg(ID));
+ }
+ return iter->second;
+}
+
+
+int CategoryFactory::getCategoryID(const QString &name) const
+{
+ auto iter = std::find_if(m_Categories.begin(), m_Categories.end(),
+ [name] (const Category &cat) -> bool {
+ return cat.m_Name == name;
+ });
+
+ if (iter != m_Categories.end()) {
+ return iter->m_ID;
+ } else {
+ return -1;
+ }
+}
+
+
+unsigned int CategoryFactory::resolveNexusID(int nexusID) const
+{
+ std::map<int, unsigned int>::const_iterator iter = m_NexusMap.find(nexusID);
+ if (iter != m_NexusMap.end()) {
+ log::debug("nexus category id {} maps to internal {}", nexusID, iter->second);
+ return iter->second;
+ } else {
+ log::debug("nexus category id {} not mapped", nexusID);
+ return 0U;
+ }
+}
diff --git a/src/categories.h b/src/categories.h
index e4a3ede9..50be9fc4 100644
--- a/src/categories.h
+++ b/src/categories.h
@@ -1,217 +1,217 @@
-/*
-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 CATEGORIES_H
-#define CATEGORIES_H
-
-
-#include <QString>
-#include <vector>
-#include <map>
-#include <functional>
-
-
-/**
- * @brief Manage the available mod categories
- * @warning member functions of this class currently use a wild mix of ids and indexes to look up categories,
- * optimized to where the request comes from. Therefore be very careful which of the two you have available
- **/
-class CategoryFactory {
-
- friend class CategoriesDialog;
-
-public:
- enum SpecialCategories
- {
- Checked = 10000,
- UpdateAvailable,
- HasCategory,
- Conflict,
- HasHiddenFiles,
- Endorsed,
- Backup,
- Managed,
- HasGameData,
- HasNexusID,
- Tracked
- };
-
-public:
- struct Category {
- Category(int sortValue, int id, const QString &name, const std::vector<int> &nexusIDs, int parentID)
- : m_SortValue(sortValue), m_ID(id), m_Name(name), m_HasChildren(false),
- m_NexusIDs(nexusIDs), m_ParentID(parentID) {}
- int m_SortValue;
- int m_ID;
- int m_ParentID;
- bool m_HasChildren;
- QString m_Name;
- std::vector<int> m_NexusIDs;
-
- friend bool operator<(const Category &LHS, const Category &RHS) {
- return LHS.m_SortValue < RHS.m_SortValue;
- }
- };
-
-public:
-
- /**
- * @brief reset the list of categories
- **/
- void reset();
-
- /**
- * @brief read categories from file
- */
- void loadCategories();
-
- /**
- * @brief save the categories to the categories.dat file
- **/
- void saveCategories();
-
- int addCategory(const QString &name, const std::vector<int> &nexusIDs, int parentID);
-
- /**
- * @brief retrieve the number of available categories
- *
- * @return unsigned int number of categories
- **/
- size_t numCategories() const { return m_Categories.size(); }
-
- /**
- * @brief count all categories that match a specified filter
- * @param filter the filter to test
- * @return number of matching categories
- */
- unsigned int countCategories(std::function<bool (const Category &category)> filter);
-
- /**
- * @brief get the id of the parent category
- *
- * @param index the index to look up
- * @return int id of the parent category
- **/
- int getParentID(unsigned int index) const;
-
- /**
- * @brief determine if a category exists (by id)
- *
- * @param id the id to check for existance
- * @return true if the category exists, false otherwise
- **/
- bool categoryExists(int id) const;
-
- /**
- * @brief test if a category is child of a second one
- * @param id the presumed child id
- * @param parentID the parent id to test for
- * @return true if id is a child of parentID
- **/
- bool isDescendantOf(int id, int parentID) const;
-
- /**
- * @brief test if the specified category has child categories
- *
- * @param index index of the category to look up
- * @return bool true if the category has child categories
- **/
- bool hasChildren(unsigned int index) const;
-
- /**
- * @brief retrieve the name of a category
- *
- * @param index index of the category to look up
- * @return QString name of the category
- **/
- QString getCategoryName(unsigned int index) const;
- QString getSpecialCategoryName(SpecialCategories type) const;
- QString getCategoryNameByID(int id) const;
-
- /**
- * @brief look up the id of a category by its index
- *
- * @param index index of the category to look up
- * @return int id of the category
- **/
- int getCategoryID(unsigned int index) const;
-
- /**
- * @brief look up the id of a category by its name
- * @note O(n)
- */
- int getCategoryID(const QString &name) const;
-
- /**
- * @brief look up the index of a category by its id
- *
- * @param id index of the category to look up
- * @return unsigned int index of the category
- **/
- int getCategoryIndex(int ID) const;
-
- /**
- * @brief retrieve the index of a category by its nexus id
- *
- * @param nexusID nexus id of the category to look up
- * @return unsigned int index of the category or 0 if no category matches
- **/
- unsigned int resolveNexusID(int nexusID) const;
-
-public:
-
- /**
- * @brief retrieve a reference to the singleton instance
- *
- * @return the reference to the singleton
- **/
- static CategoryFactory &instance();
-
- /**
- * @return path to the file that contains the categories list
- */
- static QString categoriesFilePath();
-
-private:
-
- CategoryFactory();
-
- void loadDefaultCategories();
-
- void addCategory(int id, const QString &name, const std::vector<int> &nexusID, int parentID);
-
- void setParents();
-
- static void cleanup();
-
-private:
-
- static CategoryFactory *s_Instance;
-
- std::vector<Category> m_Categories;
- std::map<int, unsigned int> m_IDMap;
- std::map<int, unsigned int> m_NexusMap;
-
-private:
- // called by isDescendantOf()
- bool isDescendantOfImpl(int id, int parentID, std::set<int>& seen) const;
-};
-
-
-#endif // CATEGORIES_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 CATEGORIES_H
+#define CATEGORIES_H
+
+
+#include <QString>
+#include <vector>
+#include <map>
+#include <functional>
+
+
+/**
+ * @brief Manage the available mod categories
+ * @warning member functions of this class currently use a wild mix of ids and indexes to look up categories,
+ * optimized to where the request comes from. Therefore be very careful which of the two you have available
+ **/
+class CategoryFactory {
+
+ friend class CategoriesDialog;
+
+public:
+ enum SpecialCategories
+ {
+ Checked = 10000,
+ UpdateAvailable,
+ HasCategory,
+ Conflict,
+ HasHiddenFiles,
+ Endorsed,
+ Backup,
+ Managed,
+ HasGameData,
+ HasNexusID,
+ Tracked
+ };
+
+public:
+ struct Category {
+ Category(int sortValue, int id, const QString &name, const std::vector<int> &nexusIDs, int parentID)
+ : m_SortValue(sortValue), m_ID(id), m_Name(name), m_HasChildren(false),
+ m_NexusIDs(nexusIDs), m_ParentID(parentID) {}
+ int m_SortValue;
+ int m_ID;
+ int m_ParentID;
+ bool m_HasChildren;
+ QString m_Name;
+ std::vector<int> m_NexusIDs;
+
+ friend bool operator<(const Category &LHS, const Category &RHS) {
+ return LHS.m_SortValue < RHS.m_SortValue;
+ }
+ };
+
+public:
+
+ /**
+ * @brief reset the list of categories
+ **/
+ void reset();
+
+ /**
+ * @brief read categories from file
+ */
+ void loadCategories();
+
+ /**
+ * @brief save the categories to the categories.dat file
+ **/
+ void saveCategories();
+
+ int addCategory(const QString &name, const std::vector<int> &nexusIDs, int parentID);
+
+ /**
+ * @brief retrieve the number of available categories
+ *
+ * @return unsigned int number of categories
+ **/
+ size_t numCategories() const { return m_Categories.size(); }
+
+ /**
+ * @brief count all categories that match a specified filter
+ * @param filter the filter to test
+ * @return number of matching categories
+ */
+ unsigned int countCategories(std::function<bool (const Category &category)> filter);
+
+ /**
+ * @brief get the id of the parent category
+ *
+ * @param index the index to look up
+ * @return int id of the parent category
+ **/
+ int getParentID(unsigned int index) const;
+
+ /**
+ * @brief determine if a category exists (by id)
+ *
+ * @param id the id to check for existance
+ * @return true if the category exists, false otherwise
+ **/
+ bool categoryExists(int id) const;
+
+ /**
+ * @brief test if a category is child of a second one
+ * @param id the presumed child id
+ * @param parentID the parent id to test for
+ * @return true if id is a child of parentID
+ **/
+ bool isDescendantOf(int id, int parentID) const;
+
+ /**
+ * @brief test if the specified category has child categories
+ *
+ * @param index index of the category to look up
+ * @return bool true if the category has child categories
+ **/
+ bool hasChildren(unsigned int index) const;
+
+ /**
+ * @brief retrieve the name of a category
+ *
+ * @param index index of the category to look up
+ * @return QString name of the category
+ **/
+ QString getCategoryName(unsigned int index) const;
+ QString getSpecialCategoryName(SpecialCategories type) const;
+ QString getCategoryNameByID(int id) const;
+
+ /**
+ * @brief look up the id of a category by its index
+ *
+ * @param index index of the category to look up
+ * @return int id of the category
+ **/
+ int getCategoryID(unsigned int index) const;
+
+ /**
+ * @brief look up the id of a category by its name
+ * @note O(n)
+ */
+ int getCategoryID(const QString &name) const;
+
+ /**
+ * @brief look up the index of a category by its id
+ *
+ * @param id index of the category to look up
+ * @return unsigned int index of the category
+ **/
+ int getCategoryIndex(int ID) const;
+
+ /**
+ * @brief retrieve the index of a category by its nexus id
+ *
+ * @param nexusID nexus id of the category to look up
+ * @return unsigned int index of the category or 0 if no category matches
+ **/
+ unsigned int resolveNexusID(int nexusID) const;
+
+public:
+
+ /**
+ * @brief retrieve a reference to the singleton instance
+ *
+ * @return the reference to the singleton
+ **/
+ static CategoryFactory &instance();
+
+ /**
+ * @return path to the file that contains the categories list
+ */
+ static QString categoriesFilePath();
+
+private:
+
+ CategoryFactory();
+
+ void loadDefaultCategories();
+
+ void addCategory(int id, const QString &name, const std::vector<int> &nexusID, int parentID);
+
+ void setParents();
+
+ static void cleanup();
+
+private:
+
+ static CategoryFactory *s_Instance;
+
+ std::vector<Category> m_Categories;
+ std::map<int, unsigned int> m_IDMap;
+ std::map<int, unsigned int> m_NexusMap;
+
+private:
+ // called by isDescendantOf()
+ bool isDescendantOfImpl(int id, int parentID, std::set<int>& seen) const;
+};
+
+
+#endif // CATEGORIES_H
diff --git a/src/categoriesdialog.cpp b/src/categoriesdialog.cpp
index 5181d9f1..7d195ce6 100644
--- a/src/categoriesdialog.cpp
+++ b/src/categoriesdialog.cpp
@@ -1,250 +1,250 @@
-/*
-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 "categoriesdialog.h"
-#include "ui_categoriesdialog.h"
-#include "categories.h"
-#include "utility.h"
-#include "settings.h"
-#include <QItemDelegate>
-#include <QRegularExpressionValidator>
-#include <QLineEdit>
-#include <QMenu>
-
-
-class NewIDValidator : public QIntValidator {
-public:
- NewIDValidator(const std::set<int> &ids)
- : m_UsedIDs(ids) {}
- virtual State validate(QString &input, int &pos) const {
- State intRes = QIntValidator::validate(input, pos);
- if (intRes == Acceptable) {
- bool ok = false;
- int id = input.toInt(&ok);
- if (m_UsedIDs.find(id) != m_UsedIDs.end()) {
- return QValidator::Intermediate;
- }
- }
- return intRes;
- }
-private:
- const std::set<int> &m_UsedIDs;
-};
-
-
-class ExistingIDValidator : public QIntValidator {
-public:
- ExistingIDValidator(const std::set<int> &ids)
- : m_UsedIDs(ids) {}
- virtual State validate(QString &input, int &pos) const {
- State intRes = QIntValidator::validate(input, pos);
- if (intRes == Acceptable) {
- bool ok = false;
- int id = input.toInt(&ok);
- if ((id == 0) || (m_UsedIDs.find(id) != m_UsedIDs.end())) {
- return QValidator::Acceptable;
- } else {
- return QValidator::Intermediate;
- }
- } else {
- return intRes;
- }
- }
-private:
- const std::set<int> &m_UsedIDs;
-};
-
-
-class ValidatingDelegate : public QItemDelegate {
-
-public:
- ValidatingDelegate(QObject *parent, QValidator *validator)
- : QItemDelegate(parent), m_Validator(validator) {}
-
- QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem&, const QModelIndex&) const
- {
- QLineEdit *edit = new QLineEdit(parent);
- edit->setValidator(m_Validator);
- return edit;
- }
- virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
- {
- QLineEdit *edit = qobject_cast<QLineEdit*>(editor);
- int pos = 0;
- QString editText = edit->text();
- if (m_Validator->validate(editText, pos) == QValidator::Acceptable) {
- QItemDelegate::setModelData(editor, model, index);
- }
- }
-private:
- QValidator *m_Validator;
-};
-
-
-CategoriesDialog::CategoriesDialog(QWidget *parent)
- : TutorableDialog("Categories", parent), ui(new Ui::CategoriesDialog)
-{
- ui->setupUi(this);
- fillTable();
- connect(ui->categoriesTable, SIGNAL(cellChanged(int,int)), this, SLOT(cellChanged(int,int)));
-}
-
-CategoriesDialog::~CategoriesDialog()
-{
- delete ui;
-}
-
-int CategoriesDialog::exec()
-{
- GeometrySaver gs(Settings::instance(), this);
- return QDialog::exec();
-}
-
-
-void CategoriesDialog::cellChanged(int row, int)
-{
- int currentID = ui->categoriesTable->item(row, 0)->text().toInt();
- if (currentID > m_HighestID) {
- m_HighestID = currentID;
- }
-}
-
-
-void CategoriesDialog::commitChanges()
-{
- CategoryFactory &categories = CategoryFactory::instance();
- categories.reset();
-
- for (int i = 0; i < ui->categoriesTable->rowCount(); ++i) {
- int index = ui->categoriesTable->verticalHeader()->logicalIndex(i);
- QString nexusIDString = ui->categoriesTable->item(index, 2)->text();
- QStringList nexusIDStringList = nexusIDString.split(',', Qt::SkipEmptyParts);
- std::vector<int> nexusIDs;
- for (QStringList::iterator iter = nexusIDStringList.begin();
- iter != nexusIDStringList.end(); ++iter) {
- nexusIDs.push_back(iter->toInt());
- }
-
- categories.addCategory(
- ui->categoriesTable->item(index, 0)->text().toInt(),
- ui->categoriesTable->item(index, 1)->text(),
- nexusIDs,
- ui->categoriesTable->item(index, 3)->text().toInt());
- }
- categories.setParents();
-
- categories.saveCategories();
-}
-
-
-void CategoriesDialog::refreshIDs()
-{
- m_HighestID = 0;
- for (int i = 0; i < ui->categoriesTable->rowCount(); ++i) {
- int id = ui->categoriesTable->item(i, 0)->text().toInt();
- if (id > m_HighestID) {
- m_HighestID = id;
- }
- m_IDs.insert(id);
- }
-}
-
-
-void CategoriesDialog::fillTable()
-{
- CategoryFactory &categories = CategoryFactory::instance();
- QTableWidget *table = ui->categoriesTable;
-
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed);
- table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
- table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed);
- table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed);
- table->verticalHeader()->setSectionsMovable(true);
- table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
-#else
- table->horizontalHeader()->setResizeMode(0, QHeaderView::Fixed);
- table->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch);
- table->horizontalHeader()->setResizeMode(2, QHeaderView::Fixed);
- table->horizontalHeader()->setResizeMode(3, QHeaderView::Fixed);
- table->verticalHeader()->setMovable(true);
- table->verticalHeader()->setResizeMode(QHeaderView::Fixed);
-#endif
-
-
- table->setItemDelegateForColumn(0, new ValidatingDelegate(this, new NewIDValidator(m_IDs)));
- table->setItemDelegateForColumn(2, new ValidatingDelegate(this, new QRegularExpressionValidator(QRegularExpression("([0-9]+)?(,[0-9]+)*"), this)));
- table->setItemDelegateForColumn(3, new ValidatingDelegate(this, new ExistingIDValidator(m_IDs)));
-
- int row = 0;
- for (std::vector<CategoryFactory::Category>::const_iterator iter = categories.m_Categories.begin();
- iter != categories.m_Categories.end(); ++iter, ++row) {
- const CategoryFactory::Category &category = *iter;
- if (category.m_ID == 0) {
- --row;
- continue;
- }
- table->insertRow(row);
-// table->setVerticalHeaderItem(row, new QTableWidgetItem(" "));
-
- QScopedPointer<QTableWidgetItem> idItem(new QTableWidgetItem());
- idItem->setData(Qt::DisplayRole, category.m_ID);
-
- QScopedPointer<QTableWidgetItem> nameItem(new QTableWidgetItem(category.m_Name));
- QScopedPointer<QTableWidgetItem> nexusIDItem(new QTableWidgetItem(MOBase::VectorJoin(category.m_NexusIDs, ",")));
- QScopedPointer<QTableWidgetItem> parentIDItem(new QTableWidgetItem());
- parentIDItem->setData(Qt::DisplayRole, category.m_ParentID);
-
- table->setItem(row, 0, idItem.take());
- table->setItem(row, 1, nameItem.take());
- table->setItem(row, 2, nexusIDItem.take());
- table->setItem(row, 3, parentIDItem.take());
- }
-
- refreshIDs();
-}
-
-
-void CategoriesDialog::addCategory_clicked()
-{
- int row = m_ContextRow >= 0 ? m_ContextRow : 0;
- ui->categoriesTable->insertRow(row);
- ui->categoriesTable->setVerticalHeaderItem(row, new QTableWidgetItem(" "));
- ui->categoriesTable->setItem(row, 0, new QTableWidgetItem(QString::number(++m_HighestID)));
- ui->categoriesTable->setItem(row, 1, new QTableWidgetItem("new"));
- ui->categoriesTable->setItem(row, 2, new QTableWidgetItem(""));
- ui->categoriesTable->setItem(row, 3, new QTableWidgetItem("0"));
-}
-
-
-void CategoriesDialog::removeCategory_clicked()
-{
- ui->categoriesTable->removeRow(m_ContextRow);
-}
-
-
-void CategoriesDialog::on_categoriesTable_customContextMenuRequested(const QPoint &pos)
-{
- m_ContextRow = ui->categoriesTable->rowAt(pos.y());
- QMenu menu;
- menu.addAction(tr("Add"), this, SLOT(addCategory_clicked()));
- menu.addAction(tr("Remove"), this, SLOT(removeCategory_clicked()));
-
- menu.exec(ui->categoriesTable->mapToGlobal(pos));
-}
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "categoriesdialog.h"
+#include "ui_categoriesdialog.h"
+#include "categories.h"
+#include "utility.h"
+#include "settings.h"
+#include <QItemDelegate>
+#include <QRegularExpressionValidator>
+#include <QLineEdit>
+#include <QMenu>
+
+
+class NewIDValidator : public QIntValidator {
+public:
+ NewIDValidator(const std::set<int> &ids)
+ : m_UsedIDs(ids) {}
+ virtual State validate(QString &input, int &pos) const {
+ State intRes = QIntValidator::validate(input, pos);
+ if (intRes == Acceptable) {
+ bool ok = false;
+ int id = input.toInt(&ok);
+ if (m_UsedIDs.find(id) != m_UsedIDs.end()) {
+ return QValidator::Intermediate;
+ }
+ }
+ return intRes;
+ }
+private:
+ const std::set<int> &m_UsedIDs;
+};
+
+
+class ExistingIDValidator : public QIntValidator {
+public:
+ ExistingIDValidator(const std::set<int> &ids)
+ : m_UsedIDs(ids) {}
+ virtual State validate(QString &input, int &pos) const {
+ State intRes = QIntValidator::validate(input, pos);
+ if (intRes == Acceptable) {
+ bool ok = false;
+ int id = input.toInt(&ok);
+ if ((id == 0) || (m_UsedIDs.find(id) != m_UsedIDs.end())) {
+ return QValidator::Acceptable;
+ } else {
+ return QValidator::Intermediate;
+ }
+ } else {
+ return intRes;
+ }
+ }
+private:
+ const std::set<int> &m_UsedIDs;
+};
+
+
+class ValidatingDelegate : public QItemDelegate {
+
+public:
+ ValidatingDelegate(QObject *parent, QValidator *validator)
+ : QItemDelegate(parent), m_Validator(validator) {}
+
+ QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem&, const QModelIndex&) const
+ {
+ QLineEdit *edit = new QLineEdit(parent);
+ edit->setValidator(m_Validator);
+ return edit;
+ }
+ virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
+ {
+ QLineEdit *edit = qobject_cast<QLineEdit*>(editor);
+ int pos = 0;
+ QString editText = edit->text();
+ if (m_Validator->validate(editText, pos) == QValidator::Acceptable) {
+ QItemDelegate::setModelData(editor, model, index);
+ }
+ }
+private:
+ QValidator *m_Validator;
+};
+
+
+CategoriesDialog::CategoriesDialog(QWidget *parent)
+ : TutorableDialog("Categories", parent), ui(new Ui::CategoriesDialog)
+{
+ ui->setupUi(this);
+ fillTable();
+ connect(ui->categoriesTable, SIGNAL(cellChanged(int,int)), this, SLOT(cellChanged(int,int)));
+}
+
+CategoriesDialog::~CategoriesDialog()
+{
+ delete ui;
+}
+
+int CategoriesDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
+
+void CategoriesDialog::cellChanged(int row, int)
+{
+ int currentID = ui->categoriesTable->item(row, 0)->text().toInt();
+ if (currentID > m_HighestID) {
+ m_HighestID = currentID;
+ }
+}
+
+
+void CategoriesDialog::commitChanges()
+{
+ CategoryFactory &categories = CategoryFactory::instance();
+ categories.reset();
+
+ for (int i = 0; i < ui->categoriesTable->rowCount(); ++i) {
+ int index = ui->categoriesTable->verticalHeader()->logicalIndex(i);
+ QString nexusIDString = ui->categoriesTable->item(index, 2)->text();
+ QStringList nexusIDStringList = nexusIDString.split(',', Qt::SkipEmptyParts);
+ std::vector<int> nexusIDs;
+ for (QStringList::iterator iter = nexusIDStringList.begin();
+ iter != nexusIDStringList.end(); ++iter) {
+ nexusIDs.push_back(iter->toInt());
+ }
+
+ categories.addCategory(
+ ui->categoriesTable->item(index, 0)->text().toInt(),
+ ui->categoriesTable->item(index, 1)->text(),
+ nexusIDs,
+ ui->categoriesTable->item(index, 3)->text().toInt());
+ }
+ categories.setParents();
+
+ categories.saveCategories();
+}
+
+
+void CategoriesDialog::refreshIDs()
+{
+ m_HighestID = 0;
+ for (int i = 0; i < ui->categoriesTable->rowCount(); ++i) {
+ int id = ui->categoriesTable->item(i, 0)->text().toInt();
+ if (id > m_HighestID) {
+ m_HighestID = id;
+ }
+ m_IDs.insert(id);
+ }
+}
+
+
+void CategoriesDialog::fillTable()
+{
+ CategoryFactory &categories = CategoryFactory::instance();
+ QTableWidget *table = ui->categoriesTable;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
+ table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed);
+ table->verticalHeader()->setSectionsMovable(true);
+ table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
+#else
+ table->horizontalHeader()->setResizeMode(0, QHeaderView::Fixed);
+ table->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch);
+ table->horizontalHeader()->setResizeMode(2, QHeaderView::Fixed);
+ table->horizontalHeader()->setResizeMode(3, QHeaderView::Fixed);
+ table->verticalHeader()->setMovable(true);
+ table->verticalHeader()->setResizeMode(QHeaderView::Fixed);
+#endif
+
+
+ table->setItemDelegateForColumn(0, new ValidatingDelegate(this, new NewIDValidator(m_IDs)));
+ table->setItemDelegateForColumn(2, new ValidatingDelegate(this, new QRegularExpressionValidator(QRegularExpression("([0-9]+)?(,[0-9]+)*"), this)));
+ table->setItemDelegateForColumn(3, new ValidatingDelegate(this, new ExistingIDValidator(m_IDs)));
+
+ int row = 0;
+ for (std::vector<CategoryFactory::Category>::const_iterator iter = categories.m_Categories.begin();
+ iter != categories.m_Categories.end(); ++iter, ++row) {
+ const CategoryFactory::Category &category = *iter;
+ if (category.m_ID == 0) {
+ --row;
+ continue;
+ }
+ table->insertRow(row);
+// table->setVerticalHeaderItem(row, new QTableWidgetItem(" "));
+
+ QScopedPointer<QTableWidgetItem> idItem(new QTableWidgetItem());
+ idItem->setData(Qt::DisplayRole, category.m_ID);
+
+ QScopedPointer<QTableWidgetItem> nameItem(new QTableWidgetItem(category.m_Name));
+ QScopedPointer<QTableWidgetItem> nexusIDItem(new QTableWidgetItem(MOBase::VectorJoin(category.m_NexusIDs, ",")));
+ QScopedPointer<QTableWidgetItem> parentIDItem(new QTableWidgetItem());
+ parentIDItem->setData(Qt::DisplayRole, category.m_ParentID);
+
+ table->setItem(row, 0, idItem.take());
+ table->setItem(row, 1, nameItem.take());
+ table->setItem(row, 2, nexusIDItem.take());
+ table->setItem(row, 3, parentIDItem.take());
+ }
+
+ refreshIDs();
+}
+
+
+void CategoriesDialog::addCategory_clicked()
+{
+ int row = m_ContextRow >= 0 ? m_ContextRow : 0;
+ ui->categoriesTable->insertRow(row);
+ ui->categoriesTable->setVerticalHeaderItem(row, new QTableWidgetItem(" "));
+ ui->categoriesTable->setItem(row, 0, new QTableWidgetItem(QString::number(++m_HighestID)));
+ ui->categoriesTable->setItem(row, 1, new QTableWidgetItem("new"));
+ ui->categoriesTable->setItem(row, 2, new QTableWidgetItem(""));
+ ui->categoriesTable->setItem(row, 3, new QTableWidgetItem("0"));
+}
+
+
+void CategoriesDialog::removeCategory_clicked()
+{
+ ui->categoriesTable->removeRow(m_ContextRow);
+}
+
+
+void CategoriesDialog::on_categoriesTable_customContextMenuRequested(const QPoint &pos)
+{
+ m_ContextRow = ui->categoriesTable->rowAt(pos.y());
+ QMenu menu;
+ menu.addAction(tr("Add"), this, SLOT(addCategory_clicked()));
+ menu.addAction(tr("Remove"), this, SLOT(removeCategory_clicked()));
+
+ menu.exec(ui->categoriesTable->mapToGlobal(pos));
+}
diff --git a/src/categoriesdialog.h b/src/categoriesdialog.h
index c743c157..39fb7130 100644
--- a/src/categoriesdialog.h
+++ b/src/categoriesdialog.h
@@ -1,75 +1,75 @@
-/*
-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 CATEGORIESDIALOG_H
-#define CATEGORIESDIALOG_H
-
-#include "tutorabledialog.h"
-#include <set>
-
-namespace Ui {
-class CategoriesDialog;
-}
-
-/**
- * @brief Dialog that allows users to configure mod categories
- **/
-class CategoriesDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT
-
-public:
-
- explicit CategoriesDialog(QWidget *parent = 0);
- ~CategoriesDialog();
-
- // also saves and restores geometry
- //
- int exec() override;
-
- /**
- * @brief store changes here to the global categories store (categories.h)
- *
- **/
- void commitChanges();
-
-private slots:
-
- void on_categoriesTable_customContextMenuRequested(const QPoint &pos);
- void addCategory_clicked();
- void removeCategory_clicked();
- void cellChanged(int row, int column);
-
-private:
-
- void refreshIDs();
- void fillTable();
-
-private:
-
- Ui::CategoriesDialog *ui;
- int m_ContextRow;
-
- int m_HighestID;
- std::set<int> m_IDs;
-
-};
-
-#endif // CATEGORIESDIALOG_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 CATEGORIESDIALOG_H
+#define CATEGORIESDIALOG_H
+
+#include "tutorabledialog.h"
+#include <set>
+
+namespace Ui {
+class CategoriesDialog;
+}
+
+/**
+ * @brief Dialog that allows users to configure mod categories
+ **/
+class CategoriesDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
+
+public:
+
+ explicit CategoriesDialog(QWidget *parent = 0);
+ ~CategoriesDialog();
+
+ // also saves and restores geometry
+ //
+ int exec() override;
+
+ /**
+ * @brief store changes here to the global categories store (categories.h)
+ *
+ **/
+ void commitChanges();
+
+private slots:
+
+ void on_categoriesTable_customContextMenuRequested(const QPoint &pos);
+ void addCategory_clicked();
+ void removeCategory_clicked();
+ void cellChanged(int row, int column);
+
+private:
+
+ void refreshIDs();
+ void fillTable();
+
+private:
+
+ Ui::CategoriesDialog *ui;
+ int m_ContextRow;
+
+ int m_HighestID;
+ std::set<int> m_IDs;
+
+};
+
+#endif // CATEGORIESDIALOG_H
+
diff --git a/src/credentialsdialog.cpp b/src/credentialsdialog.cpp
index 73e75387..91e4d309 100644
--- a/src/credentialsdialog.cpp
+++ b/src/credentialsdialog.cpp
@@ -1,43 +1,43 @@
-/*
-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 "credentialsdialog.h"
-#include "ui_credentialsdialog.h"
-
-CredentialsDialog::CredentialsDialog(QWidget *parent) :
- QDialog(parent),
- ui(new Ui::CredentialsDialog)
-{
- ui->setupUi(this);
-}
-
-CredentialsDialog::~CredentialsDialog()
-{
- delete ui;
-}
-
-bool CredentialsDialog::store() const
-{
- return ui->rememberCheck->isChecked();
-}
-
-bool CredentialsDialog::neverAsk() const
-{
- return ui->dontaskBox->isChecked();
-}
+/*
+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 "credentialsdialog.h"
+#include "ui_credentialsdialog.h"
+
+CredentialsDialog::CredentialsDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::CredentialsDialog)
+{
+ ui->setupUi(this);
+}
+
+CredentialsDialog::~CredentialsDialog()
+{
+ delete ui;
+}
+
+bool CredentialsDialog::store() const
+{
+ return ui->rememberCheck->isChecked();
+}
+
+bool CredentialsDialog::neverAsk() const
+{
+ return ui->dontaskBox->isChecked();
+}
diff --git a/src/credentialsdialog.h b/src/credentialsdialog.h
index 2f3bcbb7..598e100b 100644
--- a/src/credentialsdialog.h
+++ b/src/credentialsdialog.h
@@ -1,44 +1,44 @@
-/*
-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 CREDENTIALSDIALOG_H
-#define CREDENTIALSDIALOG_H
-
-#include <QDialog>
-
-namespace Ui {
-class CredentialsDialog;
-}
-
-class CredentialsDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit CredentialsDialog(QWidget *parent = 0);
- ~CredentialsDialog();
-
- bool store() const;
- bool neverAsk() const;
-
-private:
- Ui::CredentialsDialog *ui;
-};
-
-#endif // CREDENTIALSDIALOG_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 CREDENTIALSDIALOG_H
+#define CREDENTIALSDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+class CredentialsDialog;
+}
+
+class CredentialsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CredentialsDialog(QWidget *parent = 0);
+ ~CredentialsDialog();
+
+ bool store() const;
+ bool neverAsk() const;
+
+private:
+ Ui::CredentialsDialog *ui;
+};
+
+#endif // CREDENTIALSDIALOG_H
diff --git a/src/csvbuilder.cpp b/src/csvbuilder.cpp
index 788c9438..411fa051 100644
--- a/src/csvbuilder.cpp
+++ b/src/csvbuilder.cpp
@@ -1,272 +1,272 @@
-#include "csvbuilder.h"
-
-
-CSVBuilder::CSVBuilder(QIODevice *target)
- : m_Out(target), m_Separator(','), m_LineBreak(BREAK_CRLF)
-{
- m_Out.setEncoding(QStringConverter::Encoding::Utf8);
-
- m_QuoteMode[TYPE_INTEGER] = QUOTE_NEVER;
- m_QuoteMode[TYPE_FLOAT] = QUOTE_NEVER;
- m_QuoteMode[TYPE_STRING] = QUOTE_ONDEMAND;
-}
-
-
-CSVBuilder::~CSVBuilder()
-{
-
-}
-
-
-void CSVBuilder::setFieldSeparator(char sep)
-{
- char oldSeparator = m_Separator;
- m_Separator = sep;
- try {
- checkFields(m_Fields);
- } catch (const CSVException&) {
- m_Separator = oldSeparator;
- throw;
- }
-}
-
-
-void CSVBuilder::setLineBreak(CSVBuilder::ELineBreak lineBreak)
-{
- m_LineBreak = lineBreak;
-}
-
-
-void CSVBuilder::setEscapeMode(CSVBuilder::EFieldType type, CSVBuilder::EQuoteMode mode)
-{
- m_QuoteMode[type] = mode;
-}
-
-
-void CSVBuilder::setFields(const std::vector<std::pair<QString, EFieldType> > &fields)
-{
- std::vector<QString> fieldNames;
- std::map<QString, EFieldType> fieldTypes;
-
- for (auto iter = fields.begin(); iter != fields.end(); ++iter) {
- fieldNames.push_back(iter->first);
- fieldTypes[iter->first] = iter->second;
- }
-
- checkFields(fieldNames);
-
- m_Fields = fieldNames;
- m_FieldTypes = fieldTypes;
- m_Defaults.clear();
- m_RowBuffer.clear();
-
-}
-
-
-void CSVBuilder::checkValue(const QString &field, const QVariant &value)
-{
- auto typeIter = m_FieldTypes.find(field);
- if (typeIter == m_FieldTypes.end()) {
- throw CSVException(QObject::tr("invalid field name \"%1\"").arg(field));
- }
-
- switch (typeIter->second) {
- case TYPE_INTEGER: {
- if (!value.canConvert<int>()) {
- throw CSVException(QObject::tr("invalid type for \"%1\" (should be integer)").arg(field));
- }
- } break;
- case TYPE_STRING: {
- if (!value.canConvert<QString>()) {
- throw CSVException(QObject::tr("invalid type for \"%1\" (should be string)").arg(field));
- }
- } break;
- case TYPE_FLOAT: {
- if (!value.canConvert<float>()) {
- throw CSVException(QObject::tr("invalid type for \"%1\" (should be float)").arg(field));
- }
- } break;
- }
-}
-
-
-void CSVBuilder::setDefault(const QString &field, const QVariant &value)
-{
- checkValue(field, value);
- m_Defaults[field] = value;
-}
-
-
-void CSVBuilder::writeHeader()
-{
- if (m_Fields.size() == 0) {
- throw CSVException(QObject::tr("no fields set up yet!"));
- }
-
- for (auto iter = m_Fields.begin(); iter != m_Fields.end(); ++iter) {
- if (iter != m_Fields.begin()) {
- m_Out << m_Separator;
- }
- m_Out << *iter;
- }
- m_Out << lineBreak();
- m_Out.flush();
-}
-
-
-void CSVBuilder::setRowField(const QString &field, const QVariant &value)
-{
- checkValue(field, value);
- m_RowBuffer[field] = value;
-}
-
-
-void CSVBuilder::writeData(const std::map<QString, QVariant> &data, bool check)
-{
- QString line;
- QTextStream temp(&line);
-
- for (auto iter = m_Fields.begin(); iter != m_Fields.end(); ++iter) {
- if (iter != m_Fields.begin()) {
- temp << separator();
- }
-
- QVariant val;
-
- auto valIter = data.find(*iter);
- if (valIter == data.end()) {
- auto defaultIter = m_Defaults.find(*iter);
- if (defaultIter == m_Defaults.end()) {
- throw CSVException(QObject::tr("field not set \"%1\"").arg(*iter));
- } else {
- val = defaultIter->second;
- }
- } else {
- val = valIter->second;
- }
-
- if (check) {
- checkValue(*iter, val);
- }
-
- switch (m_FieldTypes[*iter]) {
- case TYPE_INTEGER: {
- quoteInsert(temp, val.toInt());
- } break;
- case TYPE_FLOAT: {
- quoteInsert(temp, val.toFloat());
- } break;
- case TYPE_STRING: {
- quoteInsert(temp, val.toString());
- } break;
- }
- }
- m_Out << line << lineBreak();
- m_Out.flush();
-}
-
-
-void CSVBuilder::quoteInsert(QTextStream &stream, int value)
-{
- switch (m_QuoteMode[TYPE_INTEGER]) {
- case QUOTE_NEVER:
- case QUOTE_ONDEMAND: {
- stream << value;
- } break;
- case QUOTE_ALWAYS: {
- stream << "\"" << value << "\"";
- } break;
- }
-}
-
-
-void CSVBuilder::quoteInsert(QTextStream &stream, float value)
-{
- switch (m_QuoteMode[TYPE_FLOAT]) {
- case QUOTE_NEVER:
- case QUOTE_ONDEMAND: {
- stream << value;
- } break;
- case QUOTE_ALWAYS: {
- stream << "\"" << value << "\"";
- } break;
- }
-}
-
-
-void CSVBuilder::quoteInsert(QTextStream &stream, const QString &value)
-{
- switch (m_QuoteMode[TYPE_STRING]) {
- case QUOTE_NEVER: {
- stream << value;
- } break;
- case QUOTE_ONDEMAND: {
- if (value.contains("[,\r\n]")) {
- stream << "\"" << value.mid(0).replace("\"", "\"\"") << "\"";
- } else {
- stream << value;
- }
- } break;
- case QUOTE_ALWAYS: {
- stream << "\"" << value.mid(0).replace("\"", "\"\"") << "\"";
- } break;
- }
-}
-
-
-void CSVBuilder::writeRow()
-{
- writeData(m_RowBuffer, false); // data was tested on input
- m_RowBuffer.clear();
-}
-
-
-void CSVBuilder::addRow(const std::map<QString, QVariant> &data)
-{
- writeData(data, true);
-}
-
-
-void CSVBuilder::checkFields(const std::vector<QString> &fields)
-{
- for (auto iter = fields.begin(); iter != fields.end(); ++iter) {
- if (iter->contains(m_Separator) ||
- iter->contains('\r') ||
- iter->contains('\n') ||
- iter->contains('"')) {
- throw CSVException(QObject::tr("invalid character in field \"%1\"").arg(*iter));
- }
- if (iter->length() == 0) {
- throw CSVException(QObject::tr("empty field name"));
- }
- }
-}
-
-
-/*
-
-if(cell.contains(KDefaultEscapeChar) || cell.contains(KDefaultNewLine)
- || cell.contains(KDefaultDelimiter)) {
- m_currentLine->append(cell.replace(KDefaultNewLine,
- QString(KDefaultEscapeChar) + KDefaultEscapeChar)
- .prepend(KDefaultEscapeChar)
- .append(KDefaultEscapeChar));
-} else {
-m_currentLine->append(cell);
-}*/
-
-
-const char *CSVBuilder::lineBreak()
-{
- switch (m_LineBreak) {
- case BREAK_CR: return "\r";
- case BREAK_CRLF: return "\r\n";
- case BREAK_LF: return "\n";
- default: return "\n"; // default shouldn't be necessary
- }
-}
-
-const char CSVBuilder::separator()
-{
- return m_Separator;
-}
+#include "csvbuilder.h"
+
+
+CSVBuilder::CSVBuilder(QIODevice *target)
+ : m_Out(target), m_Separator(','), m_LineBreak(BREAK_CRLF)
+{
+ m_Out.setEncoding(QStringConverter::Encoding::Utf8);
+
+ m_QuoteMode[TYPE_INTEGER] = QUOTE_NEVER;
+ m_QuoteMode[TYPE_FLOAT] = QUOTE_NEVER;
+ m_QuoteMode[TYPE_STRING] = QUOTE_ONDEMAND;
+}
+
+
+CSVBuilder::~CSVBuilder()
+{
+
+}
+
+
+void CSVBuilder::setFieldSeparator(char sep)
+{
+ char oldSeparator = m_Separator;
+ m_Separator = sep;
+ try {
+ checkFields(m_Fields);
+ } catch (const CSVException&) {
+ m_Separator = oldSeparator;
+ throw;
+ }
+}
+
+
+void CSVBuilder::setLineBreak(CSVBuilder::ELineBreak lineBreak)
+{
+ m_LineBreak = lineBreak;
+}
+
+
+void CSVBuilder::setEscapeMode(CSVBuilder::EFieldType type, CSVBuilder::EQuoteMode mode)
+{
+ m_QuoteMode[type] = mode;
+}
+
+
+void CSVBuilder::setFields(const std::vector<std::pair<QString, EFieldType> > &fields)
+{
+ std::vector<QString> fieldNames;
+ std::map<QString, EFieldType> fieldTypes;
+
+ for (auto iter = fields.begin(); iter != fields.end(); ++iter) {
+ fieldNames.push_back(iter->first);
+ fieldTypes[iter->first] = iter->second;
+ }
+
+ checkFields(fieldNames);
+
+ m_Fields = fieldNames;
+ m_FieldTypes = fieldTypes;
+ m_Defaults.clear();
+ m_RowBuffer.clear();
+
+}
+
+
+void CSVBuilder::checkValue(const QString &field, const QVariant &value)
+{
+ auto typeIter = m_FieldTypes.find(field);
+ if (typeIter == m_FieldTypes.end()) {
+ throw CSVException(QObject::tr("invalid field name \"%1\"").arg(field));
+ }
+
+ switch (typeIter->second) {
+ case TYPE_INTEGER: {
+ if (!value.canConvert<int>()) {
+ throw CSVException(QObject::tr("invalid type for \"%1\" (should be integer)").arg(field));
+ }
+ } break;
+ case TYPE_STRING: {
+ if (!value.canConvert<QString>()) {
+ throw CSVException(QObject::tr("invalid type for \"%1\" (should be string)").arg(field));
+ }
+ } break;
+ case TYPE_FLOAT: {
+ if (!value.canConvert<float>()) {
+ throw CSVException(QObject::tr("invalid type for \"%1\" (should be float)").arg(field));
+ }
+ } break;
+ }
+}
+
+
+void CSVBuilder::setDefault(const QString &field, const QVariant &value)
+{
+ checkValue(field, value);
+ m_Defaults[field] = value;
+}
+
+
+void CSVBuilder::writeHeader()
+{
+ if (m_Fields.size() == 0) {
+ throw CSVException(QObject::tr("no fields set up yet!"));
+ }
+
+ for (auto iter = m_Fields.begin(); iter != m_Fields.end(); ++iter) {
+ if (iter != m_Fields.begin()) {
+ m_Out << m_Separator;
+ }
+ m_Out << *iter;
+ }
+ m_Out << lineBreak();
+ m_Out.flush();
+}
+
+
+void CSVBuilder::setRowField(const QString &field, const QVariant &value)
+{
+ checkValue(field, value);
+ m_RowBuffer[field] = value;
+}
+
+
+void CSVBuilder::writeData(const std::map<QString, QVariant> &data, bool check)
+{
+ QString line;
+ QTextStream temp(&line);
+
+ for (auto iter = m_Fields.begin(); iter != m_Fields.end(); ++iter) {
+ if (iter != m_Fields.begin()) {
+ temp << separator();
+ }
+
+ QVariant val;
+
+ auto valIter = data.find(*iter);
+ if (valIter == data.end()) {
+ auto defaultIter = m_Defaults.find(*iter);
+ if (defaultIter == m_Defaults.end()) {
+ throw CSVException(QObject::tr("field not set \"%1\"").arg(*iter));
+ } else {
+ val = defaultIter->second;
+ }
+ } else {
+ val = valIter->second;
+ }
+
+ if (check) {
+ checkValue(*iter, val);
+ }
+
+ switch (m_FieldTypes[*iter]) {
+ case TYPE_INTEGER: {
+ quoteInsert(temp, val.toInt());
+ } break;
+ case TYPE_FLOAT: {
+ quoteInsert(temp, val.toFloat());
+ } break;
+ case TYPE_STRING: {
+ quoteInsert(temp, val.toString());
+ } break;
+ }
+ }
+ m_Out << line << lineBreak();
+ m_Out.flush();
+}
+
+
+void CSVBuilder::quoteInsert(QTextStream &stream, int value)
+{
+ switch (m_QuoteMode[TYPE_INTEGER]) {
+ case QUOTE_NEVER:
+ case QUOTE_ONDEMAND: {
+ stream << value;
+ } break;
+ case QUOTE_ALWAYS: {
+ stream << "\"" << value << "\"";
+ } break;
+ }
+}
+
+
+void CSVBuilder::quoteInsert(QTextStream &stream, float value)
+{
+ switch (m_QuoteMode[TYPE_FLOAT]) {
+ case QUOTE_NEVER:
+ case QUOTE_ONDEMAND: {
+ stream << value;
+ } break;
+ case QUOTE_ALWAYS: {
+ stream << "\"" << value << "\"";
+ } break;
+ }
+}
+
+
+void CSVBuilder::quoteInsert(QTextStream &stream, const QString &value)
+{
+ switch (m_QuoteMode[TYPE_STRING]) {
+ case QUOTE_NEVER: {
+ stream << value;
+ } break;
+ case QUOTE_ONDEMAND: {
+ if (value.contains("[,\r\n]")) {
+ stream << "\"" << value.mid(0).replace("\"", "\"\"") << "\"";
+ } else {
+ stream << value;
+ }
+ } break;
+ case QUOTE_ALWAYS: {
+ stream << "\"" << value.mid(0).replace("\"", "\"\"") << "\"";
+ } break;
+ }
+}
+
+
+void CSVBuilder::writeRow()
+{
+ writeData(m_RowBuffer, false); // data was tested on input
+ m_RowBuffer.clear();
+}
+
+
+void CSVBuilder::addRow(const std::map<QString, QVariant> &data)
+{
+ writeData(data, true);
+}
+
+
+void CSVBuilder::checkFields(const std::vector<QString> &fields)
+{
+ for (auto iter = fields.begin(); iter != fields.end(); ++iter) {
+ if (iter->contains(m_Separator) ||
+ iter->contains('\r') ||
+ iter->contains('\n') ||
+ iter->contains('"')) {
+ throw CSVException(QObject::tr("invalid character in field \"%1\"").arg(*iter));
+ }
+ if (iter->length() == 0) {
+ throw CSVException(QObject::tr("empty field name"));
+ }
+ }
+}
+
+
+/*
+
+if(cell.contains(KDefaultEscapeChar) || cell.contains(KDefaultNewLine)
+ || cell.contains(KDefaultDelimiter)) {
+ m_currentLine->append(cell.replace(KDefaultNewLine,
+ QString(KDefaultEscapeChar) + KDefaultEscapeChar)
+ .prepend(KDefaultEscapeChar)
+ .append(KDefaultEscapeChar));
+} else {
+m_currentLine->append(cell);
+}*/
+
+
+const char *CSVBuilder::lineBreak()
+{
+ switch (m_LineBreak) {
+ case BREAK_CR: return "\r";
+ case BREAK_CRLF: return "\r\n";
+ case BREAK_LF: return "\n";
+ default: return "\n"; // default shouldn't be necessary
+ }
+}
+
+const char CSVBuilder::separator()
+{
+ return m_Separator;
+}
diff --git a/src/csvbuilder.h b/src/csvbuilder.h
index f2e0e987..9d135db3 100644
--- a/src/csvbuilder.h
+++ b/src/csvbuilder.h
@@ -1,93 +1,93 @@
-#ifndef CSVBUILDER_H
-#define CSVBUILDER_H
-
-
-#include <vector>
-#include <QString>
-#include <QVariant>
-#include <QTextStream>
-
-
-class CSVException : public std::exception {
-
-public:
- CSVException(const QString &text)
- : std::exception(), m_Message(text.toLocal8Bit()) {}
-
- virtual const char* what() const throw()
- { return m_Message.constData(); }
-private:
- QByteArray m_Message;
-
-};
-
-
-class CSVBuilder
-{
-
-public:
-
- enum EFieldType {
- TYPE_INTEGER,
- TYPE_STRING,
- TYPE_FLOAT
- };
-
- enum EQuoteMode {
- QUOTE_NEVER,
- QUOTE_ONDEMAND,
- QUOTE_ALWAYS
- };
-
- enum ELineBreak {
- BREAK_LF,
- BREAK_CRLF,
- BREAK_CR
- };
-
-public:
-
- CSVBuilder(QIODevice *target);
- ~CSVBuilder();
-
- void setFieldSeparator(char sep);
- void setLineBreak(ELineBreak lineBreak);
- void setEscapeMode(EFieldType type, EQuoteMode mode);
- void setFields(const std::vector<std::pair<QString, EFieldType> > &fields);
- void setDefault(const QString &field, const QVariant &value);
-
- void writeHeader();
-
- void setRowField(const QString &field, const QVariant &value);
- void writeRow();
-
- void addRow(const std::map<QString, QVariant> &data);
-
-private:
-
- const char *lineBreak();
- const char separator();
-
- void fieldValid();
- void checkFields(const std::vector<QString> &fields);
- void checkValue(const QString &field, const QVariant &value);
- void writeData(const std::map<QString, QVariant> &data, bool check);
-
- void quoteInsert(QTextStream &stream, int value);
- void quoteInsert(QTextStream &stream, float value);
- void quoteInsert(QTextStream &stream, const QString &value);
-
-private:
-
- QTextStream m_Out;
- char m_Separator;
- ELineBreak m_LineBreak;
- std::map<EFieldType, EQuoteMode> m_QuoteMode;
- std::vector<QString> m_Fields;
- std::map<QString, EFieldType> m_FieldTypes;
- std::map<QString, QVariant> m_Defaults;
- std::map<QString, QVariant> m_RowBuffer;
-
-};
-
-#endif // CSVBUILDER_H
+#ifndef CSVBUILDER_H
+#define CSVBUILDER_H
+
+
+#include <vector>
+#include <QString>
+#include <QVariant>
+#include <QTextStream>
+
+
+class CSVException : public std::exception {
+
+public:
+ CSVException(const QString &text)
+ : std::exception(), m_Message(text.toLocal8Bit()) {}
+
+ virtual const char* what() const throw()
+ { return m_Message.constData(); }
+private:
+ QByteArray m_Message;
+
+};
+
+
+class CSVBuilder
+{
+
+public:
+
+ enum EFieldType {
+ TYPE_INTEGER,
+ TYPE_STRING,
+ TYPE_FLOAT
+ };
+
+ enum EQuoteMode {
+ QUOTE_NEVER,
+ QUOTE_ONDEMAND,
+ QUOTE_ALWAYS
+ };
+
+ enum ELineBreak {
+ BREAK_LF,
+ BREAK_CRLF,
+ BREAK_CR
+ };
+
+public:
+
+ CSVBuilder(QIODevice *target);
+ ~CSVBuilder();
+
+ void setFieldSeparator(char sep);
+ void setLineBreak(ELineBreak lineBreak);
+ void setEscapeMode(EFieldType type, EQuoteMode mode);
+ void setFields(const std::vector<std::pair<QString, EFieldType> > &fields);
+ void setDefault(const QString &field, const QVariant &value);
+
+ void writeHeader();
+
+ void setRowField(const QString &field, const QVariant &value);
+ void writeRow();
+
+ void addRow(const std::map<QString, QVariant> &data);
+
+private:
+
+ const char *lineBreak();
+ const char separator();
+
+ void fieldValid();
+ void checkFields(const std::vector<QString> &fields);
+ void checkValue(const QString &field, const QVariant &value);
+ void writeData(const std::map<QString, QVariant> &data, bool check);
+
+ void quoteInsert(QTextStream &stream, int value);
+ void quoteInsert(QTextStream &stream, float value);
+ void quoteInsert(QTextStream &stream, const QString &value);
+
+private:
+
+ QTextStream m_Out;
+ char m_Separator;
+ ELineBreak m_LineBreak;
+ std::map<EFieldType, EQuoteMode> m_QuoteMode;
+ std::vector<QString> m_Fields;
+ std::map<QString, EFieldType> m_FieldTypes;
+ std::map<QString, QVariant> m_Defaults;
+ std::map<QString, QVariant> m_RowBuffer;
+
+};
+
+#endif // CSVBUILDER_H
diff --git a/src/directoryrefresher.cpp b/src/directoryrefresher.cpp
index 90bfad3b..e9934cef 100644
--- a/src/directoryrefresher.cpp
+++ b/src/directoryrefresher.cpp
@@ -1,510 +1,510 @@
-/*
-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 "directoryrefresher.h"
-#include "shared/fileentry.h"
-#include "shared/filesorigin.h"
-
-#include "iplugingame.h"
-#include "utility.h"
-#include "report.h"
-#include "modinfo.h"
-#include "settings.h"
-#include "envfs.h"
-#include "modinfodialogfwd.h"
-#include "shared/util.h"
-
-#include <gameplugins.h>
-
-#include <QApplication>
-#include <QDir>
-#include <QString>
-
-#include <fstream>
-
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-DirectoryStats::DirectoryStats()
-{
- std::memset(this, 0, sizeof(DirectoryStats));
-}
-
-DirectoryStats& DirectoryStats::operator+=(const DirectoryStats& o)
-{
- dirTimes += o.dirTimes;
- fileTimes += o.fileTimes;
- sortTimes += o.sortTimes;
-
- subdirLookupTimes += o.subdirLookupTimes;
- addDirectoryTimes += o.addDirectoryTimes;
-
- filesLookupTimes += o.filesLookupTimes;
- addFileTimes += o.addFileTimes;
- addOriginToFileTimes += o.addOriginToFileTimes;
- addFileToOriginTimes += o.addFileToOriginTimes;
- addFileToRegisterTimes += o.addFileToRegisterTimes;
-
- originExists += o.originExists;
- originCreate += o.originCreate;
- originsNeededEnabled += o.originsNeededEnabled;
-
- subdirExists += o.subdirExists;
- subdirCreate += o.subdirCreate;
-
- fileExists += o.fileExists;
- fileCreate += o.fileCreate;
- filesInsertedInRegister += o.filesInsertedInRegister;
- filesAssignedInRegister += o.filesAssignedInRegister;
-
- return *this;
-}
-
-std::string DirectoryStats::csvHeader()
-{
- QStringList sl = {
- "dirTimes",
- "fileTimes",
- "sortTimes",
- "subdirLookupTimes",
- "addDirectoryTimes",
- "filesLookupTimes",
- "addFileTimes",
- "addOriginToFileTimes",
- "addFileToOriginTimes",
- "addFileToRegisterTimes",
- "originExists",
- "originCreate",
- "originsNeededEnabled",
- "subdirExists",
- "subdirCreate",
- "fileExists",
- "fileCreate",
- "filesInsertedInRegister",
- "filesAssignedInRegister"};
-
- return sl.join(",").toStdString();
-}
-
-std::string DirectoryStats::toCsv() const
-{
- QStringList oss;
-
- auto s = [](auto ns) {
- return ns.count() / 1000.0 / 1000.0 / 1000.0;
- };
-
- oss
- << QString::number(s(dirTimes))
- << QString::number(s(fileTimes))
- << QString::number(s(sortTimes))
-
- << QString::number(s(subdirLookupTimes))
- << QString::number(s(addDirectoryTimes))
-
- << QString::number(s(filesLookupTimes))
- << QString::number(s(addFileTimes))
- << QString::number(s(addOriginToFileTimes))
- << QString::number(s(addFileToOriginTimes))
- << QString::number(s(addFileToRegisterTimes))
-
- << QString::number(originExists)
- << QString::number(originCreate)
- << QString::number(originsNeededEnabled)
-
- << QString::number(subdirExists)
- << QString::number(subdirCreate)
-
- << QString::number(fileExists)
- << QString::number(fileCreate)
- << QString::number(filesInsertedInRegister)
- << QString::number(filesAssignedInRegister);
-
- return oss.join(",").toStdString();
-}
-
-void dumpStats(std::vector<DirectoryStats>& stats)
-{
- static int run = 0;
- static const std::string file("c:\\tmp\\data.csv");
-
- if (run == 0) {
- std::ofstream out(file, std::ios::out|std::ios::trunc);
- out << fmt::format("what,run,{}", DirectoryStats::csvHeader()) << "\n";
- }
-
- std::sort(stats.begin(), stats.end(), [](auto&& a, auto&& b){
- return (naturalCompare(QString::fromStdString(a.mod), QString::fromStdString(b.mod)) < 0);
- });
-
- std::ofstream out(file, std::ios::app);
-
- DirectoryStats total;
- for (const auto& s : stats) {
- out << fmt::format("{},{},{}", s.mod, run, s.toCsv()) << "\n";
- total += s;
- }
-
- out << fmt::format("total,{},{}", run, total.toCsv()) << "\n";
-
- ++run;
-}
-
-
-DirectoryRefresher::DirectoryRefresher(std::size_t threadCount)
- : m_threadCount(threadCount), m_lastFileCount(0)
-{
-}
-
-DirectoryEntry *DirectoryRefresher::stealDirectoryStructure()
-{
- QMutexLocker locker(&m_RefreshLock);
- return m_Root.release();
-}
-
-void DirectoryRefresher::setMods(const std::vector<std::tuple<QString, QString, int> > &mods
- , const std::set<QString> &managedArchives)
-{
- QMutexLocker locker(&m_RefreshLock);
-
- m_Mods.clear();
- for (auto mod = mods.begin(); mod != mods.end(); ++mod) {
- QString name = std::get<0>(*mod);
- ModInfo::Ptr info = ModInfo::getByIndex(ModInfo::getIndex(name));
- m_Mods.push_back(EntryInfo(name, std::get<1>(*mod), info->stealFiles(), info->archives(), std::get<2>(*mod)));
- }
-
- m_EnabledArchives = managedArchives;
-}
-
-void DirectoryRefresher::cleanStructure(DirectoryEntry *structure)
-{
- static const wchar_t *files[] = { L"meta.ini", L"readme.txt" };
- for (int i = 0; i < sizeof(files) / sizeof(wchar_t*); ++i) {
- structure->removeFile(files[i]);
- }
-
- static const wchar_t *dirs[] = { L"fomod" };
- for (int i = 0; i < sizeof(dirs) / sizeof(wchar_t*); ++i) {
- structure->removeDir(std::wstring(dirs[i]));
- }
-}
-
-void DirectoryRefresher::addModBSAToStructure(
- DirectoryEntry* root, const QString& modName,
- int priority, const QString& directory, const QStringList& archives)
-{
- const IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
-
- QStringList loadOrder;
-
- GamePlugins* gamePlugins = game->feature<GamePlugins>();
- if (gamePlugins) {
- loadOrder = gamePlugins->getLoadOrder();
- }
-
- std::vector<std::wstring> lo;
- for (auto&& s : loadOrder) {
- lo.push_back(s.toStdWString());
- }
-
- std::vector<std::wstring> archivesW;
- for (auto&& a : archives) {
- archivesW.push_back(a.toStdWString());
- }
-
- std::set<std::wstring> enabledArchives;
- for (auto&& a : m_EnabledArchives) {
- enabledArchives.insert(a.toStdWString());
- }
-
- DirectoryStats dummy;
-
- root->addFromAllBSAs(
- modName.toStdWString(),
- QDir::toNativeSeparators(directory).toStdWString(),
- priority,
- archivesW,
- enabledArchives,
- lo,
- dummy);
-}
-
-void DirectoryRefresher::stealModFilesIntoStructure(
- DirectoryEntry *directoryStructure, const QString &modName,
- int priority, const QString &directory, const QStringList &stealFiles)
-{
- std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
-
- // instead of adding all the files of the target directory, we just change the root of the specified
- // files to this mod
- DirectoryStats dummy;
- FilesOrigin &origin = directoryStructure->createOrigin(
- ToWString(modName), directoryW, priority, dummy);
-
- for (const QString &filename : stealFiles) {
- if (filename.isEmpty()) {
- log::warn("Trying to find file with no name");
- log::warn(" . modName: {}", modName);
- log::warn(" . directory: {}", directory);
- log::warn(" . priority: {}", priority);
- for (int i = 0; i < stealFiles.length(); ++i)
- log::warn(" . stealFiles[{}]: {}", i, stealFiles[i]);
- continue;
- }
- QFileInfo fileInfo(filename);
- FileEntryPtr file = directoryStructure->findFile(ToWString(fileInfo.fileName()));
- if (file.get() != nullptr) {
- if (file->getOrigin() == 0) {
- // replace data as the origin on this bsa
- file->removeOrigin(0);
- }
- origin.addFile(file->getIndex());
- file->addOrigin(origin.getID(), file->getFileTime(), L"", -1);
- } else {
- QString warnStr = fileInfo.absolutePath();
- if (warnStr.isEmpty())
- warnStr = filename;
- log::warn("file not found: {}", warnStr);
- }
- }
-}
-
-void DirectoryRefresher::addModFilesToStructure(
- DirectoryEntry *directoryStructure, const QString &modName,
- int priority, const QString &directory, const QStringList &stealFiles)
-{
- TimeThis tt("DirectoryRefresher::addModFilesToStructure()");
-
- std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
- DirectoryStats dummy;
-
- if (stealFiles.length() > 0) {
- stealModFilesIntoStructure(
- directoryStructure, modName, priority, directory, stealFiles);
- } else {
- directoryStructure->addFromOrigin(
- ToWString(modName), directoryW, priority, dummy);
- }
-}
-
-void DirectoryRefresher::addModToStructure(DirectoryEntry *directoryStructure
- , const QString &modName
- , int priority
- , const QString &directory
- , const QStringList &stealFiles
- , const QStringList &archives)
-{
- TimeThis tt("DirectoryRefresher::addModToStructure()");
-
- DirectoryStats dummy;
-
- if (stealFiles.length() > 0) {
- stealModFilesIntoStructure(
- directoryStructure, modName, priority, directory, stealFiles);
- } else {
- std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
- directoryStructure->addFromOrigin(
- ToWString(modName), directoryW, priority, dummy);
- }
-
- if (Settings::instance().archiveParsing()) {
- addModBSAToStructure(
- directoryStructure, modName, priority, directory, archives);
- }
-}
-
-
-struct ModThread
-{
- DirectoryRefreshProgress* progress = nullptr;
- DirectoryEntry* ds = nullptr;
- std::wstring modName;
- std::wstring path;
- int prio = -1;
- std::vector<std::wstring> archives;
- std::set<std::wstring> enabledArchives;
- DirectoryStats* stats = nullptr;
- env::DirectoryWalker walker;
-
- std::condition_variable cv;
- std::mutex mutex;
- bool ready = false;
-
- void wakeup()
- {
- {
- std::scoped_lock lock(mutex);
- ready = true;
- }
-
- cv.notify_one();
- }
-
- void run()
- {
- std::unique_lock lock(mutex);
- cv.wait(lock, [&]{ return ready; });
-
- SetThisThreadName(QString::fromStdWString(modName + L" refresher"));
- ds->addFromOrigin(walker, modName, path, prio, *stats);
-
- if (Settings::instance().archiveParsing()) {
- const IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
-
- QStringList loadOrder;
- GamePlugins* gamePlugins = game->feature<GamePlugins>();
- if (gamePlugins) {
- loadOrder = gamePlugins->getLoadOrder();
- }
-
- std::vector<std::wstring> lo;
- for (auto&& s : loadOrder) {
- lo.push_back(s.toStdWString());
- }
-
- ds->addFromAllBSAs(
- modName, path, prio, archives, enabledArchives, lo, *stats);
- }
-
- if (progress) {
- progress->addDone();
- }
-
- SetThisThreadName(QString::fromStdWString(L"idle refresher"));
- ready = false;
- }
-};
-
-env::ThreadPool<ModThread> g_threads;
-
-
-void DirectoryRefresher::updateProgress(const DirectoryRefreshProgress* p)
-{
- // careful: called from multiple threads
- emit progress(p);
-}
-
-void DirectoryRefresher::addMultipleModsFilesToStructure(
- MOShared::DirectoryEntry *directoryStructure,
- const std::vector<EntryInfo>& entries, DirectoryRefreshProgress* progress)
-{
- std::vector<DirectoryStats> stats(entries.size());
-
- if (progress) {
- progress->start(entries.size());
- }
-
- log::debug("refresher: using {} threads", m_threadCount);
- g_threads.setMax(m_threadCount);
-
- for (std::size_t i=0; i<entries.size(); ++i) {
- const auto& e = entries[i];
- const int prio = e.priority + 1;
-
- if constexpr (DirectoryStats::EnableInstrumentation) {
- stats[i].mod = entries[i].modName.toStdString();
- }
-
- try
- {
- if (e.stealFiles.length() > 0) {
- stealModFilesIntoStructure(
- directoryStructure, e.modName, prio, e.absolutePath, e.stealFiles);
-
- if (progress) {
- progress->addDone();
- }
- } else {
- auto& mt = g_threads.request();
-
- mt.progress = progress;
- mt.ds = directoryStructure;
- mt.modName = e.modName.toStdWString();
- mt.path = QDir::toNativeSeparators(e.absolutePath).toStdWString();
- mt.prio = prio;
-
- mt.archives.clear();
- for (auto&& a : e.archives) {
- mt.archives.push_back(a.toStdWString());
- }
-
- mt.enabledArchives.clear();
- for (auto&& a : m_EnabledArchives) {
- mt.enabledArchives.insert(a.toStdWString());
- }
-
- mt.stats = &stats[i];
-
- mt.wakeup();
- }
- } catch (const std::exception& ex) {
- emit error(tr("failed to read mod (%1): %2").arg(e.modName, ex.what()));
- }
- }
-
- g_threads.waitForAll();
-
- if constexpr (DirectoryStats::EnableInstrumentation) {
- dumpStats(stats);
- }
-}
-
-void DirectoryRefresher::refresh()
-{
- SetThisThreadName("DirectoryRefresher");
- TimeThis tt("DirectoryRefresher::refresh()");
- auto* p = new DirectoryRefreshProgress(this);
-
- {
- QMutexLocker locker(&m_RefreshLock);
-
- m_Root.reset(new DirectoryEntry(L"data", nullptr, 0));
-
- IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
-
- std::wstring dataDirectory =
- QDir::toNativeSeparators(game->dataDirectory().absolutePath()).toStdWString();
-
- {
- DirectoryStats dummy;
- m_Root->addFromOrigin(L"data", dataDirectory, 0, dummy);
- }
-
- std::sort(m_Mods.begin(), m_Mods.end(), [](auto lhs, auto rhs) {
- return lhs.priority < rhs.priority;
- });
-
- addMultipleModsFilesToStructure(m_Root.get(), m_Mods, p);
-
- m_Root->getFileRegister()->sortOrigins();
-
- cleanStructure(m_Root.get());
-
- m_lastFileCount = m_Root->getFileRegister()->highestCount();
- log::debug("refresher saw {} files", m_lastFileCount);
- }
-
- p->finish();
-
- emit progress(p);
- emit refreshed();
-}
+/*
+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 "directoryrefresher.h"
+#include "shared/fileentry.h"
+#include "shared/filesorigin.h"
+
+#include "iplugingame.h"
+#include "utility.h"
+#include "report.h"
+#include "modinfo.h"
+#include "settings.h"
+#include "envfs.h"
+#include "modinfodialogfwd.h"
+#include "shared/util.h"
+
+#include <gameplugins.h>
+
+#include <QApplication>
+#include <QDir>
+#include <QString>
+
+#include <fstream>
+
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+DirectoryStats::DirectoryStats()
+{
+ std::memset(this, 0, sizeof(DirectoryStats));
+}
+
+DirectoryStats& DirectoryStats::operator+=(const DirectoryStats& o)
+{
+ dirTimes += o.dirTimes;
+ fileTimes += o.fileTimes;
+ sortTimes += o.sortTimes;
+
+ subdirLookupTimes += o.subdirLookupTimes;
+ addDirectoryTimes += o.addDirectoryTimes;
+
+ filesLookupTimes += o.filesLookupTimes;
+ addFileTimes += o.addFileTimes;
+ addOriginToFileTimes += o.addOriginToFileTimes;
+ addFileToOriginTimes += o.addFileToOriginTimes;
+ addFileToRegisterTimes += o.addFileToRegisterTimes;
+
+ originExists += o.originExists;
+ originCreate += o.originCreate;
+ originsNeededEnabled += o.originsNeededEnabled;
+
+ subdirExists += o.subdirExists;
+ subdirCreate += o.subdirCreate;
+
+ fileExists += o.fileExists;
+ fileCreate += o.fileCreate;
+ filesInsertedInRegister += o.filesInsertedInRegister;
+ filesAssignedInRegister += o.filesAssignedInRegister;
+
+ return *this;
+}
+
+std::string DirectoryStats::csvHeader()
+{
+ QStringList sl = {
+ "dirTimes",
+ "fileTimes",
+ "sortTimes",
+ "subdirLookupTimes",
+ "addDirectoryTimes",
+ "filesLookupTimes",
+ "addFileTimes",
+ "addOriginToFileTimes",
+ "addFileToOriginTimes",
+ "addFileToRegisterTimes",
+ "originExists",
+ "originCreate",
+ "originsNeededEnabled",
+ "subdirExists",
+ "subdirCreate",
+ "fileExists",
+ "fileCreate",
+ "filesInsertedInRegister",
+ "filesAssignedInRegister"};
+
+ return sl.join(",").toStdString();
+}
+
+std::string DirectoryStats::toCsv() const
+{
+ QStringList oss;
+
+ auto s = [](auto ns) {
+ return ns.count() / 1000.0 / 1000.0 / 1000.0;
+ };
+
+ oss
+ << QString::number(s(dirTimes))
+ << QString::number(s(fileTimes))
+ << QString::number(s(sortTimes))
+
+ << QString::number(s(subdirLookupTimes))
+ << QString::number(s(addDirectoryTimes))
+
+ << QString::number(s(filesLookupTimes))
+ << QString::number(s(addFileTimes))
+ << QString::number(s(addOriginToFileTimes))
+ << QString::number(s(addFileToOriginTimes))
+ << QString::number(s(addFileToRegisterTimes))
+
+ << QString::number(originExists)
+ << QString::number(originCreate)
+ << QString::number(originsNeededEnabled)
+
+ << QString::number(subdirExists)
+ << QString::number(subdirCreate)
+
+ << QString::number(fileExists)
+ << QString::number(fileCreate)
+ << QString::number(filesInsertedInRegister)
+ << QString::number(filesAssignedInRegister);
+
+ return oss.join(",").toStdString();
+}
+
+void dumpStats(std::vector<DirectoryStats>& stats)
+{
+ static int run = 0;
+ static const std::string file("c:\\tmp\\data.csv");
+
+ if (run == 0) {
+ std::ofstream out(file, std::ios::out|std::ios::trunc);
+ out << fmt::format("what,run,{}", DirectoryStats::csvHeader()) << "\n";
+ }
+
+ std::sort(stats.begin(), stats.end(), [](auto&& a, auto&& b){
+ return (naturalCompare(QString::fromStdString(a.mod), QString::fromStdString(b.mod)) < 0);
+ });
+
+ std::ofstream out(file, std::ios::app);
+
+ DirectoryStats total;
+ for (const auto& s : stats) {
+ out << fmt::format("{},{},{}", s.mod, run, s.toCsv()) << "\n";
+ total += s;
+ }
+
+ out << fmt::format("total,{},{}", run, total.toCsv()) << "\n";
+
+ ++run;
+}
+
+
+DirectoryRefresher::DirectoryRefresher(std::size_t threadCount)
+ : m_threadCount(threadCount), m_lastFileCount(0)
+{
+}
+
+DirectoryEntry *DirectoryRefresher::stealDirectoryStructure()
+{
+ QMutexLocker locker(&m_RefreshLock);
+ return m_Root.release();
+}
+
+void DirectoryRefresher::setMods(const std::vector<std::tuple<QString, QString, int> > &mods
+ , const std::set<QString> &managedArchives)
+{
+ QMutexLocker locker(&m_RefreshLock);
+
+ m_Mods.clear();
+ for (auto mod = mods.begin(); mod != mods.end(); ++mod) {
+ QString name = std::get<0>(*mod);
+ ModInfo::Ptr info = ModInfo::getByIndex(ModInfo::getIndex(name));
+ m_Mods.push_back(EntryInfo(name, std::get<1>(*mod), info->stealFiles(), info->archives(), std::get<2>(*mod)));
+ }
+
+ m_EnabledArchives = managedArchives;
+}
+
+void DirectoryRefresher::cleanStructure(DirectoryEntry *structure)
+{
+ static const wchar_t *files[] = { L"meta.ini", L"readme.txt" };
+ for (int i = 0; i < sizeof(files) / sizeof(wchar_t*); ++i) {
+ structure->removeFile(files[i]);
+ }
+
+ static const wchar_t *dirs[] = { L"fomod" };
+ for (int i = 0; i < sizeof(dirs) / sizeof(wchar_t*); ++i) {
+ structure->removeDir(std::wstring(dirs[i]));
+ }
+}
+
+void DirectoryRefresher::addModBSAToStructure(
+ DirectoryEntry* root, const QString& modName,
+ int priority, const QString& directory, const QStringList& archives)
+{
+ const IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
+
+ QStringList loadOrder;
+
+ GamePlugins* gamePlugins = game->feature<GamePlugins>();
+ if (gamePlugins) {
+ loadOrder = gamePlugins->getLoadOrder();
+ }
+
+ std::vector<std::wstring> lo;
+ for (auto&& s : loadOrder) {
+ lo.push_back(s.toStdWString());
+ }
+
+ std::vector<std::wstring> archivesW;
+ for (auto&& a : archives) {
+ archivesW.push_back(a.toStdWString());
+ }
+
+ std::set<std::wstring> enabledArchives;
+ for (auto&& a : m_EnabledArchives) {
+ enabledArchives.insert(a.toStdWString());
+ }
+
+ DirectoryStats dummy;
+
+ root->addFromAllBSAs(
+ modName.toStdWString(),
+ QDir::toNativeSeparators(directory).toStdWString(),
+ priority,
+ archivesW,
+ enabledArchives,
+ lo,
+ dummy);
+}
+
+void DirectoryRefresher::stealModFilesIntoStructure(
+ DirectoryEntry *directoryStructure, const QString &modName,
+ int priority, const QString &directory, const QStringList &stealFiles)
+{
+ std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
+
+ // instead of adding all the files of the target directory, we just change the root of the specified
+ // files to this mod
+ DirectoryStats dummy;
+ FilesOrigin &origin = directoryStructure->createOrigin(
+ ToWString(modName), directoryW, priority, dummy);
+
+ for (const QString &filename : stealFiles) {
+ if (filename.isEmpty()) {
+ log::warn("Trying to find file with no name");
+ log::warn(" . modName: {}", modName);
+ log::warn(" . directory: {}", directory);
+ log::warn(" . priority: {}", priority);
+ for (int i = 0; i < stealFiles.length(); ++i)
+ log::warn(" . stealFiles[{}]: {}", i, stealFiles[i]);
+ continue;
+ }
+ QFileInfo fileInfo(filename);
+ FileEntryPtr file = directoryStructure->findFile(ToWString(fileInfo.fileName()));
+ if (file.get() != nullptr) {
+ if (file->getOrigin() == 0) {
+ // replace data as the origin on this bsa
+ file->removeOrigin(0);
+ }
+ origin.addFile(file->getIndex());
+ file->addOrigin(origin.getID(), file->getFileTime(), L"", -1);
+ } else {
+ QString warnStr = fileInfo.absolutePath();
+ if (warnStr.isEmpty())
+ warnStr = filename;
+ log::warn("file not found: {}", warnStr);
+ }
+ }
+}
+
+void DirectoryRefresher::addModFilesToStructure(
+ DirectoryEntry *directoryStructure, const QString &modName,
+ int priority, const QString &directory, const QStringList &stealFiles)
+{
+ TimeThis tt("DirectoryRefresher::addModFilesToStructure()");
+
+ std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
+ DirectoryStats dummy;
+
+ if (stealFiles.length() > 0) {
+ stealModFilesIntoStructure(
+ directoryStructure, modName, priority, directory, stealFiles);
+ } else {
+ directoryStructure->addFromOrigin(
+ ToWString(modName), directoryW, priority, dummy);
+ }
+}
+
+void DirectoryRefresher::addModToStructure(DirectoryEntry *directoryStructure
+ , const QString &modName
+ , int priority
+ , const QString &directory
+ , const QStringList &stealFiles
+ , const QStringList &archives)
+{
+ TimeThis tt("DirectoryRefresher::addModToStructure()");
+
+ DirectoryStats dummy;
+
+ if (stealFiles.length() > 0) {
+ stealModFilesIntoStructure(
+ directoryStructure, modName, priority, directory, stealFiles);
+ } else {
+ std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory));
+ directoryStructure->addFromOrigin(
+ ToWString(modName), directoryW, priority, dummy);
+ }
+
+ if (Settings::instance().archiveParsing()) {
+ addModBSAToStructure(
+ directoryStructure, modName, priority, directory, archives);
+ }
+}
+
+
+struct ModThread
+{
+ DirectoryRefreshProgress* progress = nullptr;
+ DirectoryEntry* ds = nullptr;
+ std::wstring modName;
+ std::wstring path;
+ int prio = -1;
+ std::vector<std::wstring> archives;
+ std::set<std::wstring> enabledArchives;
+ DirectoryStats* stats = nullptr;
+ env::DirectoryWalker walker;
+
+ std::condition_variable cv;
+ std::mutex mutex;
+ bool ready = false;
+
+ void wakeup()
+ {
+ {
+ std::scoped_lock lock(mutex);
+ ready = true;
+ }
+
+ cv.notify_one();
+ }
+
+ void run()
+ {
+ std::unique_lock lock(mutex);
+ cv.wait(lock, [&]{ return ready; });
+
+ SetThisThreadName(QString::fromStdWString(modName + L" refresher"));
+ ds->addFromOrigin(walker, modName, path, prio, *stats);
+
+ if (Settings::instance().archiveParsing()) {
+ const IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
+
+ QStringList loadOrder;
+ GamePlugins* gamePlugins = game->feature<GamePlugins>();
+ if (gamePlugins) {
+ loadOrder = gamePlugins->getLoadOrder();
+ }
+
+ std::vector<std::wstring> lo;
+ for (auto&& s : loadOrder) {
+ lo.push_back(s.toStdWString());
+ }
+
+ ds->addFromAllBSAs(
+ modName, path, prio, archives, enabledArchives, lo, *stats);
+ }
+
+ if (progress) {
+ progress->addDone();
+ }
+
+ SetThisThreadName(QString::fromStdWString(L"idle refresher"));
+ ready = false;
+ }
+};
+
+env::ThreadPool<ModThread> g_threads;
+
+
+void DirectoryRefresher::updateProgress(const DirectoryRefreshProgress* p)
+{
+ // careful: called from multiple threads
+ emit progress(p);
+}
+
+void DirectoryRefresher::addMultipleModsFilesToStructure(
+ MOShared::DirectoryEntry *directoryStructure,
+ const std::vector<EntryInfo>& entries, DirectoryRefreshProgress* progress)
+{
+ std::vector<DirectoryStats> stats(entries.size());
+
+ if (progress) {
+ progress->start(entries.size());
+ }
+
+ log::debug("refresher: using {} threads", m_threadCount);
+ g_threads.setMax(m_threadCount);
+
+ for (std::size_t i=0; i<entries.size(); ++i) {
+ const auto& e = entries[i];
+ const int prio = e.priority + 1;
+
+ if constexpr (DirectoryStats::EnableInstrumentation) {
+ stats[i].mod = entries[i].modName.toStdString();
+ }
+
+ try
+ {
+ if (e.stealFiles.length() > 0) {
+ stealModFilesIntoStructure(
+ directoryStructure, e.modName, prio, e.absolutePath, e.stealFiles);
+
+ if (progress) {
+ progress->addDone();
+ }
+ } else {
+ auto& mt = g_threads.request();
+
+ mt.progress = progress;
+ mt.ds = directoryStructure;
+ mt.modName = e.modName.toStdWString();
+ mt.path = QDir::toNativeSeparators(e.absolutePath).toStdWString();
+ mt.prio = prio;
+
+ mt.archives.clear();
+ for (auto&& a : e.archives) {
+ mt.archives.push_back(a.toStdWString());
+ }
+
+ mt.enabledArchives.clear();
+ for (auto&& a : m_EnabledArchives) {
+ mt.enabledArchives.insert(a.toStdWString());
+ }
+
+ mt.stats = &stats[i];
+
+ mt.wakeup();
+ }
+ } catch (const std::exception& ex) {
+ emit error(tr("failed to read mod (%1): %2").arg(e.modName, ex.what()));
+ }
+ }
+
+ g_threads.waitForAll();
+
+ if constexpr (DirectoryStats::EnableInstrumentation) {
+ dumpStats(stats);
+ }
+}
+
+void DirectoryRefresher::refresh()
+{
+ SetThisThreadName("DirectoryRefresher");
+ TimeThis tt("DirectoryRefresher::refresh()");
+ auto* p = new DirectoryRefreshProgress(this);
+
+ {
+ QMutexLocker locker(&m_RefreshLock);
+
+ m_Root.reset(new DirectoryEntry(L"data", nullptr, 0));
+
+ IPluginGame *game = qApp->property("managed_game").value<IPluginGame*>();
+
+ std::wstring dataDirectory =
+ QDir::toNativeSeparators(game->dataDirectory().absolutePath()).toStdWString();
+
+ {
+ DirectoryStats dummy;
+ m_Root->addFromOrigin(L"data", dataDirectory, 0, dummy);
+ }
+
+ std::sort(m_Mods.begin(), m_Mods.end(), [](auto lhs, auto rhs) {
+ return lhs.priority < rhs.priority;
+ });
+
+ addMultipleModsFilesToStructure(m_Root.get(), m_Mods, p);
+
+ m_Root->getFileRegister()->sortOrigins();
+
+ cleanStructure(m_Root.get());
+
+ m_lastFileCount = m_Root->getFileRegister()->highestCount();
+ log::debug("refresher saw {} files", m_lastFileCount);
+ }
+
+ p->finish();
+
+ emit progress(p);
+ emit refreshed();
+}
diff --git a/src/directoryrefresher.h b/src/directoryrefresher.h
index 56394d94..cb0434ca 100644
--- a/src/directoryrefresher.h
+++ b/src/directoryrefresher.h
@@ -1,211 +1,211 @@
-/*
-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 DIRECTORYREFRESHER_H
-#define DIRECTORYREFRESHER_H
-
-#include "shared/directoryentry.h"
-#include "shared/fileregisterfwd.h"
-#include "profile.h"
-#include <QObject>
-#include <QMutex>
-#include <QStringList>
-#include <vector>
-#include <set>
-#include <tuple>
-
-/**
- * @brief used to asynchronously generate the virtual view of the combined data directory
- **/
-class DirectoryRefresher : public QObject
-{
- Q_OBJECT;
-
-public:
- struct EntryInfo
- {
- EntryInfo(const QString &modName, const QString &absolutePath,
- const QStringList &stealFiles, const QStringList &archives, int priority)
- : modName(modName), absolutePath(absolutePath), stealFiles(stealFiles)
- , archives(archives), priority(priority)
- {
- }
-
- QString modName;
- QString absolutePath;
- QStringList stealFiles;
- QStringList archives;
- int priority;
- };
-
-
- DirectoryRefresher(std::size_t threadCount);
-
- /**
- * @brief retrieve the updated directory structure
- *
- * returns a pointer to the updated directory structure. DirectoryRefresher
- * deletes its own pointer and the caller takes custody of the pointer
- *
- * @return updated directory structure
- **/
- MOShared::DirectoryEntry* stealDirectoryStructure();
-
- /**
- * @brief sets up the mods to be included in the directory structure
- *
- * @param mods list of the mods to include
- **/
- void setMods(const std::vector<std::tuple<QString, QString, int> > &mods, const std::set<QString> &managedArchives);
-
- /**
- * @brief sets up the directory where mods are stored
- * @param modDirectory the mod directory
- * @note this function could be obsoleted easily by storing absolute paths in the parameter to setMods. This is legacy
- */
- //void setModDirectory(const QString &modDirectory);
-
- /**
- * @brief remove files from the directory structure that are known to be irrelevant to the game
- * @param the structure to clean
- */
- static void cleanStructure(MOShared::DirectoryEntry *structure);
-
- /**
- * @brief add files for a mod to the directory structure, including bsas
- * @param directoryStructure
- * @param modName
- * @param priority
- * @param directory
- * @param stealFiles
- * @param archives
- */
- void addModToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory, const QStringList &stealFiles, const QStringList &archives);
-
- /**
- * @brief add only the bsas of a mod to the directory structure
- * @param directoryStructure
- * @param modName
- * @param priority
- * @param directory
- * @param archives
- */
- void addModBSAToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory, const QStringList &archives);
-
- /**
- * @brief add only regular files ofr a mod to the directory structure
- * @param directoryStructure
- * @param modName
- * @param priority
- * @param directory
- * @param stealFiles
- */
- void addModFilesToStructure(
- MOShared::DirectoryEntry *directoryStructure, const QString &modName,
- int priority, const QString &directory, const QStringList &stealFiles);
-
- void addMultipleModsFilesToStructure(
- MOShared::DirectoryEntry *directoryStructure,
- const std::vector<EntryInfo>& entries,
- DirectoryRefreshProgress* progress=nullptr);
-
- void updateProgress(const DirectoryRefreshProgress* p);
-
-public slots:
-
- /**
- * @brief generate a directory structure from the mods set earlier
- **/
- void refresh();
-
-signals:
-
- void progress(const DirectoryRefreshProgress* p);
- void error(const QString &error);
- void refreshed();
-
-private:
- std::vector<EntryInfo> m_Mods;
- std::set<QString> m_EnabledArchives;
- std::unique_ptr<MOShared::DirectoryEntry> m_Root;
- QMutex m_RefreshLock;
- std::size_t m_threadCount;
- std::size_t m_lastFileCount;
-
- void stealModFilesIntoStructure(
- MOShared::DirectoryEntry *directoryStructure, const QString &modName,
- int priority, const QString &directory, const QStringList &stealFiles);
-};
-
-
-class DirectoryRefreshProgress : public QObject
-{
- Q_OBJECT
-
-public:
- DirectoryRefreshProgress(DirectoryRefresher* r) :
- QObject(r), m_refresher(r), m_modCount(0), m_modDone(0), m_finished(false)
- {
- }
-
- void start(std::size_t modCount)
- {
- m_modCount = modCount;
- m_modDone = 0;
- m_finished = false;
- }
-
-
- bool finished() const
- {
- return m_finished;
- }
-
- int percentDone() const
- {
- int percent = 100;
-
- if (m_modCount > 0) {
- const double d = static_cast<double>(m_modDone) / m_modCount;
- percent = static_cast<int>(d * 100);
- }
-
- return percent;
- }
-
-
- void finish()
- {
- m_finished = true;
- }
-
- void addDone()
- {
- ++m_modDone;
- m_refresher->updateProgress(this);
- }
-
-private:
- DirectoryRefresher* m_refresher;
- std::size_t m_modCount;
- std::atomic<std::size_t> m_modDone;
- bool m_finished;
-};
-
-#endif // DIRECTORYREFRESHER_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 DIRECTORYREFRESHER_H
+#define DIRECTORYREFRESHER_H
+
+#include "shared/directoryentry.h"
+#include "shared/fileregisterfwd.h"
+#include "profile.h"
+#include <QObject>
+#include <QMutex>
+#include <QStringList>
+#include <vector>
+#include <set>
+#include <tuple>
+
+/**
+ * @brief used to asynchronously generate the virtual view of the combined data directory
+ **/
+class DirectoryRefresher : public QObject
+{
+ Q_OBJECT;
+
+public:
+ struct EntryInfo
+ {
+ EntryInfo(const QString &modName, const QString &absolutePath,
+ const QStringList &stealFiles, const QStringList &archives, int priority)
+ : modName(modName), absolutePath(absolutePath), stealFiles(stealFiles)
+ , archives(archives), priority(priority)
+ {
+ }
+
+ QString modName;
+ QString absolutePath;
+ QStringList stealFiles;
+ QStringList archives;
+ int priority;
+ };
+
+
+ DirectoryRefresher(std::size_t threadCount);
+
+ /**
+ * @brief retrieve the updated directory structure
+ *
+ * returns a pointer to the updated directory structure. DirectoryRefresher
+ * deletes its own pointer and the caller takes custody of the pointer
+ *
+ * @return updated directory structure
+ **/
+ MOShared::DirectoryEntry* stealDirectoryStructure();
+
+ /**
+ * @brief sets up the mods to be included in the directory structure
+ *
+ * @param mods list of the mods to include
+ **/
+ void setMods(const std::vector<std::tuple<QString, QString, int> > &mods, const std::set<QString> &managedArchives);
+
+ /**
+ * @brief sets up the directory where mods are stored
+ * @param modDirectory the mod directory
+ * @note this function could be obsoleted easily by storing absolute paths in the parameter to setMods. This is legacy
+ */
+ //void setModDirectory(const QString &modDirectory);
+
+ /**
+ * @brief remove files from the directory structure that are known to be irrelevant to the game
+ * @param the structure to clean
+ */
+ static void cleanStructure(MOShared::DirectoryEntry *structure);
+
+ /**
+ * @brief add files for a mod to the directory structure, including bsas
+ * @param directoryStructure
+ * @param modName
+ * @param priority
+ * @param directory
+ * @param stealFiles
+ * @param archives
+ */
+ void addModToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory, const QStringList &stealFiles, const QStringList &archives);
+
+ /**
+ * @brief add only the bsas of a mod to the directory structure
+ * @param directoryStructure
+ * @param modName
+ * @param priority
+ * @param directory
+ * @param archives
+ */
+ void addModBSAToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory, const QStringList &archives);
+
+ /**
+ * @brief add only regular files ofr a mod to the directory structure
+ * @param directoryStructure
+ * @param modName
+ * @param priority
+ * @param directory
+ * @param stealFiles
+ */
+ void addModFilesToStructure(
+ MOShared::DirectoryEntry *directoryStructure, const QString &modName,
+ int priority, const QString &directory, const QStringList &stealFiles);
+
+ void addMultipleModsFilesToStructure(
+ MOShared::DirectoryEntry *directoryStructure,
+ const std::vector<EntryInfo>& entries,
+ DirectoryRefreshProgress* progress=nullptr);
+
+ void updateProgress(const DirectoryRefreshProgress* p);
+
+public slots:
+
+ /**
+ * @brief generate a directory structure from the mods set earlier
+ **/
+ void refresh();
+
+signals:
+
+ void progress(const DirectoryRefreshProgress* p);
+ void error(const QString &error);
+ void refreshed();
+
+private:
+ std::vector<EntryInfo> m_Mods;
+ std::set<QString> m_EnabledArchives;
+ std::unique_ptr<MOShared::DirectoryEntry> m_Root;
+ QMutex m_RefreshLock;
+ std::size_t m_threadCount;
+ std::size_t m_lastFileCount;
+
+ void stealModFilesIntoStructure(
+ MOShared::DirectoryEntry *directoryStructure, const QString &modName,
+ int priority, const QString &directory, const QStringList &stealFiles);
+};
+
+
+class DirectoryRefreshProgress : public QObject
+{
+ Q_OBJECT
+
+public:
+ DirectoryRefreshProgress(DirectoryRefresher* r) :
+ QObject(r), m_refresher(r), m_modCount(0), m_modDone(0), m_finished(false)
+ {
+ }
+
+ void start(std::size_t modCount)
+ {
+ m_modCount = modCount;
+ m_modDone = 0;
+ m_finished = false;
+ }
+
+
+ bool finished() const
+ {
+ return m_finished;
+ }
+
+ int percentDone() const
+ {
+ int percent = 100;
+
+ if (m_modCount > 0) {
+ const double d = static_cast<double>(m_modDone) / m_modCount;
+ percent = static_cast<int>(d * 100);
+ }
+
+ return percent;
+ }
+
+
+ void finish()
+ {
+ m_finished = true;
+ }
+
+ void addDone()
+ {
+ ++m_modDone;
+ m_refresher->updateProgress(this);
+ }
+
+private:
+ DirectoryRefresher* m_refresher;
+ std::size_t m_modCount;
+ std::atomic<std::size_t> m_modDone;
+ bool m_finished;
+};
+
+#endif // DIRECTORYREFRESHER_H
diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp
index 387fcad1..e16c1cda 100644
--- a/src/downloadlist.cpp
+++ b/src/downloadlist.cpp
@@ -1,291 +1,291 @@
-/*
-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 "organizercore.h"
-#include "downloadlist.h"
-#include "downloadmanager.h"
-#include "modlistdropinfo.h"
-#include "settings.h"
-#include <utility.h>
-#include <log.h>
-#include <QEvent>
-#include <QColor>
-#include <QIcon>
-#include <QSortFilterProxyModel>
-
-using namespace MOBase;
-
-DownloadList::DownloadList(OrganizerCore& core, QObject *parent)
- : QAbstractTableModel(parent), m_manager(*core.downloadManager()), m_settings(core.settings())
-{
- connect(&m_manager, SIGNAL(update(int)), this, SLOT(update(int)));
- connect(&m_manager, SIGNAL(aboutToUpdate()), this, SLOT(aboutToUpdate()));
-}
-
-
-int DownloadList::rowCount(const QModelIndex& parent) const
-{
- if (!parent.isValid()) {
- // root item
- return m_manager.numTotalDownloads() + m_manager.numPendingDownloads();
- } else {
- return 0;
- }
-}
-
-
-int DownloadList::columnCount(const QModelIndex&) const
-{
- return COL_COUNT;
-}
-
-
-QModelIndex DownloadList::index(int row, int column, const QModelIndex&) const
-{
- return createIndex(row, column, row);
-}
-
-
-QModelIndex DownloadList::parent(const QModelIndex&) const
-{
- return QModelIndex();
-}
-
-
-QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int role) const
-{
- if ((role == Qt::DisplayRole) &&
- (orientation == Qt::Horizontal)) {
- switch (section) {
- case COL_NAME: return tr("Name");
- case COL_MODNAME: return tr("Mod name");
- case COL_VERSION: return tr("Version");
- case COL_ID: return tr("Nexus ID");
- case COL_SIZE: return tr("Size");
- case COL_STATUS: return tr("Status");
- case COL_FILETIME: return tr("Filetime");
- case COL_SOURCEGAME: return tr("Source Game");
- default: return QVariant();
- }
- } else {
- return QAbstractItemModel::headerData(section, orientation, role);
- }
-}
-
-Qt::ItemFlags DownloadList::flags(const QModelIndex& idx) const
-{
- return QAbstractTableModel::flags(idx) | Qt::ItemIsDragEnabled;
-}
-
-QMimeData* DownloadList::mimeData(const QModelIndexList& indexes) const
-{
- QMimeData* result = QAbstractItemModel::mimeData(indexes);
- result->setData("text/plain", ModListDropInfo::DownloadText);
- return result;
-}
-
-QVariant DownloadList::data(const QModelIndex &index, int role) const
-{
- bool pendingDownload = index.row() >= m_manager.numTotalDownloads();
- if (role == Qt::DisplayRole) {
- if (pendingDownload) {
- std::tuple<QString, int, int> nexusids = m_manager.getPendingDownload(index.row() - m_manager.numTotalDownloads());
- switch (index.column()) {
- case COL_NAME: return tr("< game %1 mod %2 file %3 >").arg(std::get<0>(nexusids)).arg(std::get<1>(nexusids)).arg(std::get<2>(nexusids));
- case COL_SIZE: return tr("Unknown");
- case COL_STATUS: return tr("Pending");
- }
- } else {
- switch (index.column()) {
- case COL_NAME: return m_settings.interface().metaDownloads() ? m_manager.getDisplayName(index.row()) : m_manager.getFileName(index.row());
- case COL_MODNAME: {
- if (m_manager.isInfoIncomplete(index.row())) {
- return {};
- } else {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(index.row());
- return info->modName;
- }
- }
- case COL_VERSION: {
- if (m_manager.isInfoIncomplete(index.row())) {
- return {};
- } else {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(index.row());
- return info->version.canonicalString();
- }
- }
- case COL_ID: {
- if (m_manager.isInfoIncomplete(index.row())) {
- return {};
- } else {
- return QString("%1").arg(m_manager.getModID(index.row()));
- }
- }
- case COL_SOURCEGAME: {
- if (m_manager.isInfoIncomplete(index.row())) {
- return {};
- } else {
- return QString("%1").arg(m_manager.getDisplayGameName(index.row()));
- }
- }
- case COL_SIZE: return MOBase::localizedByteSize(m_manager.getFileSize(index.row()));
- case COL_FILETIME: return m_manager.getFileTime(index.row());
- case COL_STATUS:
- switch (m_manager.getState(index.row())) {
- // STATE_DOWNLOADING handled by DownloadProgressDelegate
- case DownloadManager::STATE_STARTED: return tr("Started");
- case DownloadManager::STATE_CANCELING: return tr("Canceling");
- case DownloadManager::STATE_PAUSING: return tr("Pausing");
- case DownloadManager::STATE_CANCELED: return tr("Canceled");
- case DownloadManager::STATE_PAUSED: return tr("Paused");
- case DownloadManager::STATE_ERROR: return tr("Error");
- case DownloadManager::STATE_FETCHINGMODINFO: return tr("Fetching Info");
- case DownloadManager::STATE_FETCHINGFILEINFO: return tr("Fetching Info");
- case DownloadManager::STATE_FETCHINGMODINFO_MD5: return tr("Fetching Info");
- case DownloadManager::STATE_READY: return tr("Downloaded");
- case DownloadManager::STATE_INSTALLED: return tr("Installed");
- case DownloadManager::STATE_UNINSTALLED: return tr("Uninstalled");
- }
- }
- }
- } else if (role == Qt::ForegroundRole && index.column() == COL_STATUS) {
- if (pendingDownload) {
- return QColor(Qt::darkBlue);
- } else {
- DownloadManager::DownloadState state = m_manager.getState(index.row());
- if (state == DownloadManager::STATE_READY)
- return QColor(Qt::darkGreen);
- else if (state == DownloadManager::STATE_UNINSTALLED)
- return QColor(Qt::darkYellow);
- else if (state == DownloadManager::STATE_PAUSED)
- return QColor(Qt::darkRed);
- }
- } else if (role == Qt::ToolTipRole) {
- if (pendingDownload) {
- return tr("Pending download");
- } else {
- QString text = m_manager.getFileName(index.row()) + "\n";
- if (m_manager.isInfoIncomplete(index.row())) {
- text += tr("Information missing, please select \"Query Info\" from the context menu to re-retrieve.");
- } else {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(index.row());
- return QString("%1 (ID %2) %3<br><span>%4</span>").arg(info->modName).arg(m_manager.getModID(index.row())).arg(info->version.canonicalString()).arg(info->description.chopped(4096));
- }
- return text;
- }
- } else if (role == Qt::DecorationRole && index.column() == COL_NAME) {
- if (!pendingDownload && m_manager.getState(index.row()) >= DownloadManager::STATE_READY
- && m_manager.isInfoIncomplete(index.row()))
- return QIcon(":/MO/gui/warning_16");
- } else if (role == Qt::TextAlignmentRole) {
- if (index.column() == COL_SIZE)
- return QVariant(Qt::AlignVCenter | Qt::AlignRight);
- else
- return QVariant(Qt::AlignVCenter | Qt::AlignLeft);
- }
- return QVariant();
-}
-
-
-void DownloadList::aboutToUpdate()
-{
- emit beginResetModel();
-}
-
-
-void DownloadList::update(int row)
-{
- if (row < 0)
- emit endResetModel();
- else if (row < this->rowCount())
- emit dataChanged(this->index(row, 0, QModelIndex()), this->index(row, this->columnCount(QModelIndex())-1, QModelIndex()));
- else
- log::error("invalid row {} in download list, update failed", row);
-}
-
-bool DownloadList::lessThanPredicate(const QModelIndex &left, const QModelIndex &right)
-{
- int leftIndex = left.row();
- int rightIndex = right.row();
- if ((leftIndex < m_manager.numTotalDownloads())
- && (rightIndex < m_manager.numTotalDownloads())) {
- if (left.column() == DownloadList::COL_NAME) {
- return left.data(Qt::DisplayRole).toString().compare(right.data(Qt::DisplayRole).toString(), Qt::CaseInsensitive) < 0;
- } else if (left.column() == DownloadList::COL_MODNAME) {
- QString leftName, rightName;
-
- if (!m_manager.isInfoIncomplete(left.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(left.row());
- leftName = info->modName;
- }
-
- if (!m_manager.isInfoIncomplete(right.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(right.row());
- rightName = info->modName;
- }
-
- return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
- } else if (left.column() == DownloadList::COL_VERSION) {
- MOBase::VersionInfo versionLeft, versionRight;
-
- if (!m_manager.isInfoIncomplete(left.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(left.row());
- versionLeft = info->version;
- }
-
- if (!m_manager.isInfoIncomplete(right.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(right.row());
- versionRight = info->version;
- }
-
- return versionLeft < versionRight;
- } else if (left.column() == DownloadList::COL_ID) {
- int leftID=0, rightID=0;
-
- if (!m_manager.isInfoIncomplete(left.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(left.row());
- leftID = info->modID;
- }
-
- if (!m_manager.isInfoIncomplete(right.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(right.row());
- rightID = info->modID;
- }
-
- return leftID < rightID;
- } else if (left.column() == DownloadList::COL_STATUS) {
- DownloadManager::DownloadState leftState = m_manager.getState(left.row());
- DownloadManager::DownloadState rightState = m_manager.getState(right.row());
- if (leftState == rightState)
- return m_manager.getFileTime(left.row()) > m_manager.getFileTime(right.row());
- else
- return leftState < rightState;
- } else if (left.column() == DownloadList::COL_SIZE) {
- return m_manager.getFileSize(left.row()) < m_manager.getFileSize(right.row());
- } else if (left.column() == DownloadList::COL_FILETIME) {
- return m_manager.getFileTime(left.row()) < m_manager.getFileTime(right.row());
- } else if (left.column() == DownloadList::COL_SOURCEGAME) {
- return m_manager.getDisplayGameName(left.row()) < m_manager.getDisplayGameName(right.row());
- } else {
- return leftIndex < rightIndex;
- }
- } else {
- return leftIndex < rightIndex;
- }
-}
+/*
+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 "organizercore.h"
+#include "downloadlist.h"
+#include "downloadmanager.h"
+#include "modlistdropinfo.h"
+#include "settings.h"
+#include <utility.h>
+#include <log.h>
+#include <QEvent>
+#include <QColor>
+#include <QIcon>
+#include <QSortFilterProxyModel>
+
+using namespace MOBase;
+
+DownloadList::DownloadList(OrganizerCore& core, QObject *parent)
+ : QAbstractTableModel(parent), m_manager(*core.downloadManager()), m_settings(core.settings())
+{
+ connect(&m_manager, SIGNAL(update(int)), this, SLOT(update(int)));
+ connect(&m_manager, SIGNAL(aboutToUpdate()), this, SLOT(aboutToUpdate()));
+}
+
+
+int DownloadList::rowCount(const QModelIndex& parent) const
+{
+ if (!parent.isValid()) {
+ // root item
+ return m_manager.numTotalDownloads() + m_manager.numPendingDownloads();
+ } else {
+ return 0;
+ }
+}
+
+
+int DownloadList::columnCount(const QModelIndex&) const
+{
+ return COL_COUNT;
+}
+
+
+QModelIndex DownloadList::index(int row, int column, const QModelIndex&) const
+{
+ return createIndex(row, column, row);
+}
+
+
+QModelIndex DownloadList::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+
+QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if ((role == Qt::DisplayRole) &&
+ (orientation == Qt::Horizontal)) {
+ switch (section) {
+ case COL_NAME: return tr("Name");
+ case COL_MODNAME: return tr("Mod name");
+ case COL_VERSION: return tr("Version");
+ case COL_ID: return tr("Nexus ID");
+ case COL_SIZE: return tr("Size");
+ case COL_STATUS: return tr("Status");
+ case COL_FILETIME: return tr("Filetime");
+ case COL_SOURCEGAME: return tr("Source Game");
+ default: return QVariant();
+ }
+ } else {
+ return QAbstractItemModel::headerData(section, orientation, role);
+ }
+}
+
+Qt::ItemFlags DownloadList::flags(const QModelIndex& idx) const
+{
+ return QAbstractTableModel::flags(idx) | Qt::ItemIsDragEnabled;
+}
+
+QMimeData* DownloadList::mimeData(const QModelIndexList& indexes) const
+{
+ QMimeData* result = QAbstractItemModel::mimeData(indexes);
+ result->setData("text/plain", ModListDropInfo::DownloadText);
+ return result;
+}
+
+QVariant DownloadList::data(const QModelIndex &index, int role) const
+{
+ bool pendingDownload = index.row() >= m_manager.numTotalDownloads();
+ if (role == Qt::DisplayRole) {
+ if (pendingDownload) {
+ std::tuple<QString, int, int> nexusids = m_manager.getPendingDownload(index.row() - m_manager.numTotalDownloads());
+ switch (index.column()) {
+ case COL_NAME: return tr("< game %1 mod %2 file %3 >").arg(std::get<0>(nexusids)).arg(std::get<1>(nexusids)).arg(std::get<2>(nexusids));
+ case COL_SIZE: return tr("Unknown");
+ case COL_STATUS: return tr("Pending");
+ }
+ } else {
+ switch (index.column()) {
+ case COL_NAME: return m_settings.interface().metaDownloads() ? m_manager.getDisplayName(index.row()) : m_manager.getFileName(index.row());
+ case COL_MODNAME: {
+ if (m_manager.isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(index.row());
+ return info->modName;
+ }
+ }
+ case COL_VERSION: {
+ if (m_manager.isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(index.row());
+ return info->version.canonicalString();
+ }
+ }
+ case COL_ID: {
+ if (m_manager.isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ return QString("%1").arg(m_manager.getModID(index.row()));
+ }
+ }
+ case COL_SOURCEGAME: {
+ if (m_manager.isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ return QString("%1").arg(m_manager.getDisplayGameName(index.row()));
+ }
+ }
+ case COL_SIZE: return MOBase::localizedByteSize(m_manager.getFileSize(index.row()));
+ case COL_FILETIME: return m_manager.getFileTime(index.row());
+ case COL_STATUS:
+ switch (m_manager.getState(index.row())) {
+ // STATE_DOWNLOADING handled by DownloadProgressDelegate
+ case DownloadManager::STATE_STARTED: return tr("Started");
+ case DownloadManager::STATE_CANCELING: return tr("Canceling");
+ case DownloadManager::STATE_PAUSING: return tr("Pausing");
+ case DownloadManager::STATE_CANCELED: return tr("Canceled");
+ case DownloadManager::STATE_PAUSED: return tr("Paused");
+ case DownloadManager::STATE_ERROR: return tr("Error");
+ case DownloadManager::STATE_FETCHINGMODINFO: return tr("Fetching Info");
+ case DownloadManager::STATE_FETCHINGFILEINFO: return tr("Fetching Info");
+ case DownloadManager::STATE_FETCHINGMODINFO_MD5: return tr("Fetching Info");
+ case DownloadManager::STATE_READY: return tr("Downloaded");
+ case DownloadManager::STATE_INSTALLED: return tr("Installed");
+ case DownloadManager::STATE_UNINSTALLED: return tr("Uninstalled");
+ }
+ }
+ }
+ } else if (role == Qt::ForegroundRole && index.column() == COL_STATUS) {
+ if (pendingDownload) {
+ return QColor(Qt::darkBlue);
+ } else {
+ DownloadManager::DownloadState state = m_manager.getState(index.row());
+ if (state == DownloadManager::STATE_READY)
+ return QColor(Qt::darkGreen);
+ else if (state == DownloadManager::STATE_UNINSTALLED)
+ return QColor(Qt::darkYellow);
+ else if (state == DownloadManager::STATE_PAUSED)
+ return QColor(Qt::darkRed);
+ }
+ } else if (role == Qt::ToolTipRole) {
+ if (pendingDownload) {
+ return tr("Pending download");
+ } else {
+ QString text = m_manager.getFileName(index.row()) + "\n";
+ if (m_manager.isInfoIncomplete(index.row())) {
+ text += tr("Information missing, please select \"Query Info\" from the context menu to re-retrieve.");
+ } else {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(index.row());
+ return QString("%1 (ID %2) %3<br><span>%4</span>").arg(info->modName).arg(m_manager.getModID(index.row())).arg(info->version.canonicalString()).arg(info->description.chopped(4096));
+ }
+ return text;
+ }
+ } else if (role == Qt::DecorationRole && index.column() == COL_NAME) {
+ if (!pendingDownload && m_manager.getState(index.row()) >= DownloadManager::STATE_READY
+ && m_manager.isInfoIncomplete(index.row()))
+ return QIcon(":/MO/gui/warning_16");
+ } else if (role == Qt::TextAlignmentRole) {
+ if (index.column() == COL_SIZE)
+ return QVariant(Qt::AlignVCenter | Qt::AlignRight);
+ else
+ return QVariant(Qt::AlignVCenter | Qt::AlignLeft);
+ }
+ return QVariant();
+}
+
+
+void DownloadList::aboutToUpdate()
+{
+ emit beginResetModel();
+}
+
+
+void DownloadList::update(int row)
+{
+ if (row < 0)
+ emit endResetModel();
+ else if (row < this->rowCount())
+ emit dataChanged(this->index(row, 0, QModelIndex()), this->index(row, this->columnCount(QModelIndex())-1, QModelIndex()));
+ else
+ log::error("invalid row {} in download list, update failed", row);
+}
+
+bool DownloadList::lessThanPredicate(const QModelIndex &left, const QModelIndex &right)
+{
+ int leftIndex = left.row();
+ int rightIndex = right.row();
+ if ((leftIndex < m_manager.numTotalDownloads())
+ && (rightIndex < m_manager.numTotalDownloads())) {
+ if (left.column() == DownloadList::COL_NAME) {
+ return left.data(Qt::DisplayRole).toString().compare(right.data(Qt::DisplayRole).toString(), Qt::CaseInsensitive) < 0;
+ } else if (left.column() == DownloadList::COL_MODNAME) {
+ QString leftName, rightName;
+
+ if (!m_manager.isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(left.row());
+ leftName = info->modName;
+ }
+
+ if (!m_manager.isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(right.row());
+ rightName = info->modName;
+ }
+
+ return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
+ } else if (left.column() == DownloadList::COL_VERSION) {
+ MOBase::VersionInfo versionLeft, versionRight;
+
+ if (!m_manager.isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(left.row());
+ versionLeft = info->version;
+ }
+
+ if (!m_manager.isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(right.row());
+ versionRight = info->version;
+ }
+
+ return versionLeft < versionRight;
+ } else if (left.column() == DownloadList::COL_ID) {
+ int leftID=0, rightID=0;
+
+ if (!m_manager.isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(left.row());
+ leftID = info->modID;
+ }
+
+ if (!m_manager.isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_manager.getFileInfo(right.row());
+ rightID = info->modID;
+ }
+
+ return leftID < rightID;
+ } else if (left.column() == DownloadList::COL_STATUS) {
+ DownloadManager::DownloadState leftState = m_manager.getState(left.row());
+ DownloadManager::DownloadState rightState = m_manager.getState(right.row());
+ if (leftState == rightState)
+ return m_manager.getFileTime(left.row()) > m_manager.getFileTime(right.row());
+ else
+ return leftState < rightState;
+ } else if (left.column() == DownloadList::COL_SIZE) {
+ return m_manager.getFileSize(left.row()) < m_manager.getFileSize(right.row());
+ } else if (left.column() == DownloadList::COL_FILETIME) {
+ return m_manager.getFileTime(left.row()) < m_manager.getFileTime(right.row());
+ } else if (left.column() == DownloadList::COL_SOURCEGAME) {
+ return m_manager.getDisplayGameName(left.row()) < m_manager.getDisplayGameName(right.row());
+ } else {
+ return leftIndex < rightIndex;
+ }
+ } else {
+ return leftIndex < rightIndex;
+ }
+}
diff --git a/src/downloadlist.h b/src/downloadlist.h
index cec8b6b0..6d19e5ab 100644
--- a/src/downloadlist.h
+++ b/src/downloadlist.h
@@ -1,104 +1,104 @@
-/*
-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 DOWNLOADLIST_H
-#define DOWNLOADLIST_H
-
-#include <QAbstractTableModel>
-
-class OrganizerCore;
-class DownloadManager;
-class Settings;
-
-
-/**
- * @brief model of the list of active and completed downloads
- **/
-class DownloadList : public QAbstractTableModel
-{
-
- Q_OBJECT
-
-public:
-
- enum EColumn {
- COL_NAME = 0,
- COL_STATUS,
- COL_SIZE,
- COL_FILETIME,
- COL_MODNAME,
- COL_VERSION,
- COL_ID,
- COL_SOURCEGAME,
-
- // number of columns
- COL_COUNT
- };
-
-public:
-
- explicit DownloadList(OrganizerCore& core, QObject *parent = 0);
-
- /**
- * @brief retrieve the number of rows to display. Invoked by Qt
- *
- * @param parent not relevant for this implementation
- * @return number of rows to display
- **/
- virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
- virtual int columnCount(const QModelIndex &parent) const;
-
- QModelIndex index(int row, int column, const QModelIndex &parent) const;
- QModelIndex parent(const QModelIndex &child) const;
- Qt::ItemFlags flags(const QModelIndex& idx) const override;
- QMimeData* mimeData(const QModelIndexList& indexes) const override;
-
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
-
- /**
- * @brief retrieve the data to display in a specific row. Invoked by Qt
- *
- * @param index location to look up
- * @param role ... Defaults to Qt::DisplayRole.
- * @return this implementation only returns the row, the QItemDelegate implementation is expected to fetch its information from the DownloadManager
- **/
- virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
-
- // used in DownloadsTab as the sorting predicate for the filter widget
- //
- bool lessThanPredicate(const QModelIndex &left, const QModelIndex &right);
-
-public slots:
-
- /**
- * @brief used to inform the model that data has changed
- *
- * @param row the row that changed. This can be negative to update the whole view
- **/
- void update(int row);
-
- void aboutToUpdate();
-
-private:
-
- DownloadManager& m_manager;
- Settings& m_settings;
-};
-
-#endif // DOWNLOADLIST_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 DOWNLOADLIST_H
+#define DOWNLOADLIST_H
+
+#include <QAbstractTableModel>
+
+class OrganizerCore;
+class DownloadManager;
+class Settings;
+
+
+/**
+ * @brief model of the list of active and completed downloads
+ **/
+class DownloadList : public QAbstractTableModel
+{
+
+ Q_OBJECT
+
+public:
+
+ enum EColumn {
+ COL_NAME = 0,
+ COL_STATUS,
+ COL_SIZE,
+ COL_FILETIME,
+ COL_MODNAME,
+ COL_VERSION,
+ COL_ID,
+ COL_SOURCEGAME,
+
+ // number of columns
+ COL_COUNT
+ };
+
+public:
+
+ explicit DownloadList(OrganizerCore& core, QObject *parent = 0);
+
+ /**
+ * @brief retrieve the number of rows to display. Invoked by Qt
+ *
+ * @param parent not relevant for this implementation
+ * @return number of rows to display
+ **/
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+
+ QModelIndex index(int row, int column, const QModelIndex &parent) const;
+ QModelIndex parent(const QModelIndex &child) const;
+ Qt::ItemFlags flags(const QModelIndex& idx) const override;
+ QMimeData* mimeData(const QModelIndexList& indexes) const override;
+
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+
+ /**
+ * @brief retrieve the data to display in a specific row. Invoked by Qt
+ *
+ * @param index location to look up
+ * @param role ... Defaults to Qt::DisplayRole.
+ * @return this implementation only returns the row, the QItemDelegate implementation is expected to fetch its information from the DownloadManager
+ **/
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+
+ // used in DownloadsTab as the sorting predicate for the filter widget
+ //
+ bool lessThanPredicate(const QModelIndex &left, const QModelIndex &right);
+
+public slots:
+
+ /**
+ * @brief used to inform the model that data has changed
+ *
+ * @param row the row that changed. This can be negative to update the whole view
+ **/
+ void update(int row);
+
+ void aboutToUpdate();
+
+private:
+
+ DownloadManager& m_manager;
+ Settings& m_settings;
+};
+
+#endif // DOWNLOADLIST_H
diff --git a/src/downloadlistview.cpp b/src/downloadlistview.cpp
index 239933bd..0264f3f7 100644
--- a/src/downloadlistview.cpp
+++ b/src/downloadlistview.cpp
@@ -1,447 +1,447 @@
-/*
-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 "downloadlist.h"
-#include "downloadlistview.h"
-#include <report.h>
-#include <log.h>
-#include <QPainter>
-#include <QMouseEvent>
-#include <QMenu>
-#include <QMessageBox>
-#include <QSortFilterProxyModel>
-#include <QApplication>
-#include <QHeaderView>
-#include <QCheckBox>
-#include <QWidgetAction>
-
-using namespace MOBase;
-
-DownloadProgressDelegate::DownloadProgressDelegate(
- DownloadManager* manager, DownloadListView* list)
- : QStyledItemDelegate(list), m_Manager(manager), m_List(list)
-{
-}
-
-void DownloadProgressDelegate::paint(
- QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
-{
- QModelIndex sourceIndex;
-
- if (auto* proxy=dynamic_cast<QSortFilterProxyModel*>(m_List->model())) {
- sourceIndex = proxy->mapToSource(index);
- } else {
- sourceIndex = index;
- }
-
- bool pendingDownload = (sourceIndex.row() >= m_Manager->numTotalDownloads());
- if (sourceIndex.column() == DownloadList::COL_STATUS && !pendingDownload
- && m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_DOWNLOADING) {
- QProgressBar progressBar;
- progressBar.setProperty("downloadView", option.widget->property("downloadView"));
- progressBar.setProperty("downloadProgress", true);
- progressBar.resize(option.rect.width(), option.rect.height());
- progressBar.setTextVisible(true);
- progressBar.setAlignment(Qt::AlignCenter);
- progressBar.setMinimum(0);
- progressBar.setMaximum(100);
- progressBar.setValue(m_Manager->getProgress(sourceIndex.row()).first);
- progressBar.setFormat(m_Manager->getProgress(sourceIndex.row()).second);
- progressBar.setStyle(QApplication::style());
-
- // paint the background with default delegate first to preserve table cell styling
- QStyledItemDelegate::paint(painter, option, index);
-
- painter->save();
- painter->translate(option.rect.topLeft());
- progressBar.render(painter);
- painter->restore();
- } else {
- QStyledItemDelegate::paint(painter, option, index);
- }
-}
-
-void DownloadListHeader::customResizeSections()
-{
- // find the rightmost column that is not hidden
- int rightVisible = count() - 1;
- while (isSectionHidden(rightVisible) && rightVisible > 0)
- rightVisible--;
-
- // if that column is already squashed, squash others to the right side --
- // otherwise to the left
- if (sectionSize(rightVisible) == minimumSectionSize()) {
- for (int idx = rightVisible; idx >= 0; idx--) {
- if (!isSectionHidden(idx)) {
- if (length() != width())
- resizeSection(idx, std::max(sectionSize(idx) + width() - length(), minimumSectionSize()));
- else
- break;
- }
- }
- } else {
- for (int idx = 0; idx <= rightVisible; idx++) {
- if (!isSectionHidden(idx)) {
- if (length() != width())
- resizeSection(idx, std::max(sectionSize(idx) + width() - length(), minimumSectionSize()));
- else
- break;
- }
- }
- }
-}
-
-void DownloadListHeader::mouseReleaseEvent(QMouseEvent *event)
-{
- QHeaderView::mouseReleaseEvent(event);
- customResizeSections();
-}
-
-DownloadListView::DownloadListView(QWidget *parent)
- : QTreeView(parent)
-{
- setHeader(new DownloadListHeader(Qt::Horizontal, this));
-
- header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
- header()->setSectionsMovable(true);
- header()->setContextMenuPolicy(Qt::CustomContextMenu);
- header()->setCascadingSectionResizes(true);
- header()->setStretchLastSection(false);
- header()->setSectionResizeMode(QHeaderView::Interactive);
- header()->setDefaultSectionSize(100);
-
- setUniformRowHeights(true);
- setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
- sortByColumn(1, Qt::DescendingOrder);
-
- connect(header(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onHeaderCustomContextMenu(QPoint)));
- connect(this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(onDoubleClick(QModelIndex)));
- connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenu(QPoint)));
-}
-
-DownloadListView::~DownloadListView()
-{
-}
-
-void DownloadListView::setManager(DownloadManager *manager)
-{
- m_Manager = manager;
-
- // hide these columns by default
- //
- // note that this is overridden by the ini if MO has been started at least
- // once before, which is handled in MainWindow::processUpdates() for older
- // versions
- header()->hideSection(DownloadList::COL_MODNAME);
- header()->hideSection(DownloadList::COL_VERSION);
- header()->hideSection(DownloadList::COL_ID);
- header()->hideSection(DownloadList::COL_SOURCEGAME);
-}
-
-void DownloadListView::setSourceModel(DownloadList *sourceModel)
-{
- m_SourceModel = sourceModel;
-}
-
-void DownloadListView::onDoubleClick(const QModelIndex &index)
-{
- QModelIndex sourceIndex = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index);
- if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY)
- emit installDownload(sourceIndex.row());
- else if ((m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSED)
- || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING))
- emit resumeDownload(sourceIndex.row());
-}
-
-void DownloadListView::onHeaderCustomContextMenu(const QPoint &point)
-{
- QMenu menu;
-
- // display a list of all headers as checkboxes
- QAbstractItemModel *model = header()->model();
- for (int i = 1; i < model->columnCount(); ++i) {
- QString columnName = model->headerData(i, Qt::Horizontal).toString();
- QCheckBox *checkBox = new QCheckBox(&menu);
- checkBox->setText(columnName);
- checkBox->setChecked(!header()->isSectionHidden(i));
- QWidgetAction *checkableAction = new QWidgetAction(&menu);
- checkableAction->setDefaultWidget(checkBox);
- menu.addAction(checkableAction);
- }
-
- menu.exec(header()->viewport()->mapToGlobal(point));
-
- // view/hide columns depending on check-state
- int i = 1;
- for (const QAction *action : menu.actions()) {
- const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction*>(action);
- if (widgetAction != nullptr) {
- const QCheckBox *checkBox = qobject_cast<const QCheckBox*>(widgetAction->defaultWidget());
- if (checkBox != nullptr) {
- header()->setSectionHidden(i, !checkBox->isChecked());
- }
- }
- ++i;
- }
-
- qobject_cast<DownloadListHeader*>(header())->customResizeSections();
-}
-
-void DownloadListView::resizeEvent(QResizeEvent *event)
-{
- QTreeView::resizeEvent(event);
- qobject_cast<DownloadListHeader*>(header())->customResizeSections();
-}
-
-void DownloadListView::onCustomContextMenu(const QPoint &point)
-{
- QMenu menu(this);
- QModelIndex index = indexAt(point);
- bool hidden = false;
-
- try {
- if (index.row() >= 0) {
- const int row = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
- DownloadManager::DownloadState state = m_Manager->getState(row);
-
- hidden = m_Manager->isHidden(row);
-
- if (state >= DownloadManager::STATE_READY) {
- menu.addAction(tr("Install"), [=] { issueInstall(row); });
- if (m_Manager->isInfoIncomplete(row))
- menu.addAction(tr("Query Info"), [=] { issueQueryInfoMd5(row); });
- else
- menu.addAction(tr("Visit on Nexus"), [=] { issueVisitOnNexus(row); });
- menu.addAction(tr("Open File"), [=] { issueOpenFile(row); });
- menu.addAction(tr("Open Meta File"), [=] { issueOpenMetaFile(row); });
- menu.addAction(tr("Reveal in Explorer"), [=] { issueOpenInDownloadsFolder(row); });
-
- menu.addSeparator();
-
- menu.addAction(tr("Delete..."), [=] { issueDelete(row); });
- if (hidden)
- menu.addAction(tr("Un-Hide"), [=] { issueRestoreToView(row); });
- else
- menu.addAction(tr("Hide"), [=] { issueRemoveFromView(row); });
- } else if (state == DownloadManager::STATE_DOWNLOADING) {
- menu.addAction(tr("Cancel"), [=] { issueCancel(row); });
- menu.addAction(tr("Pause"), [=] { issuePause(row); });
- menu.addAction(tr("Reveal in Explorer"), [=] { issueOpenInDownloadsFolder(row); });
- }
- else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
- || (state == DownloadManager::STATE_PAUSING)) {
- menu.addAction(tr("Delete..."), [=] { issueDelete(row); });
- menu.addAction(tr("Resume"), [=] { issueResume(row); });
- menu.addAction(tr("Reveal in Explorer"), [=] { issueOpenInDownloadsFolder(row); });
- }
-
- menu.addSeparator();
- }
- }
- catch(std::exception&) {
- // this happens when the download index is not found, ignore it and don't
- // display download-specific actions
- }
-
- menu.addAction(tr("Delete Installed Downloads..."), [=] { issueDeleteCompleted(); });
- menu.addAction(tr("Delete Uninstalled Downloads..."), [=] { issueDeleteUninstalled(); });
- menu.addAction(tr("Delete All Downloads..."), [=] { issueDeleteAll(); });
-
- menu.addSeparator();
- if (!hidden) {
- menu.addAction(tr("Hide Installed..."), [=] { issueRemoveFromViewCompleted(); });
- menu.addAction(tr("Hide Uninstalled..."), [=] { issueRemoveFromViewUninstalled(); });
- menu.addAction(tr("Hide All..."), [=] { issueRemoveFromViewAll(); });
- } else {
- menu.addAction(tr("Un-Hide All..."), [=] { issueRestoreToViewAll(); } );
- }
-
- menu.exec(viewport()->mapToGlobal(point));
-}
-
-void DownloadListView::keyPressEvent(QKeyEvent* event)
-{
- if (selectionModel()->hasSelection()) {
- const int row = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(currentIndex()).row();
- auto state = m_Manager->getState(row);
- if (state >= DownloadManager::STATE_READY) {
- if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
- issueInstall(row);
- }
- else if (event->key() == Qt::Key_Delete) {
- issueDelete(row);
- }
- }
- else if (state == DownloadManager::STATE_DOWNLOADING) {
- if (event->key() == Qt::Key_Delete) {
- issueCancel(row);
- }
- else if (event->key() == Qt::Key_Space) {
- issuePause(event->key());
- }
- }
- else if (state == DownloadManager::STATE_PAUSED
- || state == DownloadManager::STATE_ERROR
- || state == DownloadManager::STATE_PAUSING) {
- if (event->key() == Qt::Key_Delete) {
- issueDelete(row);
- }
- else if (event->key() == Qt::Key_Space) {
- issueResume(row);
- }
- }
- }
- QTreeView::keyPressEvent(event);
-}
-
-void DownloadListView::issueInstall(int index)
-{
- emit installDownload(index);
-}
-
-void DownloadListView::issueQueryInfo(int index)
-{
- emit queryInfo(index);
-}
-
-void DownloadListView::issueQueryInfoMd5(int index)
-{
- emit queryInfoMd5(index);
-}
-
-void DownloadListView::issueDelete(int index)
-{
- const auto r = MOBase::TaskDialog(this, tr("Delete download"))
- .main("Are you sure you want to delete this download?")
- .content(m_Manager->getFilePath(index))
- .icon(QMessageBox::Question)
- .button({tr("Move to the Recycle Bin"), QMessageBox::Yes})
- .button({tr("Cancel"), QMessageBox::Cancel})
- .exec();
-
- if (r != QMessageBox::Yes) {
- return;
- }
-
- emit removeDownload(index, true);
-}
-
-void DownloadListView::issueRemoveFromView(int index)
-{
- log::debug("removing from view: {}", index);
- emit removeDownload(index, false);
-}
-
-void DownloadListView::issueRestoreToView(int index)
-{
- emit restoreDownload(index);
-}
-
-void DownloadListView::issueRestoreToViewAll()
-{
- emit restoreDownload(-1);
-}
-
-void DownloadListView::issueVisitOnNexus(int index)
-{
- emit visitOnNexus(index);
-}
-
-void DownloadListView::issueOpenFile(int index)
-{
- emit openFile(index);
-}
-
-void DownloadListView::issueOpenMetaFile(int index) {
- emit openMetaFile(index);
-}
-
-void DownloadListView::issueOpenInDownloadsFolder(int index)
-{
- emit openInDownloadsFolder(index);
-}
-
-void DownloadListView::issueCancel(int index)
-{
- emit cancelDownload(index);
-}
-
-void DownloadListView::issuePause(int index)
-{
- emit pauseDownload(index);
-}
-
-void DownloadListView::issueResume(int index)
-{
- emit resumeDownload(index);
-}
-
-void DownloadListView::issueDeleteAll()
-{
- if (QMessageBox::warning(nullptr, tr("Delete Files?"),
- tr("This will remove all finished downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-1, true);
- }
-}
-
-void DownloadListView::issueDeleteCompleted()
-{
- if (QMessageBox::warning(nullptr, tr("Delete Files?"),
- tr("This will remove all installed downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-2, true);
- }
-}
-
-void DownloadListView::issueDeleteUninstalled()
-{
- if (QMessageBox::warning(nullptr, tr("Delete Files?"),
- tr("This will remove all uninstalled downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-3, true);
- }
-}
-
-void DownloadListView::issueRemoveFromViewAll()
-{
- if (QMessageBox::question(nullptr, tr("Hide Files?"),
- tr("This will remove all finished downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-1, false);
- }
-}
-
-void DownloadListView::issueRemoveFromViewCompleted()
-{
- if (QMessageBox::question(nullptr, tr("Hide Files?"),
- tr("This will remove all installed downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-2, false);
- }
-}
-
-void DownloadListView::issueRemoveFromViewUninstalled()
-{
- if (QMessageBox::question(nullptr, tr("Hide Files?"),
- tr("This will remove all uninstalled downloads from this list (but NOT from disk)."),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit removeDownload(-3, false);
- }
-}
+/*
+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 "downloadlist.h"
+#include "downloadlistview.h"
+#include <report.h>
+#include <log.h>
+#include <QPainter>
+#include <QMouseEvent>
+#include <QMenu>
+#include <QMessageBox>
+#include <QSortFilterProxyModel>
+#include <QApplication>
+#include <QHeaderView>
+#include <QCheckBox>
+#include <QWidgetAction>
+
+using namespace MOBase;
+
+DownloadProgressDelegate::DownloadProgressDelegate(
+ DownloadManager* manager, DownloadListView* list)
+ : QStyledItemDelegate(list), m_Manager(manager), m_List(list)
+{
+}
+
+void DownloadProgressDelegate::paint(
+ QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ QModelIndex sourceIndex;
+
+ if (auto* proxy=dynamic_cast<QSortFilterProxyModel*>(m_List->model())) {
+ sourceIndex = proxy->mapToSource(index);
+ } else {
+ sourceIndex = index;
+ }
+
+ bool pendingDownload = (sourceIndex.row() >= m_Manager->numTotalDownloads());
+ if (sourceIndex.column() == DownloadList::COL_STATUS && !pendingDownload
+ && m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_DOWNLOADING) {
+ QProgressBar progressBar;
+ progressBar.setProperty("downloadView", option.widget->property("downloadView"));
+ progressBar.setProperty("downloadProgress", true);
+ progressBar.resize(option.rect.width(), option.rect.height());
+ progressBar.setTextVisible(true);
+ progressBar.setAlignment(Qt::AlignCenter);
+ progressBar.setMinimum(0);
+ progressBar.setMaximum(100);
+ progressBar.setValue(m_Manager->getProgress(sourceIndex.row()).first);
+ progressBar.setFormat(m_Manager->getProgress(sourceIndex.row()).second);
+ progressBar.setStyle(QApplication::style());
+
+ // paint the background with default delegate first to preserve table cell styling
+ QStyledItemDelegate::paint(painter, option, index);
+
+ painter->save();
+ painter->translate(option.rect.topLeft());
+ progressBar.render(painter);
+ painter->restore();
+ } else {
+ QStyledItemDelegate::paint(painter, option, index);
+ }
+}
+
+void DownloadListHeader::customResizeSections()
+{
+ // find the rightmost column that is not hidden
+ int rightVisible = count() - 1;
+ while (isSectionHidden(rightVisible) && rightVisible > 0)
+ rightVisible--;
+
+ // if that column is already squashed, squash others to the right side --
+ // otherwise to the left
+ if (sectionSize(rightVisible) == minimumSectionSize()) {
+ for (int idx = rightVisible; idx >= 0; idx--) {
+ if (!isSectionHidden(idx)) {
+ if (length() != width())
+ resizeSection(idx, std::max(sectionSize(idx) + width() - length(), minimumSectionSize()));
+ else
+ break;
+ }
+ }
+ } else {
+ for (int idx = 0; idx <= rightVisible; idx++) {
+ if (!isSectionHidden(idx)) {
+ if (length() != width())
+ resizeSection(idx, std::max(sectionSize(idx) + width() - length(), minimumSectionSize()));
+ else
+ break;
+ }
+ }
+ }
+}
+
+void DownloadListHeader::mouseReleaseEvent(QMouseEvent *event)
+{
+ QHeaderView::mouseReleaseEvent(event);
+ customResizeSections();
+}
+
+DownloadListView::DownloadListView(QWidget *parent)
+ : QTreeView(parent)
+{
+ setHeader(new DownloadListHeader(Qt::Horizontal, this));
+
+ header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ header()->setSectionsMovable(true);
+ header()->setContextMenuPolicy(Qt::CustomContextMenu);
+ header()->setCascadingSectionResizes(true);
+ header()->setStretchLastSection(false);
+ header()->setSectionResizeMode(QHeaderView::Interactive);
+ header()->setDefaultSectionSize(100);
+
+ setUniformRowHeights(true);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ sortByColumn(1, Qt::DescendingOrder);
+
+ connect(header(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onHeaderCustomContextMenu(QPoint)));
+ connect(this, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(onDoubleClick(QModelIndex)));
+ connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenu(QPoint)));
+}
+
+DownloadListView::~DownloadListView()
+{
+}
+
+void DownloadListView::setManager(DownloadManager *manager)
+{
+ m_Manager = manager;
+
+ // hide these columns by default
+ //
+ // note that this is overridden by the ini if MO has been started at least
+ // once before, which is handled in MainWindow::processUpdates() for older
+ // versions
+ header()->hideSection(DownloadList::COL_MODNAME);
+ header()->hideSection(DownloadList::COL_VERSION);
+ header()->hideSection(DownloadList::COL_ID);
+ header()->hideSection(DownloadList::COL_SOURCEGAME);
+}
+
+void DownloadListView::setSourceModel(DownloadList *sourceModel)
+{
+ m_SourceModel = sourceModel;
+}
+
+void DownloadListView::onDoubleClick(const QModelIndex &index)
+{
+ QModelIndex sourceIndex = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index);
+ if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY)
+ emit installDownload(sourceIndex.row());
+ else if ((m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSED)
+ || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING))
+ emit resumeDownload(sourceIndex.row());
+}
+
+void DownloadListView::onHeaderCustomContextMenu(const QPoint &point)
+{
+ QMenu menu;
+
+ // display a list of all headers as checkboxes
+ QAbstractItemModel *model = header()->model();
+ for (int i = 1; i < model->columnCount(); ++i) {
+ QString columnName = model->headerData(i, Qt::Horizontal).toString();
+ QCheckBox *checkBox = new QCheckBox(&menu);
+ checkBox->setText(columnName);
+ checkBox->setChecked(!header()->isSectionHidden(i));
+ QWidgetAction *checkableAction = new QWidgetAction(&menu);
+ checkableAction->setDefaultWidget(checkBox);
+ menu.addAction(checkableAction);
+ }
+
+ menu.exec(header()->viewport()->mapToGlobal(point));
+
+ // view/hide columns depending on check-state
+ int i = 1;
+ for (const QAction *action : menu.actions()) {
+ const QWidgetAction *widgetAction = qobject_cast<const QWidgetAction*>(action);
+ if (widgetAction != nullptr) {
+ const QCheckBox *checkBox = qobject_cast<const QCheckBox*>(widgetAction->defaultWidget());
+ if (checkBox != nullptr) {
+ header()->setSectionHidden(i, !checkBox->isChecked());
+ }
+ }
+ ++i;
+ }
+
+ qobject_cast<DownloadListHeader*>(header())->customResizeSections();
+}
+
+void DownloadListView::resizeEvent(QResizeEvent *event)
+{
+ QTreeView::resizeEvent(event);
+ qobject_cast<DownloadListHeader*>(header())->customResizeSections();
+}
+
+void DownloadListView::onCustomContextMenu(const QPoint &point)
+{
+ QMenu menu(this);
+ QModelIndex index = indexAt(point);
+ bool hidden = false;
+
+ try {
+ if (index.row() >= 0) {
+ const int row = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
+ DownloadManager::DownloadState state = m_Manager->getState(row);
+
+ hidden = m_Manager->isHidden(row);
+
+ if (state >= DownloadManager::STATE_READY) {
+ menu.addAction(tr("Install"), [=] { issueInstall(row); });
+ if (m_Manager->isInfoIncomplete(row))
+ menu.addAction(tr("Query Info"), [=] { issueQueryInfoMd5(row); });
+ else
+ menu.addAction(tr("Visit on Nexus"), [=] { issueVisitOnNexus(row); });
+ menu.addAction(tr("Open File"), [=] { issueOpenFile(row); });
+ menu.addAction(tr("Open Meta File"), [=] { issueOpenMetaFile(row); });
+ menu.addAction(tr("Reveal in Explorer"), [=] { issueOpenInDownloadsFolder(row); });
+
+ menu.addSeparator();
+
+ menu.addAction(tr("Delete..."), [=] { issueDelete(row); });
+ if (hidden)
+ menu.addAction(tr("Un-Hide"), [=] { issueRestoreToView(row); });
+ else
+ menu.addAction(tr("Hide"), [=] { issueRemoveFromView(row); });
+ } else if (state == DownloadManager::STATE_DOWNLOADING) {
+ menu.addAction(tr("Cancel"), [=] { issueCancel(row); });
+ menu.addAction(tr("Pause"), [=] { issuePause(row); });
+ menu.addAction(tr("Reveal in Explorer"), [=] { issueOpenInDownloadsFolder(row); });
+ }
+ else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
+ || (state == DownloadManager::STATE_PAUSING)) {
+ menu.addAction(tr("Delete..."), [=] { issueDelete(row); });
+ menu.addAction(tr("Resume"), [=] { issueResume(row); });
+ menu.addAction(tr("Reveal in Explorer"), [=] { issueOpenInDownloadsFolder(row); });
+ }
+
+ menu.addSeparator();
+ }
+ }
+ catch(std::exception&) {
+ // this happens when the download index is not found, ignore it and don't
+ // display download-specific actions
+ }
+
+ menu.addAction(tr("Delete Installed Downloads..."), [=] { issueDeleteCompleted(); });
+ menu.addAction(tr("Delete Uninstalled Downloads..."), [=] { issueDeleteUninstalled(); });
+ menu.addAction(tr("Delete All Downloads..."), [=] { issueDeleteAll(); });
+
+ menu.addSeparator();
+ if (!hidden) {
+ menu.addAction(tr("Hide Installed..."), [=] { issueRemoveFromViewCompleted(); });
+ menu.addAction(tr("Hide Uninstalled..."), [=] { issueRemoveFromViewUninstalled(); });
+ menu.addAction(tr("Hide All..."), [=] { issueRemoveFromViewAll(); });
+ } else {
+ menu.addAction(tr("Un-Hide All..."), [=] { issueRestoreToViewAll(); } );
+ }
+
+ menu.exec(viewport()->mapToGlobal(point));
+}
+
+void DownloadListView::keyPressEvent(QKeyEvent* event)
+{
+ if (selectionModel()->hasSelection()) {
+ const int row = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(currentIndex()).row();
+ auto state = m_Manager->getState(row);
+ if (state >= DownloadManager::STATE_READY) {
+ if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
+ issueInstall(row);
+ }
+ else if (event->key() == Qt::Key_Delete) {
+ issueDelete(row);
+ }
+ }
+ else if (state == DownloadManager::STATE_DOWNLOADING) {
+ if (event->key() == Qt::Key_Delete) {
+ issueCancel(row);
+ }
+ else if (event->key() == Qt::Key_Space) {
+ issuePause(event->key());
+ }
+ }
+ else if (state == DownloadManager::STATE_PAUSED
+ || state == DownloadManager::STATE_ERROR
+ || state == DownloadManager::STATE_PAUSING) {
+ if (event->key() == Qt::Key_Delete) {
+ issueDelete(row);
+ }
+ else if (event->key() == Qt::Key_Space) {
+ issueResume(row);
+ }
+ }
+ }
+ QTreeView::keyPressEvent(event);
+}
+
+void DownloadListView::issueInstall(int index)
+{
+ emit installDownload(index);
+}
+
+void DownloadListView::issueQueryInfo(int index)
+{
+ emit queryInfo(index);
+}
+
+void DownloadListView::issueQueryInfoMd5(int index)
+{
+ emit queryInfoMd5(index);
+}
+
+void DownloadListView::issueDelete(int index)
+{
+ const auto r = MOBase::TaskDialog(this, tr("Delete download"))
+ .main("Are you sure you want to delete this download?")
+ .content(m_Manager->getFilePath(index))
+ .icon(QMessageBox::Question)
+ .button({tr("Move to the Recycle Bin"), QMessageBox::Yes})
+ .button({tr("Cancel"), QMessageBox::Cancel})
+ .exec();
+
+ if (r != QMessageBox::Yes) {
+ return;
+ }
+
+ emit removeDownload(index, true);
+}
+
+void DownloadListView::issueRemoveFromView(int index)
+{
+ log::debug("removing from view: {}", index);
+ emit removeDownload(index, false);
+}
+
+void DownloadListView::issueRestoreToView(int index)
+{
+ emit restoreDownload(index);
+}
+
+void DownloadListView::issueRestoreToViewAll()
+{
+ emit restoreDownload(-1);
+}
+
+void DownloadListView::issueVisitOnNexus(int index)
+{
+ emit visitOnNexus(index);
+}
+
+void DownloadListView::issueOpenFile(int index)
+{
+ emit openFile(index);
+}
+
+void DownloadListView::issueOpenMetaFile(int index) {
+ emit openMetaFile(index);
+}
+
+void DownloadListView::issueOpenInDownloadsFolder(int index)
+{
+ emit openInDownloadsFolder(index);
+}
+
+void DownloadListView::issueCancel(int index)
+{
+ emit cancelDownload(index);
+}
+
+void DownloadListView::issuePause(int index)
+{
+ emit pauseDownload(index);
+}
+
+void DownloadListView::issueResume(int index)
+{
+ emit resumeDownload(index);
+}
+
+void DownloadListView::issueDeleteAll()
+{
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will remove all finished downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(-1, true);
+ }
+}
+
+void DownloadListView::issueDeleteCompleted()
+{
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will remove all installed downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(-2, true);
+ }
+}
+
+void DownloadListView::issueDeleteUninstalled()
+{
+ if (QMessageBox::warning(nullptr, tr("Delete Files?"),
+ tr("This will remove all uninstalled downloads from this list and from disk.\n\nAre you absolutely sure you want to proceed?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(-3, true);
+ }
+}
+
+void DownloadListView::issueRemoveFromViewAll()
+{
+ if (QMessageBox::question(nullptr, tr("Hide Files?"),
+ tr("This will remove all finished downloads from this list (but NOT from disk)."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(-1, false);
+ }
+}
+
+void DownloadListView::issueRemoveFromViewCompleted()
+{
+ if (QMessageBox::question(nullptr, tr("Hide Files?"),
+ tr("This will remove all installed downloads from this list (but NOT from disk)."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(-2, false);
+ }
+}
+
+void DownloadListView::issueRemoveFromViewUninstalled()
+{
+ if (QMessageBox::question(nullptr, tr("Hide Files?"),
+ tr("This will remove all uninstalled downloads from this list (but NOT from disk)."),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ emit removeDownload(-3, false);
+ }
+}
diff --git a/src/downloadlistview.h b/src/downloadlistview.h
index 247a71cc..dfeec8a5 100644
--- a/src/downloadlistview.h
+++ b/src/downloadlistview.h
@@ -1,128 +1,128 @@
-/*
-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 DOWNLOADLISTWIDGET_H
-#define DOWNLOADLISTWIDGET_H
-
-#include "downloadmanager.h"
-#include "downloadlist.h"
-#include <QWidget>
-#include <QItemDelegate>
-#include <QLabel>
-#include <QProgressBar>
-#include <QTreeView>
-#include <QHeaderView>
-#include <QStyledItemDelegate>
-
-
-namespace Ui {
- class DownloadListView;
-}
-
-class DownloadListView;
-
-class DownloadProgressDelegate : public QStyledItemDelegate
-{
- Q_OBJECT
-
-public:
- DownloadProgressDelegate(DownloadManager* manager, DownloadListView* list);
-
- void paint(QPainter *painter, const QStyleOptionViewItem &option,
- const QModelIndex &index) const override;
-
-private:
- DownloadManager* m_Manager;
- DownloadListView* m_List;
-};
-
-class DownloadListHeader : public QHeaderView
-{
- Q_OBJECT
-
-public:
- explicit DownloadListHeader(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {}
- void customResizeSections();
-
-private:
- void mouseReleaseEvent(QMouseEvent *event) override;
-};
-
-class DownloadListView : public QTreeView
-{
- Q_OBJECT
-
-public:
- explicit DownloadListView(QWidget *parent = 0);
- ~DownloadListView();
-
- void setManager(DownloadManager *manager);
- void setSourceModel(DownloadList *sourceModel);
-
-signals:
- void installDownload(int index);
- void queryInfo(int index);
- void queryInfoMd5(int index);
- void removeDownload(int index, bool deleteFile);
- void restoreDownload(int index);
- void cancelDownload(int index);
- void pauseDownload(int index);
- void resumeDownload(int index);
- void visitOnNexus(int index);
- void openFile(int index);
- void openMetaFile(int index);
- void openInDownloadsFolder(int index);
-
-protected:
- void keyPressEvent(QKeyEvent* event) override;
-
-private slots:
- void onDoubleClick(const QModelIndex& index);
- void onCustomContextMenu(const QPoint& point);
- void onHeaderCustomContextMenu(const QPoint& point);
-
- void issueInstall(int index);
- void issueDelete(int index);
- void issueRemoveFromView(int index);
- void issueRestoreToView(int index);
- void issueRestoreToViewAll();
- void issueVisitOnNexus(int index);
- void issueOpenFile(int index);
- void issueOpenMetaFile(int index);
- void issueOpenInDownloadsFolder(int index);
- void issueCancel(int index);
- void issuePause(int index);
- void issueResume(int index);
- void issueDeleteAll();
- void issueDeleteCompleted();
- void issueDeleteUninstalled();
- void issueRemoveFromViewAll();
- void issueRemoveFromViewCompleted();
- void issueRemoveFromViewUninstalled();
- void issueQueryInfo(int index);
- void issueQueryInfoMd5(int index);
-
-private:
- DownloadManager *m_Manager;
- DownloadList *m_SourceModel = 0;
-
- void resizeEvent(QResizeEvent *event);
-};
-
-#endif // DOWNLOADLISTWIDGET_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 DOWNLOADLISTWIDGET_H
+#define DOWNLOADLISTWIDGET_H
+
+#include "downloadmanager.h"
+#include "downloadlist.h"
+#include <QWidget>
+#include <QItemDelegate>
+#include <QLabel>
+#include <QProgressBar>
+#include <QTreeView>
+#include <QHeaderView>
+#include <QStyledItemDelegate>
+
+
+namespace Ui {
+ class DownloadListView;
+}
+
+class DownloadListView;
+
+class DownloadProgressDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ DownloadProgressDelegate(DownloadManager* manager, DownloadListView* list);
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+
+private:
+ DownloadManager* m_Manager;
+ DownloadListView* m_List;
+};
+
+class DownloadListHeader : public QHeaderView
+{
+ Q_OBJECT
+
+public:
+ explicit DownloadListHeader(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {}
+ void customResizeSections();
+
+private:
+ void mouseReleaseEvent(QMouseEvent *event) override;
+};
+
+class DownloadListView : public QTreeView
+{
+ Q_OBJECT
+
+public:
+ explicit DownloadListView(QWidget *parent = 0);
+ ~DownloadListView();
+
+ void setManager(DownloadManager *manager);
+ void setSourceModel(DownloadList *sourceModel);
+
+signals:
+ void installDownload(int index);
+ void queryInfo(int index);
+ void queryInfoMd5(int index);
+ void removeDownload(int index, bool deleteFile);
+ void restoreDownload(int index);
+ void cancelDownload(int index);
+ void pauseDownload(int index);
+ void resumeDownload(int index);
+ void visitOnNexus(int index);
+ void openFile(int index);
+ void openMetaFile(int index);
+ void openInDownloadsFolder(int index);
+
+protected:
+ void keyPressEvent(QKeyEvent* event) override;
+
+private slots:
+ void onDoubleClick(const QModelIndex& index);
+ void onCustomContextMenu(const QPoint& point);
+ void onHeaderCustomContextMenu(const QPoint& point);
+
+ void issueInstall(int index);
+ void issueDelete(int index);
+ void issueRemoveFromView(int index);
+ void issueRestoreToView(int index);
+ void issueRestoreToViewAll();
+ void issueVisitOnNexus(int index);
+ void issueOpenFile(int index);
+ void issueOpenMetaFile(int index);
+ void issueOpenInDownloadsFolder(int index);
+ void issueCancel(int index);
+ void issuePause(int index);
+ void issueResume(int index);
+ void issueDeleteAll();
+ void issueDeleteCompleted();
+ void issueDeleteUninstalled();
+ void issueRemoveFromViewAll();
+ void issueRemoveFromViewCompleted();
+ void issueRemoveFromViewUninstalled();
+ void issueQueryInfo(int index);
+ void issueQueryInfoMd5(int index);
+
+private:
+ DownloadManager *m_Manager;
+ DownloadList *m_SourceModel = 0;
+
+ void resizeEvent(QResizeEvent *event);
+};
+
+#endif // DOWNLOADLISTWIDGET_H
diff --git a/src/editexecutablesdialog.h b/src/editexecutablesdialog.h
index 8da53e09..da30bf0b 100644
--- a/src/editexecutablesdialog.h
+++ b/src/editexecutablesdialog.h
@@ -1,243 +1,243 @@
-/*
-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 EDITEXECUTABLESDIALOG_H
-#define EDITEXECUTABLESDIALOG_H
-
-#include "tutorabledialog.h"
-#include <QListWidgetItem>
-#include "executableslist.h"
-#include "profile.h"
-#include "iplugingame.h"
-#include <QTimer>
-#include <QAbstractButton>
-#include <optional>
-
-namespace Ui {
- class EditExecutablesDialog;
-}
-
-class ModList;
-class OrganizerCore;
-
-/** helper class to manage custom overwrites within the edit executables
- * dialog, stores a T and a bool in map indexed by a QString
- **/
-template <class T>
-class ToggableMap
-{
-public:
- struct Value
- {
- bool enabled;
- T value;
-
- Value(bool b, T&& v)
- : enabled(b), value(std::forward<T>(v))
- {
- }
- };
-
- /**
- * returns the Value associated with the given title, or empty
- **/
- std::optional<Value> find(const QString& title) const
- {
- auto itor = m_map.find(title);
- if (itor == m_map.end()) {
- return {};
- }
-
- return itor->second;
- }
-
- /**
- * sets the given value, adds it if not found
- **/
- void set(QString title, bool b, T value)
- {
- m_map.insert_or_assign(std::move(title), Value(b, std::move(value)));
- }
-
- /**
- * sets whether the given value is enabled, inserts it if not found
- **/
- void setEnabled(const QString& title, bool b)
- {
- auto itor = m_map.find(title);
-
- if (itor == m_map.end()) {
- m_map.emplace(title, Value(b, {}));
- } else {
- itor->second.enabled = b;
- }
- }
-
- /**
- * sets the given value, inserts it enabled if not found
- **/
- void setValue(const QString& title, T value)
- {
- auto itor = m_map.find(title);
-
- if (itor == m_map.end()) {
- m_map.emplace(title, Value(true, std::move(value)));
- } else {
- itor->second.value = std::move(value);
- }
- }
-
- /**
- * renames the given value, ignored if not found
- **/
- void rename(const QString& oldTitle, QString newTitle)
- {
- auto itor = m_map.find(oldTitle);
- if (itor == m_map.end()) {
- return;
- }
-
- // move to new title, erase old
- m_map.emplace(std::move(newTitle), std::move(itor->second));
- m_map.erase(itor);
- }
-
- /**
- * removes the given value, ignored if not found
- **/
- void remove(const QString& title)
- {
- auto itor = m_map.find(title);
- if (itor == m_map.end()) {
- return;
- }
-
- m_map.erase(itor);
- }
-
-private:
- std::map<QString, Value> m_map;
-};
-
-
-/**
- * @brief Dialog to manage the list of executables
- **/
-class EditExecutablesDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT;
- friend class IgnoreChanges;
-
-public:
- using CustomOverwrites = ToggableMap<QString>;
- using ForcedLibraries = ToggableMap<QList<MOBase::ExecutableForcedLoadSetting>>;
-
- explicit EditExecutablesDialog(
- OrganizerCore& oc, int selection=-1, QWidget* parent=nullptr);
-
- ~EditExecutablesDialog();
-
- // also saves and restores geometry
- //
- int exec() override;
-
- ExecutablesList getExecutablesList() const;
- const CustomOverwrites& getCustomOverwrites() const;
- const ForcedLibraries& getForcedLibraries() const;
-
-private slots:
- void on_list_itemSelectionChanged();
-
- void on_reset_clicked();
- void on_add_clicked();
- void on_remove_clicked();
- void on_up_clicked();
- void on_down_clicked();
-
- void on_title_textChanged(const QString& s);
- void on_title_editingFinished();
- void on_overwriteSteamAppID_toggled(bool checked);
- void on_createFilesInMod_toggled(bool checked);
- void on_forceLoadLibraries_toggled(bool checked);
-
- void on_browseBinary_clicked();
- void on_browseWorkingDirectory_clicked();
- void on_configureLibraries_clicked();
-
- void on_buttons_clicked(QAbstractButton* b);
-
-private:
- std::unique_ptr<Ui::EditExecutablesDialog> ui;
- OrganizerCore& m_organizerCore;
-
- // copy of the original executables, used to clear the current settings when
- // committing changes
- const ExecutablesList m_originalExecutables;
-
- // current executable list
- ExecutablesList m_executablesList;
-
- // custom overwrites set in the dialog
- CustomOverwrites m_customOverwrites;
-
- // forced libraries set in the dialog
- ForcedLibraries m_forcedLibraries;
-
- // remembers the last executable title that made sense, reverts to this when
- // the widget loses focus if it's empty
- QString m_lastGoodTitle;
-
- // true when the change events being triggered are in response to loading
- // the executable's data into the UI, not from a user change
- bool m_settingUI;
-
-
- void loadCustomOverwrites();
- void loadForcedLibraries();
-
- QListWidgetItem* selectedItem();
- Executable* selectedExe();
-
- void fillList();
- QListWidgetItem* createListItem(const Executable& exe);
- void updateUI(const QListWidgetItem* item, const Executable* e);
- void clearEdits();
- void setEdits(const Executable& e);
- void setButtons(const QListWidgetItem* item, const Executable* e);
- void save();
- void saveOrder();
- bool canMove(const QListWidgetItem* item, int direction);
- void move(QListWidgetItem* item, int direction);
- bool isTitleConflicting(const QString& s);
- bool commitChanges();
- void setDirty(bool b);
- void selectIndex(int i);
- bool checkOutputMods(const ExecutablesList& exes);
-
- void addFromFile();
- void addEmpty();
- void clone();
- void addNew(Executable e);
-
- QFileInfo browseBinary(const QString& initial);
- void setBinary(const QFileInfo& binary);
- void setJarBinary(const QFileInfo& binary);
-};
-
-#endif // EDITEXECUTABLESDIALOG_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 EDITEXECUTABLESDIALOG_H
+#define EDITEXECUTABLESDIALOG_H
+
+#include "tutorabledialog.h"
+#include <QListWidgetItem>
+#include "executableslist.h"
+#include "profile.h"
+#include "iplugingame.h"
+#include <QTimer>
+#include <QAbstractButton>
+#include <optional>
+
+namespace Ui {
+ class EditExecutablesDialog;
+}
+
+class ModList;
+class OrganizerCore;
+
+/** helper class to manage custom overwrites within the edit executables
+ * dialog, stores a T and a bool in map indexed by a QString
+ **/
+template <class T>
+class ToggableMap
+{
+public:
+ struct Value
+ {
+ bool enabled;
+ T value;
+
+ Value(bool b, T&& v)
+ : enabled(b), value(std::forward<T>(v))
+ {
+ }
+ };
+
+ /**
+ * returns the Value associated with the given title, or empty
+ **/
+ std::optional<Value> find(const QString& title) const
+ {
+ auto itor = m_map.find(title);
+ if (itor == m_map.end()) {
+ return {};
+ }
+
+ return itor->second;
+ }
+
+ /**
+ * sets the given value, adds it if not found
+ **/
+ void set(QString title, bool b, T value)
+ {
+ m_map.insert_or_assign(std::move(title), Value(b, std::move(value)));
+ }
+
+ /**
+ * sets whether the given value is enabled, inserts it if not found
+ **/
+ void setEnabled(const QString& title, bool b)
+ {
+ auto itor = m_map.find(title);
+
+ if (itor == m_map.end()) {
+ m_map.emplace(title, Value(b, {}));
+ } else {
+ itor->second.enabled = b;
+ }
+ }
+
+ /**
+ * sets the given value, inserts it enabled if not found
+ **/
+ void setValue(const QString& title, T value)
+ {
+ auto itor = m_map.find(title);
+
+ if (itor == m_map.end()) {
+ m_map.emplace(title, Value(true, std::move(value)));
+ } else {
+ itor->second.value = std::move(value);
+ }
+ }
+
+ /**
+ * renames the given value, ignored if not found
+ **/
+ void rename(const QString& oldTitle, QString newTitle)
+ {
+ auto itor = m_map.find(oldTitle);
+ if (itor == m_map.end()) {
+ return;
+ }
+
+ // move to new title, erase old
+ m_map.emplace(std::move(newTitle), std::move(itor->second));
+ m_map.erase(itor);
+ }
+
+ /**
+ * removes the given value, ignored if not found
+ **/
+ void remove(const QString& title)
+ {
+ auto itor = m_map.find(title);
+ if (itor == m_map.end()) {
+ return;
+ }
+
+ m_map.erase(itor);
+ }
+
+private:
+ std::map<QString, Value> m_map;
+};
+
+
+/**
+ * @brief Dialog to manage the list of executables
+ **/
+class EditExecutablesDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT;
+ friend class IgnoreChanges;
+
+public:
+ using CustomOverwrites = ToggableMap<QString>;
+ using ForcedLibraries = ToggableMap<QList<MOBase::ExecutableForcedLoadSetting>>;
+
+ explicit EditExecutablesDialog(
+ OrganizerCore& oc, int selection=-1, QWidget* parent=nullptr);
+
+ ~EditExecutablesDialog();
+
+ // also saves and restores geometry
+ //
+ int exec() override;
+
+ ExecutablesList getExecutablesList() const;
+ const CustomOverwrites& getCustomOverwrites() const;
+ const ForcedLibraries& getForcedLibraries() const;
+
+private slots:
+ void on_list_itemSelectionChanged();
+
+ void on_reset_clicked();
+ void on_add_clicked();
+ void on_remove_clicked();
+ void on_up_clicked();
+ void on_down_clicked();
+
+ void on_title_textChanged(const QString& s);
+ void on_title_editingFinished();
+ void on_overwriteSteamAppID_toggled(bool checked);
+ void on_createFilesInMod_toggled(bool checked);
+ void on_forceLoadLibraries_toggled(bool checked);
+
+ void on_browseBinary_clicked();
+ void on_browseWorkingDirectory_clicked();
+ void on_configureLibraries_clicked();
+
+ void on_buttons_clicked(QAbstractButton* b);
+
+private:
+ std::unique_ptr<Ui::EditExecutablesDialog> ui;
+ OrganizerCore& m_organizerCore;
+
+ // copy of the original executables, used to clear the current settings when
+ // committing changes
+ const ExecutablesList m_originalExecutables;
+
+ // current executable list
+ ExecutablesList m_executablesList;
+
+ // custom overwrites set in the dialog
+ CustomOverwrites m_customOverwrites;
+
+ // forced libraries set in the dialog
+ ForcedLibraries m_forcedLibraries;
+
+ // remembers the last executable title that made sense, reverts to this when
+ // the widget loses focus if it's empty
+ QString m_lastGoodTitle;
+
+ // true when the change events being triggered are in response to loading
+ // the executable's data into the UI, not from a user change
+ bool m_settingUI;
+
+
+ void loadCustomOverwrites();
+ void loadForcedLibraries();
+
+ QListWidgetItem* selectedItem();
+ Executable* selectedExe();
+
+ void fillList();
+ QListWidgetItem* createListItem(const Executable& exe);
+ void updateUI(const QListWidgetItem* item, const Executable* e);
+ void clearEdits();
+ void setEdits(const Executable& e);
+ void setButtons(const QListWidgetItem* item, const Executable* e);
+ void save();
+ void saveOrder();
+ bool canMove(const QListWidgetItem* item, int direction);
+ void move(QListWidgetItem* item, int direction);
+ bool isTitleConflicting(const QString& s);
+ bool commitChanges();
+ void setDirty(bool b);
+ void selectIndex(int i);
+ bool checkOutputMods(const ExecutablesList& exes);
+
+ void addFromFile();
+ void addEmpty();
+ void clone();
+ void addNew(Executable e);
+
+ QFileInfo browseBinary(const QString& initial);
+ void setBinary(const QFileInfo& binary);
+ void setJarBinary(const QFileInfo& binary);
+};
+
+#endif // EDITEXECUTABLESDIALOG_H
diff --git a/src/executableslist.cpp b/src/executableslist.cpp
index 137f38dc..81fb53cb 100644
--- a/src/executableslist.cpp
+++ b/src/executableslist.cpp
@@ -1,491 +1,491 @@
-/*
-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 "executableslist.h"
-
-#include "iplugingame.h"
-#include "utility.h"
-#include "settings.h"
-#include <log.h>
-
-#include <QFileInfo>
-#include <QDir>
-#include <QDebug>
-#include <QCoreApplication>
-
-
-#include <algorithm>
-
-
-using namespace MOBase;
-
-ExecutablesList::iterator ExecutablesList::begin()
-{
- return m_Executables.begin();
-}
-
-ExecutablesList::const_iterator ExecutablesList::begin() const
-{
- return m_Executables.begin();
-}
-
-ExecutablesList::iterator ExecutablesList::end()
-{
- return m_Executables.end();
-}
-
-ExecutablesList::const_iterator ExecutablesList::end() const
-{
- return m_Executables.end();
-}
-
-std::size_t ExecutablesList::size() const
-{
- return m_Executables.size();
-}
-
-bool ExecutablesList::empty() const
-{
- return m_Executables.empty();
-}
-
-void ExecutablesList::load(const MOBase::IPluginGame* game, const Settings& s)
-{
- log::debug("loading executables");
-
- m_Executables.clear();
-
- // whether the executable list in the .ini is still using the old custom
- // executables from 2.2.0, see upgradeFromCustom()
- bool needsUpgrade = false;
-
- for (auto& map : s.executables()) {
- Executable::Flags flags;
-
- if (map["toolbar"].toBool())
- flags |= Executable::ShowInToolbar;
-
- if (map["ownicon"].toBool())
- flags |= Executable::UseApplicationIcon;
-
- if (map["hide"].toBool())
- flags |= Executable::Hide;
-
- if (map.contains("custom")) {
- // the "custom" setting only exists in older versions
- needsUpgrade = true;
- }
-
- setExecutable(Executable()
- .title(map["title"].toString())
- .binaryInfo(QFileInfo(map["binary"].toString()))
- .arguments(map["arguments"].toString())
- .steamAppID(map["steamAppID"].toString())
- .workingDirectory(map["workingDirectory"].toString())
- .flags(flags));
- }
-
- addFromPlugin(game, IgnoreExisting);
-
- if (needsUpgrade)
- upgradeFromCustom(game);
-
- dump();
-}
-
-void ExecutablesList::store(Settings& s)
-{
- std::vector<std::map<QString, QVariant>> v;
-
- for (const auto& item : *this) {
- std::map<QString, QVariant> map;
-
- map["title"] = item.title();
- map["toolbar"] = item.isShownOnToolbar();
- map["ownicon"] = item.usesOwnIcon();
- map["hide"] = item.hide();
- map["binary"] = item.binaryInfo().filePath();
- map["arguments"] = item.arguments();
- map["workingDirectory"] = item.workingDirectory();
- map["steamAppID"] = item.steamAppID();
-
- v.push_back(std::move(map));
- }
-
- s.setExecutables(v);
-}
-
-std::vector<Executable> ExecutablesList::getPluginExecutables(
- MOBase::IPluginGame const *game) const
-{
- Q_ASSERT(game != nullptr);
-
- std::vector<Executable> v;
-
- for (const ExecutableInfo &info : game->executables()) {
- if (!info.isValid()) {
- continue;
- }
-
- v.push_back({info, Executable::UseApplicationIcon});
- }
-
- const QFileInfo eppBin(QCoreApplication::applicationDirPath() + "/explorer++/Explorer++.exe");
-
- if (eppBin.exists()) {
- const auto args = QString("\"%1\"")
- .arg(QDir::toNativeSeparators(game->dataDirectory().absolutePath()));
-
- const auto exe = Executable()
- .title("Explore Virtual Folder")
- .binaryInfo(eppBin)
- .arguments(args)
- .workingDirectory(eppBin.absolutePath())
- .flags(Executable::UseApplicationIcon);
-
- v.push_back(exe);
- }
-
- return v;
-}
-
-void ExecutablesList::resetFromPlugin(MOBase::IPluginGame const *game)
-{
- log::debug("resetting plugin executables");
-
- Q_ASSERT(game != nullptr);
-
- for (const auto& exe : getPluginExecutables(game)) {
- setExecutable(exe, MoveExisting);
- }
-}
-
-void ExecutablesList::addFromPlugin(IPluginGame const *game, SetFlags flags)
-{
- Q_ASSERT(game != nullptr);
-
- for (const auto& exe : getPluginExecutables(game)) {
- setExecutable(exe, flags);
- }
-}
-
-const Executable &ExecutablesList::get(const QString &title) const
-{
- for (const auto& exe : m_Executables) {
- if (exe.title() == title) {
- return exe;
- }
- }
-
- throw std::runtime_error(QString("executable not found: %1").arg(title).toLocal8Bit().constData());
-}
-
-Executable &ExecutablesList::get(const QString &title)
-{
- return const_cast<Executable&>(std::as_const(*this).get(title));
-}
-
-Executable &ExecutablesList::getByBinary(const QFileInfo &info)
-{
- for (Executable &exe : m_Executables) {
- if (exe.binaryInfo() == info) {
- return exe;
- }
- }
- throw std::runtime_error("invalid info");
-}
-
-ExecutablesList::iterator ExecutablesList::find(const QString &title, bool ci)
-{
- const auto cif = ci ? Qt::CaseInsensitive : Qt::CaseSensitive;
-
- return std::find_if(begin(), end(), [&](auto&& e) {
- return (e.title().compare(title, cif) == 0);
- });
-}
-
-ExecutablesList::const_iterator ExecutablesList::find(const QString &title, bool ci) const
-{
- const auto cif = ci ? Qt::CaseInsensitive : Qt::CaseSensitive;
-
- return std::find_if(begin(), end(), [&](auto&& e) {
- return (e.title().compare(title, cif) == 0);
- });
-}
-
-bool ExecutablesList::titleExists(const QString &title) const
-{
- auto test = [&] (const Executable &exe) { return exe.title() == title; };
- return std::find_if(m_Executables.begin(), m_Executables.end(), test) != m_Executables.end();
-}
-
-void ExecutablesList::setExecutable(const Executable &exe)
-{
- setExecutable(exe, MergeExisting);
-}
-
-void ExecutablesList::setExecutable(const Executable &exe, SetFlags flags)
-{
- auto itor = find(exe.title());
-
- if (itor != end()) {
- if (flags == IgnoreExisting) {
- return;
- }
-
- if (flags == MoveExisting) {
- const auto newTitle = makeNonConflictingTitle(exe.title());
- if (!newTitle) {
- log::error(
- "executable '{}' was in the way but could not be renamed",
- exe.title());
-
- return;
- }
-
- log::warn(
- "executable '{}' was in the way and was renamed to '{}'",
- itor->title(), *newTitle);
-
- itor->title(*newTitle);
- itor = end();
- }
- }
-
- if (itor == m_Executables.end()) {
- m_Executables.push_back(exe);
- } else {
- itor->mergeFrom(exe);
- }
-}
-
-void ExecutablesList::remove(const QString &title)
-{
- auto itor = find(title);
- if (itor != m_Executables.end()) {
- m_Executables.erase(itor);
- }
-}
-
-std::optional<QString> ExecutablesList::makeNonConflictingTitle(
- const QString& prefix)
-{
- const int max = 100;
-
- QString title = prefix;
-
- for (int i=1; i<max; ++i) {
- if (!titleExists(title)) {
- return title;
- }
-
- title = prefix + QString(" (%1)").arg(i);
- }
-
- log::error("ran out of executable titles for prefix '{}'", prefix);
- return {};
-}
-
-void ExecutablesList::upgradeFromCustom(MOBase::IPluginGame const *game)
-{
- log::debug("upgrading executables list");
-
- Q_ASSERT(game != nullptr);
-
- // prior to 2.2.1, plugin executables were special in the sense that they
- // did not store certain settings, like the binary info and working directory;
- // those were filled in when MO started, but never saved
- //
- // this interferes with the new executables list, which is completely
- // customizable, because plugin executables are only added to the list when
- // they don't exist at all and are ignored otherwise, leaving some of the
- // fields completely blank
- //
- // when the "custom" setting is found in the .ini file (see load()), it is
- // assumed that the older scheme is still present; in that case, the plugin
- // executables force their binary info and working directory into the existing
- // executables one last time
- //
- // from that point on, plugin executables are ignored unless they're
- // completely missing from the list, allowing users to customize them as they
- // want
-
- for (const auto& exe : getPluginExecutables(game)) {
- auto itor = find(exe.title());
- if (itor == end()){
- continue;
- }
-
- if (!itor->binaryInfo().exists()) {
- itor->binaryInfo(exe.binaryInfo());
- }
-
- if (itor->workingDirectory().isEmpty()) {
- itor->workingDirectory(exe.workingDirectory());
- }
- }
-}
-
-void ExecutablesList::dump() const
-{
- for (const auto& e : m_Executables) {
- QStringList flags;
-
- if (e.flags() & Executable::ShowInToolbar) {
- flags.push_back("toolbar");
- }
-
- if (e.flags() & Executable::UseApplicationIcon) {
- flags.push_back("icon");
- }
-
- if (e.flags() & Executable::Hide) {
- flags.push_back("hide");
- }
-
- log::debug(
- " . executable '{}'\n"
- " binary: {}\n"
- " arguments: {}\n"
- " steam ID: {}\n"
- " directory: {}\n"
- " flags: {} ({})",
- e.title(), e.binaryInfo().filePath(), e.arguments(),
- e.steamAppID(), e.workingDirectory(), flags.join("|"), e.flags());
- }
-}
-
-
-Executable::Executable(QString title)
- : m_title(title)
-{
-}
-
-Executable::Executable(const MOBase::ExecutableInfo& info, Flags flags) :
- m_title(info.title()),
- m_binaryInfo(info.binary()),
- m_arguments(info.arguments().join(" ")),
- m_steamAppID(info.steamAppID()),
- m_workingDirectory(info.workingDirectory().path()),
- m_flags(flags)
-{
-}
-
-const QString& Executable::title() const
-{
- return m_title;
-}
-
-const QFileInfo& Executable::binaryInfo() const
-{
- return m_binaryInfo;
-}
-
-const QString& Executable::arguments() const
-{
- return m_arguments;
-}
-
-const QString& Executable::steamAppID() const
-{
- return m_steamAppID;
-}
-
-const QString& Executable::workingDirectory() const
-{
- return m_workingDirectory;
-}
-
-Executable::Flags Executable::flags() const
-{
- return m_flags;
-}
-
-Executable& Executable::title(const QString& s)
-{
- m_title = s;
- return *this;
-}
-
-Executable& Executable::binaryInfo(const QFileInfo& fi)
-{
- m_binaryInfo = fi;
- return *this;
-}
-
-Executable& Executable::arguments(const QString& s)
-{
- m_arguments = s;
- return *this;
-}
-
-Executable& Executable::steamAppID(const QString& s)
-{
- m_steamAppID = s;
- return *this;
-}
-
-Executable& Executable::workingDirectory(const QString& s)
-{
- m_workingDirectory = s;
- return *this;
-}
-
-Executable& Executable::flags(Flags f)
-{
- m_flags = f;
- return *this;
-}
-
-bool Executable::isShownOnToolbar() const
-{
- return m_flags.testFlag(ShowInToolbar);
-}
-
-void Executable::setShownOnToolbar(bool state)
-{
- if (state) {
- m_flags |= ShowInToolbar;
- } else {
- m_flags &= ~ShowInToolbar;
- }
-}
-
-bool Executable::usesOwnIcon() const
-{
- return m_flags.testFlag(UseApplicationIcon);
-}
-
-bool Executable::hide() const
-{
- return m_flags.testFlag(Hide);
-}
-
-void Executable::mergeFrom(const Executable& other)
-{
- // this happens after executables are loaded from settings and plugin
- // executables are being added, or when users are modifying executables
-
- m_title = other.title();
- m_binaryInfo = other.binaryInfo();
- m_arguments = other.arguments();
- m_steamAppID = other.steamAppID();
- m_workingDirectory = other.workingDirectory();
- m_flags = other.flags();
-}
+/*
+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 "executableslist.h"
+
+#include "iplugingame.h"
+#include "utility.h"
+#include "settings.h"
+#include <log.h>
+
+#include <QFileInfo>
+#include <QDir>
+#include <QDebug>
+#include <QCoreApplication>
+
+
+#include <algorithm>
+
+
+using namespace MOBase;
+
+ExecutablesList::iterator ExecutablesList::begin()
+{
+ return m_Executables.begin();
+}
+
+ExecutablesList::const_iterator ExecutablesList::begin() const
+{
+ return m_Executables.begin();
+}
+
+ExecutablesList::iterator ExecutablesList::end()
+{
+ return m_Executables.end();
+}
+
+ExecutablesList::const_iterator ExecutablesList::end() const
+{
+ return m_Executables.end();
+}
+
+std::size_t ExecutablesList::size() const
+{
+ return m_Executables.size();
+}
+
+bool ExecutablesList::empty() const
+{
+ return m_Executables.empty();
+}
+
+void ExecutablesList::load(const MOBase::IPluginGame* game, const Settings& s)
+{
+ log::debug("loading executables");
+
+ m_Executables.clear();
+
+ // whether the executable list in the .ini is still using the old custom
+ // executables from 2.2.0, see upgradeFromCustom()
+ bool needsUpgrade = false;
+
+ for (auto& map : s.executables()) {
+ Executable::Flags flags;
+
+ if (map["toolbar"].toBool())
+ flags |= Executable::ShowInToolbar;
+
+ if (map["ownicon"].toBool())
+ flags |= Executable::UseApplicationIcon;
+
+ if (map["hide"].toBool())
+ flags |= Executable::Hide;
+
+ if (map.contains("custom")) {
+ // the "custom" setting only exists in older versions
+ needsUpgrade = true;
+ }
+
+ setExecutable(Executable()
+ .title(map["title"].toString())
+ .binaryInfo(QFileInfo(map["binary"].toString()))
+ .arguments(map["arguments"].toString())
+ .steamAppID(map["steamAppID"].toString())
+ .workingDirectory(map["workingDirectory"].toString())
+ .flags(flags));
+ }
+
+ addFromPlugin(game, IgnoreExisting);
+
+ if (needsUpgrade)
+ upgradeFromCustom(game);
+
+ dump();
+}
+
+void ExecutablesList::store(Settings& s)
+{
+ std::vector<std::map<QString, QVariant>> v;
+
+ for (const auto& item : *this) {
+ std::map<QString, QVariant> map;
+
+ map["title"] = item.title();
+ map["toolbar"] = item.isShownOnToolbar();
+ map["ownicon"] = item.usesOwnIcon();
+ map["hide"] = item.hide();
+ map["binary"] = item.binaryInfo().filePath();
+ map["arguments"] = item.arguments();
+ map["workingDirectory"] = item.workingDirectory();
+ map["steamAppID"] = item.steamAppID();
+
+ v.push_back(std::move(map));
+ }
+
+ s.setExecutables(v);
+}
+
+std::vector<Executable> ExecutablesList::getPluginExecutables(
+ MOBase::IPluginGame const *game) const
+{
+ Q_ASSERT(game != nullptr);
+
+ std::vector<Executable> v;
+
+ for (const ExecutableInfo &info : game->executables()) {
+ if (!info.isValid()) {
+ continue;
+ }
+
+ v.push_back({info, Executable::UseApplicationIcon});
+ }
+
+ const QFileInfo eppBin(QCoreApplication::applicationDirPath() + "/explorer++/Explorer++.exe");
+
+ if (eppBin.exists()) {
+ const auto args = QString("\"%1\"")
+ .arg(QDir::toNativeSeparators(game->dataDirectory().absolutePath()));
+
+ const auto exe = Executable()
+ .title("Explore Virtual Folder")
+ .binaryInfo(eppBin)
+ .arguments(args)
+ .workingDirectory(eppBin.absolutePath())
+ .flags(Executable::UseApplicationIcon);
+
+ v.push_back(exe);
+ }
+
+ return v;
+}
+
+void ExecutablesList::resetFromPlugin(MOBase::IPluginGame const *game)
+{
+ log::debug("resetting plugin executables");
+
+ Q_ASSERT(game != nullptr);
+
+ for (const auto& exe : getPluginExecutables(game)) {
+ setExecutable(exe, MoveExisting);
+ }
+}
+
+void ExecutablesList::addFromPlugin(IPluginGame const *game, SetFlags flags)
+{
+ Q_ASSERT(game != nullptr);
+
+ for (const auto& exe : getPluginExecutables(game)) {
+ setExecutable(exe, flags);
+ }
+}
+
+const Executable &ExecutablesList::get(const QString &title) const
+{
+ for (const auto& exe : m_Executables) {
+ if (exe.title() == title) {
+ return exe;
+ }
+ }
+
+ throw std::runtime_error(QString("executable not found: %1").arg(title).toLocal8Bit().constData());
+}
+
+Executable &ExecutablesList::get(const QString &title)
+{
+ return const_cast<Executable&>(std::as_const(*this).get(title));
+}
+
+Executable &ExecutablesList::getByBinary(const QFileInfo &info)
+{
+ for (Executable &exe : m_Executables) {
+ if (exe.binaryInfo() == info) {
+ return exe;
+ }
+ }
+ throw std::runtime_error("invalid info");
+}
+
+ExecutablesList::iterator ExecutablesList::find(const QString &title, bool ci)
+{
+ const auto cif = ci ? Qt::CaseInsensitive : Qt::CaseSensitive;
+
+ return std::find_if(begin(), end(), [&](auto&& e) {
+ return (e.title().compare(title, cif) == 0);
+ });
+}
+
+ExecutablesList::const_iterator ExecutablesList::find(const QString &title, bool ci) const
+{
+ const auto cif = ci ? Qt::CaseInsensitive : Qt::CaseSensitive;
+
+ return std::find_if(begin(), end(), [&](auto&& e) {
+ return (e.title().compare(title, cif) == 0);
+ });
+}
+
+bool ExecutablesList::titleExists(const QString &title) const
+{
+ auto test = [&] (const Executable &exe) { return exe.title() == title; };
+ return std::find_if(m_Executables.begin(), m_Executables.end(), test) != m_Executables.end();
+}
+
+void ExecutablesList::setExecutable(const Executable &exe)
+{
+ setExecutable(exe, MergeExisting);
+}
+
+void ExecutablesList::setExecutable(const Executable &exe, SetFlags flags)
+{
+ auto itor = find(exe.title());
+
+ if (itor != end()) {
+ if (flags == IgnoreExisting) {
+ return;
+ }
+
+ if (flags == MoveExisting) {
+ const auto newTitle = makeNonConflictingTitle(exe.title());
+ if (!newTitle) {
+ log::error(
+ "executable '{}' was in the way but could not be renamed",
+ exe.title());
+
+ return;
+ }
+
+ log::warn(
+ "executable '{}' was in the way and was renamed to '{}'",
+ itor->title(), *newTitle);
+
+ itor->title(*newTitle);
+ itor = end();
+ }
+ }
+
+ if (itor == m_Executables.end()) {
+ m_Executables.push_back(exe);
+ } else {
+ itor->mergeFrom(exe);
+ }
+}
+
+void ExecutablesList::remove(const QString &title)
+{
+ auto itor = find(title);
+ if (itor != m_Executables.end()) {
+ m_Executables.erase(itor);
+ }
+}
+
+std::optional<QString> ExecutablesList::makeNonConflictingTitle(
+ const QString& prefix)
+{
+ const int max = 100;
+
+ QString title = prefix;
+
+ for (int i=1; i<max; ++i) {
+ if (!titleExists(title)) {
+ return title;
+ }
+
+ title = prefix + QString(" (%1)").arg(i);
+ }
+
+ log::error("ran out of executable titles for prefix '{}'", prefix);
+ return {};
+}
+
+void ExecutablesList::upgradeFromCustom(MOBase::IPluginGame const *game)
+{
+ log::debug("upgrading executables list");
+
+ Q_ASSERT(game != nullptr);
+
+ // prior to 2.2.1, plugin executables were special in the sense that they
+ // did not store certain settings, like the binary info and working directory;
+ // those were filled in when MO started, but never saved
+ //
+ // this interferes with the new executables list, which is completely
+ // customizable, because plugin executables are only added to the list when
+ // they don't exist at all and are ignored otherwise, leaving some of the
+ // fields completely blank
+ //
+ // when the "custom" setting is found in the .ini file (see load()), it is
+ // assumed that the older scheme is still present; in that case, the plugin
+ // executables force their binary info and working directory into the existing
+ // executables one last time
+ //
+ // from that point on, plugin executables are ignored unless they're
+ // completely missing from the list, allowing users to customize them as they
+ // want
+
+ for (const auto& exe : getPluginExecutables(game)) {
+ auto itor = find(exe.title());
+ if (itor == end()){
+ continue;
+ }
+
+ if (!itor->binaryInfo().exists()) {
+ itor->binaryInfo(exe.binaryInfo());
+ }
+
+ if (itor->workingDirectory().isEmpty()) {
+ itor->workingDirectory(exe.workingDirectory());
+ }
+ }
+}
+
+void ExecutablesList::dump() const
+{
+ for (const auto& e : m_Executables) {
+ QStringList flags;
+
+ if (e.flags() & Executable::ShowInToolbar) {
+ flags.push_back("toolbar");
+ }
+
+ if (e.flags() & Executable::UseApplicationIcon) {
+ flags.push_back("icon");
+ }
+
+ if (e.flags() & Executable::Hide) {
+ flags.push_back("hide");
+ }
+
+ log::debug(
+ " . executable '{}'\n"
+ " binary: {}\n"
+ " arguments: {}\n"
+ " steam ID: {}\n"
+ " directory: {}\n"
+ " flags: {} ({})",
+ e.title(), e.binaryInfo().filePath(), e.arguments(),
+ e.steamAppID(), e.workingDirectory(), flags.join("|"), e.flags());
+ }
+}
+
+
+Executable::Executable(QString title)
+ : m_title(title)
+{
+}
+
+Executable::Executable(const MOBase::ExecutableInfo& info, Flags flags) :
+ m_title(info.title()),
+ m_binaryInfo(info.binary()),
+ m_arguments(info.arguments().join(" ")),
+ m_steamAppID(info.steamAppID()),
+ m_workingDirectory(info.workingDirectory().path()),
+ m_flags(flags)
+{
+}
+
+const QString& Executable::title() const
+{
+ return m_title;
+}
+
+const QFileInfo& Executable::binaryInfo() const
+{
+ return m_binaryInfo;
+}
+
+const QString& Executable::arguments() const
+{
+ return m_arguments;
+}
+
+const QString& Executable::steamAppID() const
+{
+ return m_steamAppID;
+}
+
+const QString& Executable::workingDirectory() const
+{
+ return m_workingDirectory;
+}
+
+Executable::Flags Executable::flags() const
+{
+ return m_flags;
+}
+
+Executable& Executable::title(const QString& s)
+{
+ m_title = s;
+ return *this;
+}
+
+Executable& Executable::binaryInfo(const QFileInfo& fi)
+{
+ m_binaryInfo = fi;
+ return *this;
+}
+
+Executable& Executable::arguments(const QString& s)
+{
+ m_arguments = s;
+ return *this;
+}
+
+Executable& Executable::steamAppID(const QString& s)
+{
+ m_steamAppID = s;
+ return *this;
+}
+
+Executable& Executable::workingDirectory(const QString& s)
+{
+ m_workingDirectory = s;
+ return *this;
+}
+
+Executable& Executable::flags(Flags f)
+{
+ m_flags = f;
+ return *this;
+}
+
+bool Executable::isShownOnToolbar() const
+{
+ return m_flags.testFlag(ShowInToolbar);
+}
+
+void Executable::setShownOnToolbar(bool state)
+{
+ if (state) {
+ m_flags |= ShowInToolbar;
+ } else {
+ m_flags &= ~ShowInToolbar;
+ }
+}
+
+bool Executable::usesOwnIcon() const
+{
+ return m_flags.testFlag(UseApplicationIcon);
+}
+
+bool Executable::hide() const
+{
+ return m_flags.testFlag(Hide);
+}
+
+void Executable::mergeFrom(const Executable& other)
+{
+ // this happens after executables are loaded from settings and plugin
+ // executables are being added, or when users are modifying executables
+
+ m_title = other.title();
+ m_binaryInfo = other.binaryInfo();
+ m_arguments = other.arguments();
+ m_steamAppID = other.steamAppID();
+ m_workingDirectory = other.workingDirectory();
+ m_flags = other.flags();
+}
diff --git a/src/executableslist.h b/src/executableslist.h
index 97d39500..b8016c72 100644
--- a/src/executableslist.h
+++ b/src/executableslist.h
@@ -1,228 +1,228 @@
-/*
-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 EXECUTABLESLIST_H
-#define EXECUTABLESLIST_H
-
-#include "executableinfo.h"
-
-#include <vector>
-#include <optional>
-
-#include <QFileInfo>
-#include <QMetaType>
-
-namespace MOBase { class IPluginGame; class ExecutableInfo; }
-class Settings;
-
-/*!
- * @brief Information about an executable
- **/
-class Executable
-{
-public:
- enum Flag
- {
- ShowInToolbar = 0x02,
- UseApplicationIcon = 0x04,
- Hide = 0x08
- };
-
- Q_DECLARE_FLAGS(Flags, Flag);
-
- Executable(QString title={});
-
- /**
- * @brief Executable from plugin
- */
- Executable(const MOBase::ExecutableInfo& info, Flags flags);
-
- const QString& title() const;
- const QFileInfo& binaryInfo() const;
- const QString& arguments() const;
- const QString& steamAppID() const;
- const QString& workingDirectory() const;
- Flags flags() const;
-
- Executable& title(const QString& s);
- Executable& binaryInfo(const QFileInfo& fi);
- Executable& arguments(const QString& s);
- Executable& steamAppID(const QString& s);
- Executable& workingDirectory(const QString& s);
- Executable& flags(Flags f);
-
- bool isShownOnToolbar() const;
- void setShownOnToolbar(bool state);
- bool usesOwnIcon() const;
- bool hide() const;
-
- void mergeFrom(const Executable& other);
-
-private:
- QString m_title;
- QFileInfo m_binaryInfo;
- QString m_arguments;
- QString m_steamAppID;
- QString m_workingDirectory;
- Flags m_flags;
-};
-
-
-/*!
- * @brief List of executables configured to by started from MO
- **/
-class ExecutablesList {
-public:
- using vector_type = std::vector<Executable>;
- using iterator = vector_type::iterator;
- using const_iterator = vector_type::const_iterator;
-
- /**
- * standard container interface
- */
- iterator begin();
- const_iterator begin() const;
- iterator end();
- const_iterator end() const;
- std::size_t size() const;
- bool empty() const;
-
- /**
- * @brief initializes the list from the settings and the given plugin
- **/
- void load(const MOBase::IPluginGame* game, const Settings& settings);
-
- /**
- * @brief re-adds all the executables from the plugin and renames existing
- * executables that are in the way
- **/
- void resetFromPlugin(MOBase::IPluginGame const *game);
-
- /**
- * @brief writes the current list to the settings
- */
- void store(Settings& settings);
-
- /**
- * @brief get an executable by name
- *
- * @param title the title of the executable to look up
- * @return the executable
- * @exception runtime_error will throw an exception if the executable is not found
- **/
- const Executable &get(const QString &title) const;
-
- /**
- * @brief find an executable by its name
- *
- * @param title the title of the executable to look up
- * @return the executable
- * @exception runtime_error will throw an exception if the name is not correct
- **/
- Executable &get(const QString &title);
-
- /**
- * @brief find an executable by a fileinfo structure
- * @param info the info object to search for
- * @return the executable
- * @exception runtime_error will throw an exception if the name is not correct
- */
- Executable &getByBinary(const QFileInfo &info);
-
- /**
- * @brief returns an iterator for the given executable by title, or end()
- */
- iterator find(const QString &title, bool caseSensitive=true);
- const_iterator find(const QString &title, bool caseSensitive=true) const;
-
- /**
- * @brief determine if an executable exists
- * @param title the title of the executable to look up
- * @return true if the executable exists, false otherwise
- **/
- bool titleExists(const QString &title) const;
-
- /**
- * @brief add a new executable to the list
- * @param executable
- */
- void setExecutable(const Executable &executable);
-
- /**
- * @brief remove the executable with the specified file name. This needs to
- * be an absolute file path
- *
- * @param title title of the executable to remove
- * @note if the executable name is invalid, nothing happens. There is no way
- * to determine if this was successful
- **/
- void remove(const QString &title);
-
- /**
- * returns a title that starts with the given prefix and does not clash with
- * an existing executable, may fail
- */
- std::optional<QString> makeNonConflictingTitle(const QString& prefix);
-
-private:
- enum SetFlags
- {
- // executables having the same name as existing ones are ignored
- IgnoreExisting = 1,
-
- // executables having the same name are merged
- MergeExisting,
-
- // an existing executable with the same name is renamed
- MoveExisting
- };
-
- std::vector<Executable> m_Executables;
-
-
- /**
- * @brief add the executables preconfigured for this game
- **/
- void addFromPlugin(MOBase::IPluginGame const *game, SetFlags flags);
-
- /**
- * @brief add a new executable to the list
- * @param executable
- */
- void setExecutable(const Executable &exe, SetFlags flags);
-
- /**
- * returns the executables provided by the game plugin
- **/
- std::vector<Executable> getPluginExecutables(
- MOBase::IPluginGame const *game) const;
-
- /**
- * called when MO is still using the old custom executables from 2.2.0
- **/
- void upgradeFromCustom(const MOBase::IPluginGame* game);
-
- // logs all executables
- //
- void dump() const;
-};
-
-Q_DECLARE_OPERATORS_FOR_FLAGS(Executable::Flags)
-
-#endif // EXECUTABLESLIST_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 EXECUTABLESLIST_H
+#define EXECUTABLESLIST_H
+
+#include "executableinfo.h"
+
+#include <vector>
+#include <optional>
+
+#include <QFileInfo>
+#include <QMetaType>
+
+namespace MOBase { class IPluginGame; class ExecutableInfo; }
+class Settings;
+
+/*!
+ * @brief Information about an executable
+ **/
+class Executable
+{
+public:
+ enum Flag
+ {
+ ShowInToolbar = 0x02,
+ UseApplicationIcon = 0x04,
+ Hide = 0x08
+ };
+
+ Q_DECLARE_FLAGS(Flags, Flag);
+
+ Executable(QString title={});
+
+ /**
+ * @brief Executable from plugin
+ */
+ Executable(const MOBase::ExecutableInfo& info, Flags flags);
+
+ const QString& title() const;
+ const QFileInfo& binaryInfo() const;
+ const QString& arguments() const;
+ const QString& steamAppID() const;
+ const QString& workingDirectory() const;
+ Flags flags() const;
+
+ Executable& title(const QString& s);
+ Executable& binaryInfo(const QFileInfo& fi);
+ Executable& arguments(const QString& s);
+ Executable& steamAppID(const QString& s);
+ Executable& workingDirectory(const QString& s);
+ Executable& flags(Flags f);
+
+ bool isShownOnToolbar() const;
+ void setShownOnToolbar(bool state);
+ bool usesOwnIcon() const;
+ bool hide() const;
+
+ void mergeFrom(const Executable& other);
+
+private:
+ QString m_title;
+ QFileInfo m_binaryInfo;
+ QString m_arguments;
+ QString m_steamAppID;
+ QString m_workingDirectory;
+ Flags m_flags;
+};
+
+
+/*!
+ * @brief List of executables configured to by started from MO
+ **/
+class ExecutablesList {
+public:
+ using vector_type = std::vector<Executable>;
+ using iterator = vector_type::iterator;
+ using const_iterator = vector_type::const_iterator;
+
+ /**
+ * standard container interface
+ */
+ iterator begin();
+ const_iterator begin() const;
+ iterator end();
+ const_iterator end() const;
+ std::size_t size() const;
+ bool empty() const;
+
+ /**
+ * @brief initializes the list from the settings and the given plugin
+ **/
+ void load(const MOBase::IPluginGame* game, const Settings& settings);
+
+ /**
+ * @brief re-adds all the executables from the plugin and renames existing
+ * executables that are in the way
+ **/
+ void resetFromPlugin(MOBase::IPluginGame const *game);
+
+ /**
+ * @brief writes the current list to the settings
+ */
+ void store(Settings& settings);
+
+ /**
+ * @brief get an executable by name
+ *
+ * @param title the title of the executable to look up
+ * @return the executable
+ * @exception runtime_error will throw an exception if the executable is not found
+ **/
+ const Executable &get(const QString &title) const;
+
+ /**
+ * @brief find an executable by its name
+ *
+ * @param title the title of the executable to look up
+ * @return the executable
+ * @exception runtime_error will throw an exception if the name is not correct
+ **/
+ Executable &get(const QString &title);
+
+ /**
+ * @brief find an executable by a fileinfo structure
+ * @param info the info object to search for
+ * @return the executable
+ * @exception runtime_error will throw an exception if the name is not correct
+ */
+ Executable &getByBinary(const QFileInfo &info);
+
+ /**
+ * @brief returns an iterator for the given executable by title, or end()
+ */
+ iterator find(const QString &title, bool caseSensitive=true);
+ const_iterator find(const QString &title, bool caseSensitive=true) const;
+
+ /**
+ * @brief determine if an executable exists
+ * @param title the title of the executable to look up
+ * @return true if the executable exists, false otherwise
+ **/
+ bool titleExists(const QString &title) const;
+
+ /**
+ * @brief add a new executable to the list
+ * @param executable
+ */
+ void setExecutable(const Executable &executable);
+
+ /**
+ * @brief remove the executable with the specified file name. This needs to
+ * be an absolute file path
+ *
+ * @param title title of the executable to remove
+ * @note if the executable name is invalid, nothing happens. There is no way
+ * to determine if this was successful
+ **/
+ void remove(const QString &title);
+
+ /**
+ * returns a title that starts with the given prefix and does not clash with
+ * an existing executable, may fail
+ */
+ std::optional<QString> makeNonConflictingTitle(const QString& prefix);
+
+private:
+ enum SetFlags
+ {
+ // executables having the same name as existing ones are ignored
+ IgnoreExisting = 1,
+
+ // executables having the same name are merged
+ MergeExisting,
+
+ // an existing executable with the same name is renamed
+ MoveExisting
+ };
+
+ std::vector<Executable> m_Executables;
+
+
+ /**
+ * @brief add the executables preconfigured for this game
+ **/
+ void addFromPlugin(MOBase::IPluginGame const *game, SetFlags flags);
+
+ /**
+ * @brief add a new executable to the list
+ * @param executable
+ */
+ void setExecutable(const Executable &exe, SetFlags flags);
+
+ /**
+ * returns the executables provided by the game plugin
+ **/
+ std::vector<Executable> getPluginExecutables(
+ MOBase::IPluginGame const *game) const;
+
+ /**
+ * called when MO is still using the old custom executables from 2.2.0
+ **/
+ void upgradeFromCustom(const MOBase::IPluginGame* game);
+
+ // logs all executables
+ //
+ void dump() const;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(Executable::Flags)
+
+#endif // EXECUTABLESLIST_H
diff --git a/src/filedialogmemory.cpp b/src/filedialogmemory.cpp
index 8cfeb6b5..62c29a3f 100644
--- a/src/filedialogmemory.cpp
+++ b/src/filedialogmemory.cpp
@@ -1,82 +1,82 @@
-/*
-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 "filedialogmemory.h"
-#include "settings.h"
-#include <QFileDialog>
-
-static std::map<QString, QString> g_Cache;
-
-void FileDialogMemory::save(Settings& s)
-{
- s.paths().setRecent(g_Cache);
-}
-
-void FileDialogMemory::restore(const Settings& s)
-{
- g_Cache = s.paths().recent();
-}
-
-QString FileDialogMemory::getOpenFileName(
- const QString &dirID, QWidget *parent, const QString &caption,
- const QString &dir, const QString &filter, QString *selectedFilter,
- QFileDialog::Options options)
-{
- QString currentDir = dir;
-
- if (currentDir.isEmpty()) {
- auto itor = g_Cache.find(dirID);
- if (itor != g_Cache.end()) {
- currentDir = itor->second;
- }
- }
-
- QString result = QFileDialog::getOpenFileName(
- parent, caption, currentDir, filter, selectedFilter, options);
-
- if (!result.isNull()) {
- g_Cache[dirID] = QFileInfo(result).path();
- }
-
- return result;
-}
-
-
-QString FileDialogMemory::getExistingDirectory(
- const QString &dirID, QWidget *parent, const QString &caption,
- const QString &dir, QFileDialog::Options options)
-{
- QString currentDir = dir;
-
- if (currentDir.isEmpty()) {
- auto itor = g_Cache.find(dirID);
- if (itor != g_Cache.end()) {
- currentDir = itor->second;
- }
- }
-
- QString result = QFileDialog::getExistingDirectory(
- parent, caption, currentDir, options);
-
- if (!result.isNull()) {
- g_Cache[dirID] = result;
- }
-
- return result;
-}
+/*
+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 "filedialogmemory.h"
+#include "settings.h"
+#include <QFileDialog>
+
+static std::map<QString, QString> g_Cache;
+
+void FileDialogMemory::save(Settings& s)
+{
+ s.paths().setRecent(g_Cache);
+}
+
+void FileDialogMemory::restore(const Settings& s)
+{
+ g_Cache = s.paths().recent();
+}
+
+QString FileDialogMemory::getOpenFileName(
+ const QString &dirID, QWidget *parent, const QString &caption,
+ const QString &dir, const QString &filter, QString *selectedFilter,
+ QFileDialog::Options options)
+{
+ QString currentDir = dir;
+
+ if (currentDir.isEmpty()) {
+ auto itor = g_Cache.find(dirID);
+ if (itor != g_Cache.end()) {
+ currentDir = itor->second;
+ }
+ }
+
+ QString result = QFileDialog::getOpenFileName(
+ parent, caption, currentDir, filter, selectedFilter, options);
+
+ if (!result.isNull()) {
+ g_Cache[dirID] = QFileInfo(result).path();
+ }
+
+ return result;
+}
+
+
+QString FileDialogMemory::getExistingDirectory(
+ const QString &dirID, QWidget *parent, const QString &caption,
+ const QString &dir, QFileDialog::Options options)
+{
+ QString currentDir = dir;
+
+ if (currentDir.isEmpty()) {
+ auto itor = g_Cache.find(dirID);
+ if (itor != g_Cache.end()) {
+ currentDir = itor->second;
+ }
+ }
+
+ QString result = QFileDialog::getExistingDirectory(
+ parent, caption, currentDir, options);
+
+ if (!result.isNull()) {
+ g_Cache[dirID] = result;
+ }
+
+ return result;
+}
diff --git a/src/filedialogmemory.h b/src/filedialogmemory.h
index 11b156ed..995d5640 100644
--- a/src/filedialogmemory.h
+++ b/src/filedialogmemory.h
@@ -1,49 +1,49 @@
-/*
-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 FILEDIALOGMEMORY_H
-#define FILEDIALOGMEMORY_H
-
-
-#include <map>
-#include <QString>
-#include <QFileDialog>
-
-class Settings;
-
-class FileDialogMemory
-{
-public:
- FileDialogMemory() = delete;
-
- static void save(Settings& settings);
- static void restore(const Settings& settings);
-
- static QString getOpenFileName(
- const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
- const QString &dir = QString(), const QString &filter = QString(),
- QString *selectedFilter = 0, QFileDialog::Options options = QFileDialog::Option(0));
-
- static QString getExistingDirectory(
- const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
- const QString &dir = QString(),
- QFileDialog::Options options = QFileDialog::ShowDirsOnly);
-};
-
-#endif // FILEDIALOGMEMORY_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 FILEDIALOGMEMORY_H
+#define FILEDIALOGMEMORY_H
+
+
+#include <map>
+#include <QString>
+#include <QFileDialog>
+
+class Settings;
+
+class FileDialogMemory
+{
+public:
+ FileDialogMemory() = delete;
+
+ static void save(Settings& settings);
+ static void restore(const Settings& settings);
+
+ static QString getOpenFileName(
+ const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
+ const QString &dir = QString(), const QString &filter = QString(),
+ QString *selectedFilter = 0, QFileDialog::Options options = QFileDialog::Option(0));
+
+ static QString getExistingDirectory(
+ const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
+ const QString &dir = QString(),
+ QFileDialog::Options options = QFileDialog::ShowDirsOnly);
+};
+
+#endif // FILEDIALOGMEMORY_H
diff --git a/src/genericicondelegate.cpp b/src/genericicondelegate.cpp
index 7afaca3f..bb039b05 100644
--- a/src/genericicondelegate.cpp
+++ b/src/genericicondelegate.cpp
@@ -1,29 +1,29 @@
-#include "genericicondelegate.h"
-#include "pluginlist.h"
-#include <QList>
-#include <QPixmapCache>
-
-
-GenericIconDelegate::GenericIconDelegate(QTreeView* parent, int role, int logicalIndex, int compactSize)
- : IconDelegate(parent, logicalIndex, compactSize)
- , m_Role(role)
-{
-}
-
-QList<QString> GenericIconDelegate::getIcons(const QModelIndex &index) const
-{
- QList<QString> result;
- if (index.isValid()) {
- for (const QVariant &var : index.data(m_Role).toList()) {
- if (!compact() || !var.toString().isEmpty()) {
- result.append(var.toString());
- }
- }
- }
- return result;
-}
-
-size_t GenericIconDelegate::getNumIcons(const QModelIndex &index) const
-{
- return index.data(m_Role).toList().count();
-}
+#include "genericicondelegate.h"
+#include "pluginlist.h"
+#include <QList>
+#include <QPixmapCache>
+
+
+GenericIconDelegate::GenericIconDelegate(QTreeView* parent, int role, int logicalIndex, int compactSize)
+ : IconDelegate(parent, logicalIndex, compactSize)
+ , m_Role(role)
+{
+}
+
+QList<QString> GenericIconDelegate::getIcons(const QModelIndex &index) const
+{
+ QList<QString> result;
+ if (index.isValid()) {
+ for (const QVariant &var : index.data(m_Role).toList()) {
+ if (!compact() || !var.toString().isEmpty()) {
+ result.append(var.toString());
+ }
+ }
+ }
+ return result;
+}
+
+size_t GenericIconDelegate::getNumIcons(const QModelIndex &index) const
+{
+ return index.data(m_Role).toList().count();
+}
diff --git a/src/genericicondelegate.h b/src/genericicondelegate.h
index 52db2bb6..60cdf94b 100644
--- a/src/genericicondelegate.h
+++ b/src/genericicondelegate.h
@@ -1,35 +1,35 @@
-#ifndef GENERICICONDELEGATE_H
-#define GENERICICONDELEGATE_H
-
-#include "icondelegate.h"
-
-/**
- * @brief an icon delegate that takes the list of icons from a user-defines data role
- */
-class GenericIconDelegate : public IconDelegate
-{
-Q_OBJECT
-public:
- /**
- * @brief constructor
- * @param parent parent object
- * @param role role of the itemmodel from which the icon list can be queried (as a QVariantList)
- * @param logicalIndex logical index within the model. This is part of a "hack". Normally "empty" icons will be allocated the same
- * space as a regular icon. This way the model can use empty icons as spacers and thus align same icons horizontally.
- * Now, if you set the logical Index to a valid column and connect the columnResized slot to the sectionResized signal
- * of the view, the delegate will turn off this behaviour if the column is smaller than "compactSize"
- * @param compactSize see explanation of logicalIndex
- */
- GenericIconDelegate(QTreeView* parent, int role = Qt::UserRole + 1, int logicalIndex = -1, int compactSize = 150);
-
-private:
- virtual QList<QString> getIcons(const QModelIndex &index) const;
- virtual size_t getNumIcons(const QModelIndex &index) const;
-private:
- int m_Role;
- int m_LogicalIndex;
- int m_CompactSize;
- bool m_Compact;
-};
-
-#endif // GENERICICONDELEGATE_H
+#ifndef GENERICICONDELEGATE_H
+#define GENERICICONDELEGATE_H
+
+#include "icondelegate.h"
+
+/**
+ * @brief an icon delegate that takes the list of icons from a user-defines data role
+ */
+class GenericIconDelegate : public IconDelegate
+{
+Q_OBJECT
+public:
+ /**
+ * @brief constructor
+ * @param parent parent object
+ * @param role role of the itemmodel from which the icon list can be queried (as a QVariantList)
+ * @param logicalIndex logical index within the model. This is part of a "hack". Normally "empty" icons will be allocated the same
+ * space as a regular icon. This way the model can use empty icons as spacers and thus align same icons horizontally.
+ * Now, if you set the logical Index to a valid column and connect the columnResized slot to the sectionResized signal
+ * of the view, the delegate will turn off this behaviour if the column is smaller than "compactSize"
+ * @param compactSize see explanation of logicalIndex
+ */
+ GenericIconDelegate(QTreeView* parent, int role = Qt::UserRole + 1, int logicalIndex = -1, int compactSize = 150);
+
+private:
+ virtual QList<QString> getIcons(const QModelIndex &index) const;
+ virtual size_t getNumIcons(const QModelIndex &index) const;
+private:
+ int m_Role;
+ int m_LogicalIndex;
+ int m_CompactSize;
+ bool m_Compact;
+};
+
+#endif // GENERICICONDELEGATE_H
diff --git a/src/icondelegate.cpp b/src/icondelegate.cpp
index 7205ba62..23235814 100644
--- a/src/icondelegate.cpp
+++ b/src/icondelegate.cpp
@@ -1,87 +1,87 @@
-/*
-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 "icondelegate.h"
-#include <log.h>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QPainter>
-#include <QDebug>
-#include <QPixmapCache>
-
-using namespace MOBase;
-
-IconDelegate::IconDelegate(QTreeView* view, int column, int compactSize) :
- QStyledItemDelegate(view), m_column(column), m_compactSize(compactSize), m_compact(false)
-{
- if (view) {
- connect(view->header(), &QHeaderView::sectionResized, [=](int column, int, int size) {
- if (column == m_column) {
- m_compact = size < m_compactSize;
- }
- });
- }
-}
-
-void IconDelegate::paintIcons(
- QPainter *painter, const QStyleOptionViewItem &option,
- const QModelIndex &index, const QList<QString>& icons)
-{
- int x = 4;
- painter->save();
-
- int iconWidth = icons.size() > 0 ? ((option.rect.width() / icons.size()) - 4) : 16;
- iconWidth = std::min(16, iconWidth);
-
- const int margin = (option.rect.height() - iconWidth) / 2;
-
- painter->translate(option.rect.topLeft());
- for (const QString &iconId : icons) {
- if (iconId.isEmpty()) {
- x += iconWidth + 4;
- continue;
- }
- QPixmap icon;
- QString fullIconId = QString("%1_%2").arg(iconId).arg(iconWidth);
- if (!QPixmapCache::find(fullIconId, &icon)) {
- icon = QIcon(iconId).pixmap(iconWidth, iconWidth);
- if (icon.isNull()) {
- log::warn("failed to load icon {}", iconId);
- }
- QPixmapCache::insert(fullIconId, icon);
- }
- painter->drawPixmap(x, margin, iconWidth, iconWidth, icon);
- x += iconWidth + 4;
- }
-
- painter->restore();
-}
-
-void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
-{
- if (auto* w = qobject_cast<QAbstractItemView*>(parent())) {
- w->itemDelegate()->paint(painter, option, index);
- }
- else {
- QStyledItemDelegate::paint(painter, option, index);
- }
-
- paintIcons(painter, option, index, getIcons(index));
-}
-
+/*
+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 "icondelegate.h"
+#include <log.h>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPainter>
+#include <QDebug>
+#include <QPixmapCache>
+
+using namespace MOBase;
+
+IconDelegate::IconDelegate(QTreeView* view, int column, int compactSize) :
+ QStyledItemDelegate(view), m_column(column), m_compactSize(compactSize), m_compact(false)
+{
+ if (view) {
+ connect(view->header(), &QHeaderView::sectionResized, [=](int column, int, int size) {
+ if (column == m_column) {
+ m_compact = size < m_compactSize;
+ }
+ });
+ }
+}
+
+void IconDelegate::paintIcons(
+ QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index, const QList<QString>& icons)
+{
+ int x = 4;
+ painter->save();
+
+ int iconWidth = icons.size() > 0 ? ((option.rect.width() / icons.size()) - 4) : 16;
+ iconWidth = std::min(16, iconWidth);
+
+ const int margin = (option.rect.height() - iconWidth) / 2;
+
+ painter->translate(option.rect.topLeft());
+ for (const QString &iconId : icons) {
+ if (iconId.isEmpty()) {
+ x += iconWidth + 4;
+ continue;
+ }
+ QPixmap icon;
+ QString fullIconId = QString("%1_%2").arg(iconId).arg(iconWidth);
+ if (!QPixmapCache::find(fullIconId, &icon)) {
+ icon = QIcon(iconId).pixmap(iconWidth, iconWidth);
+ if (icon.isNull()) {
+ log::warn("failed to load icon {}", iconId);
+ }
+ QPixmapCache::insert(fullIconId, icon);
+ }
+ painter->drawPixmap(x, margin, iconWidth, iconWidth, icon);
+ x += iconWidth + 4;
+ }
+
+ painter->restore();
+}
+
+void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ if (auto* w = qobject_cast<QAbstractItemView*>(parent())) {
+ w->itemDelegate()->paint(painter, option, index);
+ }
+ else {
+ QStyledItemDelegate::paint(painter, option, index);
+ }
+
+ paintIcons(painter, option, index, getIcons(index));
+}
+
diff --git a/src/icondelegate.h b/src/icondelegate.h
index a123470e..04621591 100644
--- a/src/icondelegate.h
+++ b/src/icondelegate.h
@@ -1,57 +1,57 @@
-/*
-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 ICONDELEGATE_H
-#define ICONDELEGATE_H
-
-#include <QAbstractProxyModel>
-#include <QAbstractItemView>
-#include <QStyledItemDelegate>
-
-
-class IconDelegate : public QStyledItemDelegate
-{
- Q_OBJECT;
-
-public:
- explicit IconDelegate(QTreeView* view, int column = -1, int compactSize = 100);
-
- void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
-
-protected:
-
- // check if icons should be compacted or not
- //
- bool compact() const { return m_compact; }
-
- static void paintIcons(
- QPainter *painter, const QStyleOptionViewItem &option,
- const QModelIndex &index, const QList<QString>& icons);
-
- virtual QList<QString> getIcons(const QModelIndex &index) const = 0;
- virtual size_t getNumIcons(const QModelIndex &index) const = 0;
-
-private:
-
- int m_column;
- int m_compactSize;
- bool m_compact;
-};
-
-#endif // ICONDELEGATE_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 ICONDELEGATE_H
+#define ICONDELEGATE_H
+
+#include <QAbstractProxyModel>
+#include <QAbstractItemView>
+#include <QStyledItemDelegate>
+
+
+class IconDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT;
+
+public:
+ explicit IconDelegate(QTreeView* view, int column = -1, int compactSize = 100);
+
+ void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+
+protected:
+
+ // check if icons should be compacted or not
+ //
+ bool compact() const { return m_compact; }
+
+ static void paintIcons(
+ QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index, const QList<QString>& icons);
+
+ virtual QList<QString> getIcons(const QModelIndex &index) const = 0;
+ virtual size_t getNumIcons(const QModelIndex &index) const = 0;
+
+private:
+
+ int m_column;
+ int m_compactSize;
+ bool m_compact;
+};
+
+#endif // ICONDELEGATE_H
diff --git a/src/iuserinterface.h b/src/iuserinterface.h
index 277d5cf5..61e78768 100644
--- a/src/iuserinterface.h
+++ b/src/iuserinterface.h
@@ -1,33 +1,33 @@
-#ifndef IUSERINTERFACE_H
-#define IUSERINTERFACE_H
-
-
-#include "modinfodialogfwd.h"
-#include <iplugintool.h>
-#include <ipluginmodpage.h>
-#include <delayedfilewriter.h>
-#include <QMenu>
-#include <QMainWindow>
-
-
-class IUserInterface
-{
-public:
- virtual void registerModPage(MOBase::IPluginModPage *modPage) = 0;
-
- virtual void installTranslator(const QString &name) = 0;
-
- virtual bool closeWindow() = 0;
- virtual void setWindowEnabled(bool enabled) = 0;
-
- virtual void displayModInformation(
- ModInfoPtr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) = 0;
-
- virtual void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives) = 0;
-
- virtual MOBase::DelayedFileWriterBase &archivesWriter() = 0;
-
- virtual QMainWindow* mainWindow() = 0;
-};
-
-#endif // IUSERINTERFACE_H
+#ifndef IUSERINTERFACE_H
+#define IUSERINTERFACE_H
+
+
+#include "modinfodialogfwd.h"
+#include <iplugintool.h>
+#include <ipluginmodpage.h>
+#include <delayedfilewriter.h>
+#include <QMenu>
+#include <QMainWindow>
+
+
+class IUserInterface
+{
+public:
+ virtual void registerModPage(MOBase::IPluginModPage *modPage) = 0;
+
+ virtual void installTranslator(const QString &name) = 0;
+
+ virtual bool closeWindow() = 0;
+ virtual void setWindowEnabled(bool enabled) = 0;
+
+ virtual void displayModInformation(
+ ModInfoPtr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) = 0;
+
+ virtual void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives) = 0;
+
+ virtual MOBase::DelayedFileWriterBase &archivesWriter() = 0;
+
+ virtual QMainWindow* mainWindow() = 0;
+};
+
+#endif // IUSERINTERFACE_H
diff --git a/src/loghighlighter.cpp b/src/loghighlighter.cpp
index cd7129ac..ef83cd58 100644
--- a/src/loghighlighter.cpp
+++ b/src/loghighlighter.cpp
@@ -1,51 +1,51 @@
-/*
-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 "loghighlighter.h"
-
-LogHighlighter::LogHighlighter(QObject *parent) :
- QSyntaxHighlighter(parent)
-{
-}
-
-
-void LogHighlighter::highlightBlock(const QString &text)
-{
- int spacePos = text.indexOf(" ");
- if (spacePos != -1) {
- QString type = text.mid(0, spacePos);
- if (type == "DEBUG") {
- setFormat(0, text.length(), Qt::gray);
- } else if (type == "INFO") {
- setFormat(0, text.length(), Qt::darkGreen);
- } else if (type == "ERROR") {
- setFormat(0, text.length(), Qt::red);
- }
- }
-
- int markPos = text.indexOf("injecting to");
- if (markPos != -1) {
- setFormat(markPos + 12, text.length(), Qt::blue);
- }
-
- markPos = text.indexOf("using profile");
- if (markPos != -1) {
- setFormat(markPos + 13, text.length(), Qt::blue);
- }
-}
+/*
+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 "loghighlighter.h"
+
+LogHighlighter::LogHighlighter(QObject *parent) :
+ QSyntaxHighlighter(parent)
+{
+}
+
+
+void LogHighlighter::highlightBlock(const QString &text)
+{
+ int spacePos = text.indexOf(" ");
+ if (spacePos != -1) {
+ QString type = text.mid(0, spacePos);
+ if (type == "DEBUG") {
+ setFormat(0, text.length(), Qt::gray);
+ } else if (type == "INFO") {
+ setFormat(0, text.length(), Qt::darkGreen);
+ } else if (type == "ERROR") {
+ setFormat(0, text.length(), Qt::red);
+ }
+ }
+
+ int markPos = text.indexOf("injecting to");
+ if (markPos != -1) {
+ setFormat(markPos + 12, text.length(), Qt::blue);
+ }
+
+ markPos = text.indexOf("using profile");
+ if (markPos != -1) {
+ setFormat(markPos + 13, text.length(), Qt::blue);
+ }
+}
diff --git a/src/loghighlighter.h b/src/loghighlighter.h
index 55c15142..95ea6f2f 100644
--- a/src/loghighlighter.h
+++ b/src/loghighlighter.h
@@ -1,45 +1,45 @@
-/*
-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 LOGHIGHLIGHTER_H
-#define LOGHIGHLIGHTER_H
-
-#include <QSyntaxHighlighter>
-
-/**
- * @brief Syntax highlighter to make log files from mo.dll more readable.
- * @note this is currently not used!
- **/
-class LogHighlighter : public QSyntaxHighlighter
-{
- Q_OBJECT
-public:
- explicit LogHighlighter(QObject *parent = 0);
-
-signals:
-
-public slots:
-
-protected:
-
- virtual void highlightBlock(const QString &text);
-
-};
-
-#endif // LOGHIGHLIGHTER_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 LOGHIGHLIGHTER_H
+#define LOGHIGHLIGHTER_H
+
+#include <QSyntaxHighlighter>
+
+/**
+ * @brief Syntax highlighter to make log files from mo.dll more readable.
+ * @note this is currently not used!
+ **/
+class LogHighlighter : public QSyntaxHighlighter
+{
+ Q_OBJECT
+public:
+ explicit LogHighlighter(QObject *parent = 0);
+
+signals:
+
+public slots:
+
+protected:
+
+ virtual void highlightBlock(const QString &text);
+
+};
+
+#endif // LOGHIGHLIGHTER_H
diff --git a/src/loglist.cpp b/src/loglist.cpp
index 07592edc..587cf223 100644
--- a/src/loglist.cpp
+++ b/src/loglist.cpp
@@ -1,397 +1,397 @@
-/*
-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 "loglist.h"
-#include "organizercore.h"
-#include "copyeventfilter.h"
-#include "env.h"
-
-using namespace MOBase;
-
-static LogModel* g_instance = nullptr;
-const std::size_t MaxLines = 1000;
-
-static std::unique_ptr<env::Console> m_console;
-static bool m_stdout = false;
-static std::mutex m_stdoutMutex;
-
-
-LogModel::LogModel()
-{
-}
-
-void LogModel::create()
-{
- g_instance = new LogModel;
-}
-
-LogModel& LogModel::instance()
-{
- return *g_instance;
-}
-
-void LogModel::add(MOBase::log::Entry e)
-{
- QMetaObject::invokeMethod(
- this, [this, e]{ onEntryAdded(std::move(e)); }, Qt::QueuedConnection);
-}
-
-QString LogModel::formattedMessage(const QModelIndex& index) const
-{
- if (!index.isValid()) {
- return "";
- }
- return QString::fromStdString(m_entries[index.row()].formattedMessage);
-}
-
-void LogModel::clear()
-{
- beginResetModel();
- m_entries.clear();
- endResetModel();
-}
-
-const std::deque<MOBase::log::Entry>& LogModel::entries() const
-{
- return m_entries;
-}
-
-void LogModel::onEntryAdded(MOBase::log::Entry e)
-{
- bool full = false;
- if (m_entries.size() > MaxLines) {
- m_entries.pop_front();
- full = true;
- }
-
- const int row = static_cast<int>(m_entries.size());
-
- if (!full) {
- beginInsertRows(QModelIndex(), row, row + 1);
- }
-
- m_entries.emplace_back(std::move(e));
-
- if (!full) {
- endInsertRows();
- } else {
- emit dataChanged(
- createIndex(row, 0),
- createIndex(row + 1, columnCount({})));
- }
-}
-
-QModelIndex LogModel::index(int row, int column, const QModelIndex&) const
-{
- return createIndex(row, column, row);
-}
-
-QModelIndex LogModel::parent(const QModelIndex&) const
-{
- return QModelIndex();
-}
-
-int LogModel::rowCount(const QModelIndex& parent) const
-{
- if (parent.isValid())
- return 0;
- else
- return static_cast<int>(m_entries.size());
-}
-
-int LogModel::columnCount(const QModelIndex&) const
-{
- return 3;
-}
-
-QVariant LogModel::data(const QModelIndex& index, int role) const
-{
- using namespace std::chrono;
-
- const auto row = static_cast<std::size_t>(index.row());
- if (row >= m_entries.size()) {
- return {};
- }
-
- const auto& e = m_entries[row];
-
- if (role == Qt::DisplayRole) {
- if (index.column() == 0) {
- const auto ms = duration_cast<milliseconds>(e.time.time_since_epoch());
- const auto s = duration_cast<seconds>(ms);
-
- const std::time_t tt = s.count();
- const int frac = static_cast<int>(ms.count() % 1000);
-
- const auto time = QDateTime::fromSecsSinceEpoch(tt).time().addMSecs(frac);
- return time.toString("hh:mm:ss.zzz");
- } else if (index.column() == 2) {
- return QString::fromStdString(e.message);
- }
- }
-
- if (role == Qt::DecorationRole) {
- if (index.column() == 1) {
- switch (e.level) {
- case log::Warning:
- return QIcon(":/MO/gui/warning");
-
- case log::Error:
- return QIcon(":/MO/gui/problem");
-
- case log::Debug:
- return QIcon(":/MO/gui/debug");
- case log::Info:
- return QIcon(":/MO/gui/information");
- default:
- return {};
- }
- }
- }
-
- return QVariant();
-}
-
-QVariant LogModel::headerData(int, Qt::Orientation, int) const
-{
- return {};
-}
-
-
-LogList::LogList(QWidget* parent)
- : QTreeView(parent),
- m_core(nullptr),
- m_copyFilter(this, [=](auto&& index) { return LogModel::instance().formattedMessage(index); })
-{
- setModel(&LogModel::instance());
-
- header()->setMinimumSectionSize(0);
- header()->resizeSection(1, 20);
- header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
-
- setAutoScroll(true);
- scrollToBottom();
-
- connect(
- this, &QWidget::customContextMenuRequested,
- [&](auto&& pos){ onContextMenu(pos); });
-
- connect(model(), &LogModel::rowsInserted, this, [&]{ onNewEntry(); });
- connect(model(), &LogModel::dataChanged, this, [&]{ onNewEntry(); });
-
- m_timer.setSingleShot(true);
- connect(&m_timer, &QTimer::timeout, [&]{ scrollToBottom(); });
-
- installEventFilter(&m_copyFilter);
-}
-
-void LogList::onNewEntry()
-{
- m_timer.start(std::chrono::milliseconds(10));
-}
-
-void LogList::setCore(OrganizerCore& core)
-{
- m_core = &core;
-}
-
-void LogList::copyToClipboard()
-{
- std::string s;
-
- for (const auto& e : LogModel::instance().entries()) {
- s += e.formattedMessage + "\n";
- }
-
- if (!s.empty()) {
- // last newline
- s.pop_back();
- }
-
- QApplication::clipboard()->setText(QString::fromStdString(s));
-}
-
-void LogList::clear()
-{
- static_cast<LogModel*>(model())->clear();
-}
-
-void LogList::openLogsFolder()
-{
- QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath());
- shell::Explore(logsPath);
-}
-
-QMenu* LogList::createMenu(QWidget* parent)
-{
- auto* menu = new QMenu(parent);
-
- menu->addAction(tr("Copy"), [&]{ m_copyFilter.copySelection(); });
- menu->addAction(tr("&Copy all"), [&]{ copyToClipboard(); });
- menu->addSeparator();
- menu->addAction(tr("C&lear all"), [&]{ clear(); });
- menu->addAction(tr("&Open logs folder"), [&]{ openLogsFolder(); });
-
- auto* levels = new QMenu(tr("&Level"));
- menu->addMenu(levels);
-
- auto* ag = new QActionGroup(menu);
-
- auto addAction = [&](auto&& text, auto&& level) {
- auto* a = new QAction(text, ag);
-
- a->setCheckable(true);
- a->setChecked(log::getDefault().level() == level);
-
- connect(a, &QAction::triggered, [this, level]{
- if (m_core) {
- m_core->setLogLevel(level);
- }
- });
-
- levels->addAction(a);
- };
-
- addAction(tr("&Debug"), log::Debug);
- addAction(tr("&Info"), log::Info);
- addAction(tr("&Warnings"), log::Warning);
- addAction(tr("&Errors"), log::Error);
-
- return menu;
-}
-
-void LogList::onContextMenu(const QPoint& pos)
-{
- auto* menu = createMenu(this);
- menu->popup(viewport()->mapToGlobal(pos));
-}
-
-
-log::Levels convertQtLevel(QtMsgType t)
-{
- switch (t)
- {
- case QtDebugMsg:
- return log::Debug;
-
- case QtWarningMsg:
- return log::Warning;
-
- case QtCriticalMsg: // fall-through
- case QtFatalMsg:
- return log::Error;
-
- case QtInfoMsg: // fall-through
- default:
- return log::Info;
- }
-}
-
-void qtLogCallback(
- QtMsgType type, const QMessageLogContext& context, const QString& message)
-{
- std::string_view file = "";
-
- if (type != QtDebugMsg) {
- if (context.file) {
- file = context.file;
-
- const auto lastSep = file.find_last_of("/\\");
- if (lastSep != std::string_view::npos) {
- file = {context.file + lastSep + 1};
- }
- }
- }
-
- if (file.empty()) {
- log::log(
- convertQtLevel(type), "{}",
- message.toStdString());
- } else {
- log::log(
- convertQtLevel(type), "[{}:{}] {}",
- file, context.line, message.toStdString());
- }
-}
-
-void logToStdout(bool b)
-{
- m_stdout = b;
-
- // logging to stdout is already set up in uibase by log::createDefault(),
- // all it needs is to redirect stdout to the console, which is done by
- // creating an env::Console object
-
- if (m_stdout) {
- m_console.reset(new env::Console);
- } else {
- m_console.reset();
- }
-}
-
-void initLogging()
-{
- LogModel::create();
-
- log::LoggerConfiguration conf;
- conf.maxLevel = MOBase::log::Debug;
- conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$";
- conf.utc = true;
-
- log::createDefault(conf);
-
- log::getDefault().setCallback(
- [](log::Entry e){ LogModel::instance().add(e); });
-
- log::getDefault().addToBlacklist(std::string("\\") + getenv("USERNAME"), "\\USERNAME");
- log::getDefault().addToBlacklist(std::string("/") + getenv("USERNAME"), "/USERNAME");
-
- qInstallMessageHandler(qtLogCallback);
-}
-
-bool createAndMakeWritable(const std::wstring &subPath) {
- QString const dataPath = qApp->property("dataPath").toString();
- QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
-
- if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) {
- QMessageBox::critical(nullptr, QObject::tr("Error"),
- QObject::tr("Failed to create \"%1\". Your user "
- "account probably lacks permission.")
- .arg(fullPath));
- return false;
- } else {
- return true;
- }
-}
-
-bool setLogDirectory(const QString& dir)
-{
- const auto logFile =
- dir + "/" +
- QString::fromStdWString(AppConfig::logPath()) + "/" +
- QString::fromStdWString(AppConfig::logFileName());
-
- if (!createAndMakeWritable(AppConfig::logPath())) {
- return false;
- }
-
- log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString()));
-
- return true;
-}
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "loglist.h"
+#include "organizercore.h"
+#include "copyeventfilter.h"
+#include "env.h"
+
+using namespace MOBase;
+
+static LogModel* g_instance = nullptr;
+const std::size_t MaxLines = 1000;
+
+static std::unique_ptr<env::Console> m_console;
+static bool m_stdout = false;
+static std::mutex m_stdoutMutex;
+
+
+LogModel::LogModel()
+{
+}
+
+void LogModel::create()
+{
+ g_instance = new LogModel;
+}
+
+LogModel& LogModel::instance()
+{
+ return *g_instance;
+}
+
+void LogModel::add(MOBase::log::Entry e)
+{
+ QMetaObject::invokeMethod(
+ this, [this, e]{ onEntryAdded(std::move(e)); }, Qt::QueuedConnection);
+}
+
+QString LogModel::formattedMessage(const QModelIndex& index) const
+{
+ if (!index.isValid()) {
+ return "";
+ }
+ return QString::fromStdString(m_entries[index.row()].formattedMessage);
+}
+
+void LogModel::clear()
+{
+ beginResetModel();
+ m_entries.clear();
+ endResetModel();
+}
+
+const std::deque<MOBase::log::Entry>& LogModel::entries() const
+{
+ return m_entries;
+}
+
+void LogModel::onEntryAdded(MOBase::log::Entry e)
+{
+ bool full = false;
+ if (m_entries.size() > MaxLines) {
+ m_entries.pop_front();
+ full = true;
+ }
+
+ const int row = static_cast<int>(m_entries.size());
+
+ if (!full) {
+ beginInsertRows(QModelIndex(), row, row + 1);
+ }
+
+ m_entries.emplace_back(std::move(e));
+
+ if (!full) {
+ endInsertRows();
+ } else {
+ emit dataChanged(
+ createIndex(row, 0),
+ createIndex(row + 1, columnCount({})));
+ }
+}
+
+QModelIndex LogModel::index(int row, int column, const QModelIndex&) const
+{
+ return createIndex(row, column, row);
+}
+
+QModelIndex LogModel::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+int LogModel::rowCount(const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return 0;
+ else
+ return static_cast<int>(m_entries.size());
+}
+
+int LogModel::columnCount(const QModelIndex&) const
+{
+ return 3;
+}
+
+QVariant LogModel::data(const QModelIndex& index, int role) const
+{
+ using namespace std::chrono;
+
+ const auto row = static_cast<std::size_t>(index.row());
+ if (row >= m_entries.size()) {
+ return {};
+ }
+
+ const auto& e = m_entries[row];
+
+ if (role == Qt::DisplayRole) {
+ if (index.column() == 0) {
+ const auto ms = duration_cast<milliseconds>(e.time.time_since_epoch());
+ const auto s = duration_cast<seconds>(ms);
+
+ const std::time_t tt = s.count();
+ const int frac = static_cast<int>(ms.count() % 1000);
+
+ const auto time = QDateTime::fromSecsSinceEpoch(tt).time().addMSecs(frac);
+ return time.toString("hh:mm:ss.zzz");
+ } else if (index.column() == 2) {
+ return QString::fromStdString(e.message);
+ }
+ }
+
+ if (role == Qt::DecorationRole) {
+ if (index.column() == 1) {
+ switch (e.level) {
+ case log::Warning:
+ return QIcon(":/MO/gui/warning");
+
+ case log::Error:
+ return QIcon(":/MO/gui/problem");
+
+ case log::Debug:
+ return QIcon(":/MO/gui/debug");
+ case log::Info:
+ return QIcon(":/MO/gui/information");
+ default:
+ return {};
+ }
+ }
+ }
+
+ return QVariant();
+}
+
+QVariant LogModel::headerData(int, Qt::Orientation, int) const
+{
+ return {};
+}
+
+
+LogList::LogList(QWidget* parent)
+ : QTreeView(parent),
+ m_core(nullptr),
+ m_copyFilter(this, [=](auto&& index) { return LogModel::instance().formattedMessage(index); })
+{
+ setModel(&LogModel::instance());
+
+ header()->setMinimumSectionSize(0);
+ header()->resizeSection(1, 20);
+ header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+
+ setAutoScroll(true);
+ scrollToBottom();
+
+ connect(
+ this, &QWidget::customContextMenuRequested,
+ [&](auto&& pos){ onContextMenu(pos); });
+
+ connect(model(), &LogModel::rowsInserted, this, [&]{ onNewEntry(); });
+ connect(model(), &LogModel::dataChanged, this, [&]{ onNewEntry(); });
+
+ m_timer.setSingleShot(true);
+ connect(&m_timer, &QTimer::timeout, [&]{ scrollToBottom(); });
+
+ installEventFilter(&m_copyFilter);
+}
+
+void LogList::onNewEntry()
+{
+ m_timer.start(std::chrono::milliseconds(10));
+}
+
+void LogList::setCore(OrganizerCore& core)
+{
+ m_core = &core;
+}
+
+void LogList::copyToClipboard()
+{
+ std::string s;
+
+ for (const auto& e : LogModel::instance().entries()) {
+ s += e.formattedMessage + "\n";
+ }
+
+ if (!s.empty()) {
+ // last newline
+ s.pop_back();
+ }
+
+ QApplication::clipboard()->setText(QString::fromStdString(s));
+}
+
+void LogList::clear()
+{
+ static_cast<LogModel*>(model())->clear();
+}
+
+void LogList::openLogsFolder()
+{
+ QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath());
+ shell::Explore(logsPath);
+}
+
+QMenu* LogList::createMenu(QWidget* parent)
+{
+ auto* menu = new QMenu(parent);
+
+ menu->addAction(tr("Copy"), [&]{ m_copyFilter.copySelection(); });
+ menu->addAction(tr("&Copy all"), [&]{ copyToClipboard(); });
+ menu->addSeparator();
+ menu->addAction(tr("C&lear all"), [&]{ clear(); });
+ menu->addAction(tr("&Open logs folder"), [&]{ openLogsFolder(); });
+
+ auto* levels = new QMenu(tr("&Level"));
+ menu->addMenu(levels);
+
+ auto* ag = new QActionGroup(menu);
+
+ auto addAction = [&](auto&& text, auto&& level) {
+ auto* a = new QAction(text, ag);
+
+ a->setCheckable(true);
+ a->setChecked(log::getDefault().level() == level);
+
+ connect(a, &QAction::triggered, [this, level]{
+ if (m_core) {
+ m_core->setLogLevel(level);
+ }
+ });
+
+ levels->addAction(a);
+ };
+
+ addAction(tr("&Debug"), log::Debug);
+ addAction(tr("&Info"), log::Info);
+ addAction(tr("&Warnings"), log::Warning);
+ addAction(tr("&Errors"), log::Error);
+
+ return menu;
+}
+
+void LogList::onContextMenu(const QPoint& pos)
+{
+ auto* menu = createMenu(this);
+ menu->popup(viewport()->mapToGlobal(pos));
+}
+
+
+log::Levels convertQtLevel(QtMsgType t)
+{
+ switch (t)
+ {
+ case QtDebugMsg:
+ return log::Debug;
+
+ case QtWarningMsg:
+ return log::Warning;
+
+ case QtCriticalMsg: // fall-through
+ case QtFatalMsg:
+ return log::Error;
+
+ case QtInfoMsg: // fall-through
+ default:
+ return log::Info;
+ }
+}
+
+void qtLogCallback(
+ QtMsgType type, const QMessageLogContext& context, const QString& message)
+{
+ std::string_view file = "";
+
+ if (type != QtDebugMsg) {
+ if (context.file) {
+ file = context.file;
+
+ const auto lastSep = file.find_last_of("/\\");
+ if (lastSep != std::string_view::npos) {
+ file = {context.file + lastSep + 1};
+ }
+ }
+ }
+
+ if (file.empty()) {
+ log::log(
+ convertQtLevel(type), "{}",
+ message.toStdString());
+ } else {
+ log::log(
+ convertQtLevel(type), "[{}:{}] {}",
+ file, context.line, message.toStdString());
+ }
+}
+
+void logToStdout(bool b)
+{
+ m_stdout = b;
+
+ // logging to stdout is already set up in uibase by log::createDefault(),
+ // all it needs is to redirect stdout to the console, which is done by
+ // creating an env::Console object
+
+ if (m_stdout) {
+ m_console.reset(new env::Console);
+ } else {
+ m_console.reset();
+ }
+}
+
+void initLogging()
+{
+ LogModel::create();
+
+ log::LoggerConfiguration conf;
+ conf.maxLevel = MOBase::log::Debug;
+ conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$";
+ conf.utc = true;
+
+ log::createDefault(conf);
+
+ log::getDefault().setCallback(
+ [](log::Entry e){ LogModel::instance().add(e); });
+
+ log::getDefault().addToBlacklist(std::string("\\") + getenv("USERNAME"), "\\USERNAME");
+ log::getDefault().addToBlacklist(std::string("/") + getenv("USERNAME"), "/USERNAME");
+
+ qInstallMessageHandler(qtLogCallback);
+}
+
+bool createAndMakeWritable(const std::wstring &subPath) {
+ QString const dataPath = qApp->property("dataPath").toString();
+ QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
+
+ if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) {
+ QMessageBox::critical(nullptr, QObject::tr("Error"),
+ QObject::tr("Failed to create \"%1\". Your user "
+ "account probably lacks permission.")
+ .arg(fullPath));
+ return false;
+ } else {
+ return true;
+ }
+}
+
+bool setLogDirectory(const QString& dir)
+{
+ const auto logFile =
+ dir + "/" +
+ QString::fromStdWString(AppConfig::logPath()) + "/" +
+ QString::fromStdWString(AppConfig::logFileName());
+
+ if (!createAndMakeWritable(AppConfig::logPath())) {
+ return false;
+ }
+
+ log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString()));
+
+ return true;
+}
diff --git a/src/loglist.h b/src/loglist.h
index df7c1467..ddf5f49c 100644
--- a/src/loglist.h
+++ b/src/loglist.h
@@ -1,94 +1,94 @@
-/*
-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 LOGBUFFER_H
-#define LOGBUFFER_H
-
-
-#include "shared/appconfig.h"
-#include "copyeventfilter.h"
-#include <log.h>
-#include <QTreeView>
-#include <deque>
-
-class OrganizerCore;
-
-class LogModel : public QAbstractItemModel
-{
- Q_OBJECT
-
-public:
- static void create();
- static LogModel& instance();
-
- void add(MOBase::log::Entry e);
- void clear();
-
- const std::deque<MOBase::log::Entry>& entries() const;
-
- QString formattedMessage(const QModelIndex& index) const;
-
-protected:
- QModelIndex index(int row, int column, const QModelIndex& parent) const override;
- QModelIndex parent(const QModelIndex &child) const override;
- int rowCount(const QModelIndex &parent) const override;
- int columnCount(const QModelIndex &parent) const override;
- QVariant data(const QModelIndex &index, int role) const override;
-
- QVariant headerData(
- int section, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
-
-private:
- std::deque<MOBase::log::Entry> m_entries;
-
- LogModel();
- void onEntryAdded(MOBase::log::Entry e);
-};
-
-
-class LogList : public QTreeView
-{
- Q_OBJECT;
-
-public:
- LogList(QWidget* parent=nullptr);
-
- void setCore(OrganizerCore& core);
-
- void copyToClipboard();
- void clear();
- void openLogsFolder();
-
- QMenu* createMenu(QWidget* parent=nullptr);
-
-private:
- OrganizerCore* m_core;
- QTimer m_timer;
- CopyEventFilter m_copyFilter;
-
- void onContextMenu(const QPoint& pos);
- void onNewEntry();
-};
-
-
-void logToStdout(bool b);
-void initLogging();
-bool setLogDirectory(const QString& dir);
-
-#endif // LOGBUFFER_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 LOGBUFFER_H
+#define LOGBUFFER_H
+
+
+#include "shared/appconfig.h"
+#include "copyeventfilter.h"
+#include <log.h>
+#include <QTreeView>
+#include <deque>
+
+class OrganizerCore;
+
+class LogModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ static void create();
+ static LogModel& instance();
+
+ void add(MOBase::log::Entry e);
+ void clear();
+
+ const std::deque<MOBase::log::Entry>& entries() const;
+
+ QString formattedMessage(const QModelIndex& index) const;
+
+protected:
+ QModelIndex index(int row, int column, const QModelIndex& parent) const override;
+ QModelIndex parent(const QModelIndex &child) const override;
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ QVariant headerData(
+ int section, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
+
+private:
+ std::deque<MOBase::log::Entry> m_entries;
+
+ LogModel();
+ void onEntryAdded(MOBase::log::Entry e);
+};
+
+
+class LogList : public QTreeView
+{
+ Q_OBJECT;
+
+public:
+ LogList(QWidget* parent=nullptr);
+
+ void setCore(OrganizerCore& core);
+
+ void copyToClipboard();
+ void clear();
+ void openLogsFolder();
+
+ QMenu* createMenu(QWidget* parent=nullptr);
+
+private:
+ OrganizerCore* m_core;
+ QTimer m_timer;
+ CopyEventFilter m_copyFilter;
+
+ void onContextMenu(const QPoint& pos);
+ void onNewEntry();
+};
+
+
+void logToStdout(bool b);
+void initLogging();
+bool setLogDirectory(const QString& dir);
+
+#endif // LOGBUFFER_H
diff --git a/src/messagedialog.cpp b/src/messagedialog.cpp
index 1244e8c6..afde0056 100644
--- a/src/messagedialog.cpp
+++ b/src/messagedialog.cpp
@@ -1,108 +1,108 @@
-/*
-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 "messagedialog.h"
-#include "ui_messagedialog.h"
-#include <log.h>
-#include <QTimer>
-#include <QResizeEvent>
-#include <Windows.h>
-
-using namespace MOBase;
-
-MessageDialog::MessageDialog(const QString &text, QWidget *reference) :
- QDialog(reference),
- ui(new Ui::MessageDialog)
-{
- ui->setupUi(this);
-
- // very crude way to ensure no single word in the test is wider than the message window. ellide in the center if necessary
- QFontMetrics metrics(ui->message->font());
- QString restrictedText;
- QStringList lines = text.split("\n");
- foreach (const QString &line, lines) {
- QString newLine;
- QStringList words = line.split(" ");
- foreach (const QString &word, words) {
- if (word.length() > 10) {
- newLine += "<span style=\"nobreak\">" + metrics.elidedText(word, Qt::ElideMiddle, ui->message->maximumWidth()) + "</span>";
- } else {
- newLine += word;
- }
- newLine += " ";
- }
- restrictedText += newLine + "\n";
- }
-
- ui->message->setText(restrictedText);
- this->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
- this->setFocusPolicy(Qt::NoFocus);
- this->setAttribute(Qt::WA_ShowWithoutActivating);
- QTimer::singleShot(1000 + (text.length() * 40), this, SLOT(hide()));
- if (reference != nullptr) {
- QPoint position = reference->mapToGlobal(QPoint(reference->width() / 2, reference->height()));
- position.rx() -= this->width() / 2;
- position.ry() -= this->height() + 5;
- move(position);
- }
-}
-
-
-MessageDialog::~MessageDialog()
-{
- delete ui;
-}
-
-
-void MessageDialog::resizeEvent(QResizeEvent *event)
-{
- QWidget *par = parentWidget();
- if (par != nullptr) {
- QPoint position = par->mapToGlobal(QPoint(par->width() / 2, par->height()));
- position.rx() -= event->size().width() / 2;
- position.ry() -= event->size().height() + 5;
- move(position);
- }
-}
-
-
-void MessageDialog::showMessage(const QString &text, QWidget *reference, bool bringToFront)
-{
- log::debug("{}", text);
-
- if (!reference) {
- for (QWidget* w : qApp->topLevelWidgets()) {
- if (dynamic_cast<QMainWindow*>(w)) {
- reference = w;
- break;
- }
- }
- }
-
- if (!reference) {
- return;
- }
-
- MessageDialog *dialog = new MessageDialog(text, reference);
- dialog->show();
-
- if (bringToFront) {
- reference->activateWindow();
- }
-}
+/*
+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 "messagedialog.h"
+#include "ui_messagedialog.h"
+#include <log.h>
+#include <QTimer>
+#include <QResizeEvent>
+#include <Windows.h>
+
+using namespace MOBase;
+
+MessageDialog::MessageDialog(const QString &text, QWidget *reference) :
+ QDialog(reference),
+ ui(new Ui::MessageDialog)
+{
+ ui->setupUi(this);
+
+ // very crude way to ensure no single word in the test is wider than the message window. ellide in the center if necessary
+ QFontMetrics metrics(ui->message->font());
+ QString restrictedText;
+ QStringList lines = text.split("\n");
+ foreach (const QString &line, lines) {
+ QString newLine;
+ QStringList words = line.split(" ");
+ foreach (const QString &word, words) {
+ if (word.length() > 10) {
+ newLine += "<span style=\"nobreak\">" + metrics.elidedText(word, Qt::ElideMiddle, ui->message->maximumWidth()) + "</span>";
+ } else {
+ newLine += word;
+ }
+ newLine += " ";
+ }
+ restrictedText += newLine + "\n";
+ }
+
+ ui->message->setText(restrictedText);
+ this->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
+ this->setFocusPolicy(Qt::NoFocus);
+ this->setAttribute(Qt::WA_ShowWithoutActivating);
+ QTimer::singleShot(1000 + (text.length() * 40), this, SLOT(hide()));
+ if (reference != nullptr) {
+ QPoint position = reference->mapToGlobal(QPoint(reference->width() / 2, reference->height()));
+ position.rx() -= this->width() / 2;
+ position.ry() -= this->height() + 5;
+ move(position);
+ }
+}
+
+
+MessageDialog::~MessageDialog()
+{
+ delete ui;
+}
+
+
+void MessageDialog::resizeEvent(QResizeEvent *event)
+{
+ QWidget *par = parentWidget();
+ if (par != nullptr) {
+ QPoint position = par->mapToGlobal(QPoint(par->width() / 2, par->height()));
+ position.rx() -= event->size().width() / 2;
+ position.ry() -= event->size().height() + 5;
+ move(position);
+ }
+}
+
+
+void MessageDialog::showMessage(const QString &text, QWidget *reference, bool bringToFront)
+{
+ log::debug("{}", text);
+
+ if (!reference) {
+ for (QWidget* w : qApp->topLevelWidgets()) {
+ if (dynamic_cast<QMainWindow*>(w)) {
+ reference = w;
+ break;
+ }
+ }
+ }
+
+ if (!reference) {
+ return;
+ }
+
+ MessageDialog *dialog = new MessageDialog(text, reference);
+ dialog->show();
+
+ if (bringToFront) {
+ reference->activateWindow();
+ }
+}
diff --git a/src/messagedialog.h b/src/messagedialog.h
index 7e9b9c3b..0dbc0e6e 100644
--- a/src/messagedialog.h
+++ b/src/messagedialog.h
@@ -1,67 +1,67 @@
-/*
-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 MESSAGEDIALOG_H
-#define MESSAGEDIALOG_H
-
-#include <QDialog>
-
-namespace Ui {
- class MessageDialog;
-}
-
-/**
- * borderless dialog used to display short messages that will automatically
- * vanish after a moment
- **/
-class MessageDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- /**
- * @brief constructor
- *
- * @param text the message to display
- * @param reference parent widget. This will also be used to position the message at the bottom center of the dialog
- **/
-
- explicit MessageDialog(const QString &text, QWidget *reference);
-
- ~MessageDialog();
-
- /**
- * factory function for message dialogs. This can be used as a fire-and-forget. The message
- * will automatically positioned to the reference dialog and get a reasonable view time
- *
- * @param text the text to display. The length of this text is used to determine how long the dialog is to be shown
- * @param reference the reference widget on top of which the message should be displayed
- * @param true if the message should bring MO to front to ensure this message is visible
- **/
- static void showMessage(const QString &text, QWidget *reference, bool bringToFront = true);
-
-protected:
-
- virtual void resizeEvent(QResizeEvent *event);
-
-private:
- Ui::MessageDialog *ui;
-};
-
-#endif // MESSAGEDIALOG_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 MESSAGEDIALOG_H
+#define MESSAGEDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+ class MessageDialog;
+}
+
+/**
+ * borderless dialog used to display short messages that will automatically
+ * vanish after a moment
+ **/
+class MessageDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ *
+ * @param text the message to display
+ * @param reference parent widget. This will also be used to position the message at the bottom center of the dialog
+ **/
+
+ explicit MessageDialog(const QString &text, QWidget *reference);
+
+ ~MessageDialog();
+
+ /**
+ * factory function for message dialogs. This can be used as a fire-and-forget. The message
+ * will automatically positioned to the reference dialog and get a reasonable view time
+ *
+ * @param text the text to display. The length of this text is used to determine how long the dialog is to be shown
+ * @param reference the reference widget on top of which the message should be displayed
+ * @param true if the message should bring MO to front to ensure this message is visible
+ **/
+ static void showMessage(const QString &text, QWidget *reference, bool bringToFront = true);
+
+protected:
+
+ virtual void resizeEvent(QResizeEvent *event);
+
+private:
+ Ui::MessageDialog *ui;
+};
+
+#endif // MESSAGEDIALOG_H
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index ef33663d..ff3d32f3 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -1,663 +1,663 @@
-/*
-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 "moapplication.h"
-#include "settings.h"
-#include "commandline.h"
-#include "instancemanager.h"
-#include "organizercore.h"
-#include "thread_utils.h"
-#include "loglist.h"
-#include "multiprocess.h"
-#include "nexusinterface.h"
-#include "nxmaccessmanager.h"
-#include "tutorialmanager.h"
-#include "sanitychecks.h"
-#include "mainwindow.h"
-#include "messagedialog.h"
-#include "shared/util.h"
-#include <iplugingame.h>
-#include <report.h>
-#include <utility.h>
-#include <log.h>
-#include "shared/appconfig.h"
-#include <QFile>
-#include <QStringList>
-#include <QProxyStyle>
-#include <QStyleFactory>
-#include <QPainter>
-#include <QStyleOption>
-#include <QDebug>
-#include <QSSLSocket>
-
-// see addDllsToPath() below
-#pragma comment(linker, "/manifestDependency:\"" \
- "name='dlls' " \
- "processorArchitecture='x86' " \
- "version='1.0.0.0' " \
- "type='win32' \"")
-
-using namespace MOBase;
-using namespace MOShared;
-
-// style proxy that changes the appearance of drop indicators
-//
-class ProxyStyle : public QProxyStyle {
-public:
- ProxyStyle(QStyle* baseStyle = 0)
- : QProxyStyle(baseStyle)
- {
- }
-
- void drawPrimitive(
- PrimitiveElement element, const QStyleOption* option,
- QPainter* painter, const QWidget* widget) const override
- {
- if (element == QStyle::PE_IndicatorItemViewItemDrop) {
-
- // 0. Fix a bug that made the drop indicator sometimes appear on top
- // of the mod list when selecting a mod.
- if (option->rect.height() == 0
- && option->rect.bottomRight() == QPoint(-1, -1)) {
- return;
- }
-
- // 1. full-width drop indicator
- QRect rect(option->rect);
- if (auto* view = qobject_cast<const QTreeView*>(widget)) {
- rect.setLeft(view->indentation());
- rect.setRight(widget->width());
- }
-
- // 2. stylish drop indicator
- painter->setRenderHint(QPainter::Antialiasing, true);
-
- QColor col(option->palette.windowText().color());
- QPen pen(col);
- pen.setWidth(2);
- col.setAlpha(50);
-
- painter->setPen(pen);
- painter->setBrush(QBrush(col));
- if (rect.height() == 0) {
- QPoint tri[3] = {
- rect.topLeft(),
- rect.topLeft() + QPoint(-5, 5),
- rect.topLeft() + QPoint(-5, -5)
- };
- painter->drawPolygon(tri, 3);
- painter->drawLine(rect.topLeft(), rect.topRight());
- }
- else {
- painter->drawRoundedRect(rect, 5, 5);
- }
- }
- else {
- QProxyStyle::drawPrimitive(element, option, painter, widget);
- }
- }
-
-};
-
-
-// This adds the `dlls` directory to the path so the dlls can be found. How
-// MO is able to find dlls in there is a bit convoluted:
-//
-// Dependencies on DLLs can be baked into an executable by passing a
-// `manifestdependency` option to the linker. This can be done on the command
-// line or with a pragma. Typically, the dependency will not be a hardcoded
-// filename, but an assembly name, such as Microsoft.Windows.Common-Controls.
-//
-// When Windows loads the exe, it will look for this assembly in a variety of
-// places, such as in the WinSxS folder, but also in the program's folder. It
-// will look for `assemblyname.dll` or `assemblyname/assemblyname.dll` and try
-// to load that.
-//
-// If these files don't exist, then the loader gets creative and looks for
-// `assemblyname.manifest` and `assemblyname/assemblyname.manifest`. A manifest
-// file is just an XML file that can contain a list of DLLs to load for this
-// assembly.
-//
-// In MO's case, there's a `pragma` at the beginning of this file which adds
-// `dlls` as an "assembly" dependency. This is a bit of a hack to just force
-// the loader to eventually find `dlls/dlls.manifest`, which contains the list
-// of all the DLLs MO requires to load.
-//
-// This file was handwritten in `modorganizer/src/dlls.manifest.qt5` and
-// is copied and renamed in CMakeLists.txt into `bin/dlls/dlls.manifest`. Note
-// that the useless and incorrect .qt5 extension is removed.
-//
-void addDllsToPath()
-{
- const auto dllsPath = QDir::toNativeSeparators(
- QCoreApplication::applicationDirPath() + "/dlls");
-
- QCoreApplication::setLibraryPaths(
- QStringList(dllsPath) + QCoreApplication::libraryPaths());
-
- env::prependToPath(dllsPath);
-}
-
-
-MOApplication::MOApplication(int& argc, char** argv)
- : QApplication(argc, argv)
-{
- TimeThis tt("MOApplication()");
-
- qputenv("QML_DISABLE_DISK_CACHE", "true");
-
- connect(&m_styleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){
- log::debug("style file '{}' changed, reloading", file);
- updateStyle(file);
- });
-
- m_defaultStyle = style()->objectName();
- setStyle(new ProxyStyle(style()));
- addDllsToPath();
-}
-
-OrganizerCore& MOApplication::core()
-{
- return *m_core;
-}
-
-void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess)
-{
- connect(
- &multiProcess, &MOMultiProcess::messageSent, this,
- [this](auto&& s){ externalMessage(s); },
- Qt::QueuedConnection);
-}
-
-int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
-{
- TimeThis tt("MOApplication setup()");
-
- // makes plugin data path available to plugins, see
- // IOrganizer::getPluginDataPath()
- MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath());
-
- // figuring out the current instance
- m_instance = getCurrentInstance(forceSelect);
- if (!m_instance) {
- return 1;
- }
-
- // first time the data path is available, set the global property and log
- // directory, then log a bunch of debug stuff
- const QString dataPath = m_instance->directory();
- setProperty("dataPath", dataPath);
-
- if (!setLogDirectory(dataPath)) {
- reportError(tr("Failed to create log folder."));
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
- }
-
- log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
-
- log::info(
- "starting Mod Organizer version {} revision {} in {}, usvfs: {}",
- createVersionInfo().displayString(3), GITID,
- QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString());
-
- if (multiProcess.secondary()) {
- log::debug("another instance of MO is running but --multiple was given");
- }
-
- log::info("data path: {}", m_instance->directory());
- log::info("working directory: {}", QDir::currentPath());
-
-
- tt.start("MOApplication::doOneRun() settings");
-
- // deleting old files, only for the main instance
- if (!multiProcess.secondary()) {
- purgeOldFiles();
- }
-
- // loading settings
- m_settings.reset(new Settings(m_instance->iniPath(), true));
- log::getDefault().setLevel(m_settings->diagnostics().logLevel());
- log::debug("using ini at '{}'", m_settings->filename());
-
- OrganizerCore::setGlobalCoreDumpType(m_settings->diagnostics().coreDumpType());
-
-
- tt.start("MOApplication::doOneRun() log and checks");
-
- // logging and checking
- env::Environment env;
- env.dump(*m_settings);
- m_settings->dump();
- sanity::checkEnvironment(env);
-
- m_modules = std::move(env.onModuleLoaded(qApp, [](auto&& m) {
- if (m.interesting()) {
- log::debug("loaded module {}", m.toString());
- }
-
- sanity::checkIncompatibleModule(m);
- }));
-
- auto sslBuildVersion = QSslSocket::sslLibraryBuildVersionString();
- auto sslVersion = QSslSocket::sslLibraryVersionString();
- log::debug("SSL Build Version: {}, SSL Runtime Version {}", sslBuildVersion, sslVersion);
-
- // nexus interface
- tt.start("MOApplication::doOneRun() NexusInterface");
- log::debug("initializing nexus interface");
- m_nexus.reset(new NexusInterface(m_settings.get()));
-
- // organizer core
- tt.start("MOApplication::doOneRun() OrganizerCore");
- log::debug("initializing core");
-
- m_core.reset(new OrganizerCore(*m_settings));
- if (!m_core->bootstrap()) {
- reportError(tr("Failed to set up data paths."));
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
- }
-
- // plugins
- tt.start("MOApplication::doOneRun() plugins");
- log::debug("initializing plugins");
-
- m_plugins = std::make_unique<PluginContainer>(m_core.get());
- m_plugins->loadPlugins();
-
- // instance
- if (auto r=setupInstanceLoop(*m_instance, *m_plugins)) {
- return *r;
- }
-
- if (m_instance->isPortable()) {
- log::debug("this is a portable instance");
- }
-
- tt.start("MOApplication::doOneRun() OrganizerCore setup");
-
- sanity::checkPaths(*m_instance->gamePlugin(), *m_settings);
-
- // setting up organizer core
- m_core->setManagedGame(m_instance->gamePlugin());
- m_core->createDefaultProfile();
-
- log::info(
- "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}",
- m_instance->gamePlugin()->gameName(),
- m_instance->gamePlugin()->gameShortName(),
- (m_settings->game().edition().value_or("").isEmpty() ?
- "(none)" : *m_settings->game().edition()),
- m_instance->gamePlugin()->steamAPPId(),
- m_instance->gamePlugin()->gameDirectory().absolutePath());
-
- CategoryFactory::instance().loadCategories();
- m_core->updateExecutablesList();
- m_core->updateModInfoFromDisc();
- m_core->setCurrentProfile(m_instance->profileName());
-
- return 0;
-}
-
-int MOApplication::run(MOMultiProcess& multiProcess)
-{
- // checking command line
- TimeThis tt("MOApplication::run()");
-
- // show splash
- tt.start("MOApplication::doOneRun() splash");
-
- MOSplash splash(*m_settings, m_instance->directory(), m_instance->gamePlugin());
-
- tt.start("MOApplication::doOneRun() finishing");
-
- // start an api check
- QString apiKey;
- if (GlobalSettings::nexusApiKey(apiKey)) {
- m_nexus->getAccessManager()->apiCheck(apiKey);
- }
-
- // tutorials
- log::debug("initializing tutorials");
- TutorialManager::init(
- qApp->applicationDirPath() + "/"
- + QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
- m_core.get());
-
- // styling
- if (!setStyleFile(m_settings->interface().styleName().value_or(""))) {
- // disable invalid stylesheet
- m_settings->interface().setStyleName("");
- }
-
-
- int res = 1;
-
- {
- tt.start("MOApplication::doOneRun() MainWindow setup");
- MainWindow mainWindow(*m_settings, *m_core, *m_plugins);
-
- // the nexus interface can show dialogs, make sure they're parented to the
- // main window
- m_nexus->getAccessManager()->setTopLevelWidget(&mainWindow);
-
- connect(
- &mainWindow, &MainWindow::styleChanged, this,
- [this](auto&& file){ setStyleFile(file); },
- Qt::QueuedConnection);
-
-
- log::debug("displaying main window");
- mainWindow.show();
- mainWindow.activateWindow();
- splash.close();
-
- tt.stop();
-
- res = exec();
- mainWindow.close();
-
- // main window is about to be destroyed
- m_nexus->getAccessManager()->setTopLevelWidget(nullptr);
- }
-
- // reset geometry if the flag was set from the settings dialog
- m_settings->geometry().resetIfNeeded();
-
- return res;
-}
-
-void MOApplication::externalMessage(const QString& message)
-{
- log::debug("received external message '{}'", message);
-
- MOShortcut moshortcut(message);
-
- if (moshortcut.isValid()) {
- if(moshortcut.hasExecutable()) {
- try {
- m_core->processRunner()
- .setFromShortcut(moshortcut)
- .setWaitForCompletion(ProcessRunner::TriggerRefresh)
- .run();
- } catch(std::exception&) {
- // user was already warned
- }
- }
- } else if (isNxmLink(message)) {
- MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false);
- m_core->downloadRequestedNXM(message);
- } else {
- cl::CommandLine cl;
-
- if (auto r=cl.process(message.toStdWString())) {
- log::debug(
- "while processing external message, command line wants to "
- "exit; ignoring");
-
- return;
- }
-
- if (auto i=cl.instance()) {
- const auto ci = InstanceManager::singleton().currentInstance();
-
- if (*i != ci->displayName()) {
- reportError(tr(
- "This shortcut or command line is for instance '%1', but the current "
- "instance is '%2'.")
- .arg(*i).arg(ci->displayName()));
-
- return;
- }
- }
-
- if (auto p=cl.profile()) {
- if (*p != m_core->profileName()) {
- reportError(tr(
- "This shortcut or command line is for profile '%1', but the current "
- "profile is '%2'.")
- .arg(*p).arg(m_core->profileName()));
-
- return;
- }
- }
-
- cl.runPostOrganizer(*m_core);
- }
-}
-
-std::unique_ptr<Instance> MOApplication::getCurrentInstance(bool forceSelect)
-{
- auto& m = InstanceManager::singleton();
- auto currentInstance = m.currentInstance();
-
- if (forceSelect || !currentInstance)
- {
- // clear any overrides that might have been given on the command line
- m.clearOverrides();
- currentInstance = selectInstance();
- }
- else
- {
- if (!QDir(currentInstance->directory()).exists()) {
- // the previously used instance doesn't exist anymore
-
- // clear any overrides that might have been given on the command line
- m.clearOverrides();
-
- if (m.hasAnyInstances()) {
- reportError(QObject::tr(
- "Instance at '%1' not found. Select another instance.")
- .arg(currentInstance->directory()));
- } else {
- reportError(QObject::tr(
- "Instance at '%1' not found. You must create a new instance")
- .arg(currentInstance->directory()));
- }
-
- currentInstance = selectInstance();
- }
- }
-
- return currentInstance;
-}
-
-std::optional<int> MOApplication::setupInstanceLoop(
- Instance& currentInstance, PluginContainer& pc)
-{
- for (;;)
- {
- const auto setupResult = setupInstance(currentInstance, pc);
-
- if (setupResult == SetupInstanceResults::Okay) {
- return {};
- } else if (setupResult == SetupInstanceResults::TryAgain) {
- continue;
- } else if (setupResult == SetupInstanceResults::SelectAnother) {
- InstanceManager::singleton().clearCurrentInstance();
- return ReselectExitCode;
- } else {
- return 1;
- }
- }
-}
-
-void MOApplication::purgeOldFiles()
-{
- // remove the temporary backup directory in case we're restarting after an
- // update
- QString backupDirectory = qApp->applicationDirPath() + "/update_backup";
- if (QDir(backupDirectory).exists()) {
- shellDelete(QStringList(backupDirectory));
- }
-
- // cycle log file
- removeOldFiles(
- qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()),
- "usvfs*.log", 5, QDir::Name);
-}
-
-void MOApplication::resetForRestart()
-{
- LogModel::instance().clear();
- ResetExitFlag();
-
- // make sure the log file isn't locked in case MO was restarted and
- // the previous instance gets deleted
- log::getDefault().setFile({});
-
- // clear instance and profile overrides
- InstanceManager::singleton().clearOverrides();
-
- m_core = {};
- m_plugins = {};
- m_nexus = {};
- m_settings = {};
- m_instance = {};
-}
-
-bool MOApplication::setStyleFile(const QString& styleName)
-{
- // remove all files from watch
- QStringList currentWatch = m_styleWatcher.files();
- if (currentWatch.count() != 0) {
- m_styleWatcher.removePaths(currentWatch);
- }
- // set new stylesheet or clear it
- if (styleName.length() != 0) {
- QString styleSheetName = applicationDirPath() + "/" + MOBase::ToQString(AppConfig::stylesheetsPath()) + "/" + styleName;
- if (QFile::exists(styleSheetName)) {
- m_styleWatcher.addPath(styleSheetName);
- updateStyle(styleSheetName);
- } else {
- updateStyle(styleName);
- }
- } else {
- setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle)));
- setStyleSheet("");
- }
- return true;
-}
-
-bool MOApplication::notify(QObject* receiver, QEvent* event)
-{
- try {
- return QApplication::notify(receiver, event);
- } catch (const std::exception &e) {
- log::error(
- "uncaught exception in handler (object {}, eventtype {}): {}",
- receiver->objectName(), event->type(), e.what());
- reportError(tr("an error occurred: %1").arg(e.what()));
- return false;
- } catch (...) {
- log::error(
- "uncaught non-std exception in handler (object {}, eventtype {})",
- receiver->objectName(), event->type());
- reportError(tr("an error occurred"));
- return false;
- }
-}
-
-void MOApplication::updateStyle(const QString& fileName)
-{
- if (QStyleFactory::keys().contains(fileName)) {
- setStyleSheet("");
- setStyle(new ProxyStyle(QStyleFactory::create(fileName)));
- } else {
- setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle)));
- if (QFile::exists(fileName)) {
- setStyleSheet(QString("file:///%1").arg(fileName));
- } else {
- log::warn("invalid stylesheet: {}", fileName);
- }
- }
-}
-
-
-MOSplash::MOSplash(
- const Settings& settings, const QString& dataPath,
- const MOBase::IPluginGame* game)
-{
- const auto splashPath = getSplashPath(settings, dataPath, game);
- if (splashPath.isEmpty()) {
- return;
- }
-
- QPixmap image(splashPath);
- if (image.isNull()) {
- log::error("failed to load splash from {}", splashPath);
- return;
- }
-
- ss_.reset(new QSplashScreen(image));
- settings.geometry().centerOnMainWindowMonitor(ss_.get());
-
- ss_->show();
- ss_->activateWindow();
-}
-
-void MOSplash::close()
-{
- if (ss_) {
- // don't pass mainwindow as it just waits half a second for it
- // instead of proceding
- ss_->finish(nullptr);
- }
-}
-
-QString MOSplash::getSplashPath(
- const Settings& settings, const QString& dataPath,
- const MOBase::IPluginGame* game) const
-{
- if (!settings.useSplash()) {
- return {};
- }
-
- // try splash from instance directory
- const QString splashPath = dataPath + "/splash.png";
- if (QFile::exists(dataPath + "/splash.png")) {
- QImage image(splashPath);
- if (!image.isNull()) {
- return splashPath;
- }
- }
-
- // try splash from plugin
- QString pluginSplash = QString(":/%1/splash").arg(game->gameShortName());
- if (QFile::exists(pluginSplash)) {
- QImage image(pluginSplash);
- if (!image.isNull()) {
- image.save(splashPath);
- return pluginSplash;
- }
- }
-
- // try default splash from resource
- QString defaultSplash = ":/MO/gui/splash";
- if (QFile::exists(defaultSplash)) {
- QImage image(defaultSplash);
- if (!image.isNull()) {
- return defaultSplash;
- }
- }
-
- return splashPath;
-}
+/*
+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 "moapplication.h"
+#include "settings.h"
+#include "commandline.h"
+#include "instancemanager.h"
+#include "organizercore.h"
+#include "thread_utils.h"
+#include "loglist.h"
+#include "multiprocess.h"
+#include "nexusinterface.h"
+#include "nxmaccessmanager.h"
+#include "tutorialmanager.h"
+#include "sanitychecks.h"
+#include "mainwindow.h"
+#include "messagedialog.h"
+#include "shared/util.h"
+#include <iplugingame.h>
+#include <report.h>
+#include <utility.h>
+#include <log.h>
+#include "shared/appconfig.h"
+#include <QFile>
+#include <QStringList>
+#include <QProxyStyle>
+#include <QStyleFactory>
+#include <QPainter>
+#include <QStyleOption>
+#include <QDebug>
+#include <QSSLSocket>
+
+// see addDllsToPath() below
+#pragma comment(linker, "/manifestDependency:\"" \
+ "name='dlls' " \
+ "processorArchitecture='x86' " \
+ "version='1.0.0.0' " \
+ "type='win32' \"")
+
+using namespace MOBase;
+using namespace MOShared;
+
+// style proxy that changes the appearance of drop indicators
+//
+class ProxyStyle : public QProxyStyle {
+public:
+ ProxyStyle(QStyle* baseStyle = 0)
+ : QProxyStyle(baseStyle)
+ {
+ }
+
+ void drawPrimitive(
+ PrimitiveElement element, const QStyleOption* option,
+ QPainter* painter, const QWidget* widget) const override
+ {
+ if (element == QStyle::PE_IndicatorItemViewItemDrop) {
+
+ // 0. Fix a bug that made the drop indicator sometimes appear on top
+ // of the mod list when selecting a mod.
+ if (option->rect.height() == 0
+ && option->rect.bottomRight() == QPoint(-1, -1)) {
+ return;
+ }
+
+ // 1. full-width drop indicator
+ QRect rect(option->rect);
+ if (auto* view = qobject_cast<const QTreeView*>(widget)) {
+ rect.setLeft(view->indentation());
+ rect.setRight(widget->width());
+ }
+
+ // 2. stylish drop indicator
+ painter->setRenderHint(QPainter::Antialiasing, true);
+
+ QColor col(option->palette.windowText().color());
+ QPen pen(col);
+ pen.setWidth(2);
+ col.setAlpha(50);
+
+ painter->setPen(pen);
+ painter->setBrush(QBrush(col));
+ if (rect.height() == 0) {
+ QPoint tri[3] = {
+ rect.topLeft(),
+ rect.topLeft() + QPoint(-5, 5),
+ rect.topLeft() + QPoint(-5, -5)
+ };
+ painter->drawPolygon(tri, 3);
+ painter->drawLine(rect.topLeft(), rect.topRight());
+ }
+ else {
+ painter->drawRoundedRect(rect, 5, 5);
+ }
+ }
+ else {
+ QProxyStyle::drawPrimitive(element, option, painter, widget);
+ }
+ }
+
+};
+
+
+// This adds the `dlls` directory to the path so the dlls can be found. How
+// MO is able to find dlls in there is a bit convoluted:
+//
+// Dependencies on DLLs can be baked into an executable by passing a
+// `manifestdependency` option to the linker. This can be done on the command
+// line or with a pragma. Typically, the dependency will not be a hardcoded
+// filename, but an assembly name, such as Microsoft.Windows.Common-Controls.
+//
+// When Windows loads the exe, it will look for this assembly in a variety of
+// places, such as in the WinSxS folder, but also in the program's folder. It
+// will look for `assemblyname.dll` or `assemblyname/assemblyname.dll` and try
+// to load that.
+//
+// If these files don't exist, then the loader gets creative and looks for
+// `assemblyname.manifest` and `assemblyname/assemblyname.manifest`. A manifest
+// file is just an XML file that can contain a list of DLLs to load for this
+// assembly.
+//
+// In MO's case, there's a `pragma` at the beginning of this file which adds
+// `dlls` as an "assembly" dependency. This is a bit of a hack to just force
+// the loader to eventually find `dlls/dlls.manifest`, which contains the list
+// of all the DLLs MO requires to load.
+//
+// This file was handwritten in `modorganizer/src/dlls.manifest.qt5` and
+// is copied and renamed in CMakeLists.txt into `bin/dlls/dlls.manifest`. Note
+// that the useless and incorrect .qt5 extension is removed.
+//
+void addDllsToPath()
+{
+ const auto dllsPath = QDir::toNativeSeparators(
+ QCoreApplication::applicationDirPath() + "/dlls");
+
+ QCoreApplication::setLibraryPaths(
+ QStringList(dllsPath) + QCoreApplication::libraryPaths());
+
+ env::prependToPath(dllsPath);
+}
+
+
+MOApplication::MOApplication(int& argc, char** argv)
+ : QApplication(argc, argv)
+{
+ TimeThis tt("MOApplication()");
+
+ qputenv("QML_DISABLE_DISK_CACHE", "true");
+
+ connect(&m_styleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){
+ log::debug("style file '{}' changed, reloading", file);
+ updateStyle(file);
+ });
+
+ m_defaultStyle = style()->objectName();
+ setStyle(new ProxyStyle(style()));
+ addDllsToPath();
+}
+
+OrganizerCore& MOApplication::core()
+{
+ return *m_core;
+}
+
+void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess)
+{
+ connect(
+ &multiProcess, &MOMultiProcess::messageSent, this,
+ [this](auto&& s){ externalMessage(s); },
+ Qt::QueuedConnection);
+}
+
+int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
+{
+ TimeThis tt("MOApplication setup()");
+
+ // makes plugin data path available to plugins, see
+ // IOrganizer::getPluginDataPath()
+ MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath());
+
+ // figuring out the current instance
+ m_instance = getCurrentInstance(forceSelect);
+ if (!m_instance) {
+ return 1;
+ }
+
+ // first time the data path is available, set the global property and log
+ // directory, then log a bunch of debug stuff
+ const QString dataPath = m_instance->directory();
+ setProperty("dataPath", dataPath);
+
+ if (!setLogDirectory(dataPath)) {
+ reportError(tr("Failed to create log folder."));
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+
+ log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
+
+ log::info(
+ "starting Mod Organizer version {} revision {} in {}, usvfs: {}",
+ createVersionInfo().displayString(3), GITID,
+ QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString());
+
+ if (multiProcess.secondary()) {
+ log::debug("another instance of MO is running but --multiple was given");
+ }
+
+ log::info("data path: {}", m_instance->directory());
+ log::info("working directory: {}", QDir::currentPath());
+
+
+ tt.start("MOApplication::doOneRun() settings");
+
+ // deleting old files, only for the main instance
+ if (!multiProcess.secondary()) {
+ purgeOldFiles();
+ }
+
+ // loading settings
+ m_settings.reset(new Settings(m_instance->iniPath(), true));
+ log::getDefault().setLevel(m_settings->diagnostics().logLevel());
+ log::debug("using ini at '{}'", m_settings->filename());
+
+ OrganizerCore::setGlobalCoreDumpType(m_settings->diagnostics().coreDumpType());
+
+
+ tt.start("MOApplication::doOneRun() log and checks");
+
+ // logging and checking
+ env::Environment env;
+ env.dump(*m_settings);
+ m_settings->dump();
+ sanity::checkEnvironment(env);
+
+ m_modules = std::move(env.onModuleLoaded(qApp, [](auto&& m) {
+ if (m.interesting()) {
+ log::debug("loaded module {}", m.toString());
+ }
+
+ sanity::checkIncompatibleModule(m);
+ }));
+
+ auto sslBuildVersion = QSslSocket::sslLibraryBuildVersionString();
+ auto sslVersion = QSslSocket::sslLibraryVersionString();
+ log::debug("SSL Build Version: {}, SSL Runtime Version {}", sslBuildVersion, sslVersion);
+
+ // nexus interface
+ tt.start("MOApplication::doOneRun() NexusInterface");
+ log::debug("initializing nexus interface");
+ m_nexus.reset(new NexusInterface(m_settings.get()));
+
+ // organizer core
+ tt.start("MOApplication::doOneRun() OrganizerCore");
+ log::debug("initializing core");
+
+ m_core.reset(new OrganizerCore(*m_settings));
+ if (!m_core->bootstrap()) {
+ reportError(tr("Failed to set up data paths."));
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+
+ // plugins
+ tt.start("MOApplication::doOneRun() plugins");
+ log::debug("initializing plugins");
+
+ m_plugins = std::make_unique<PluginContainer>(m_core.get());
+ m_plugins->loadPlugins();
+
+ // instance
+ if (auto r=setupInstanceLoop(*m_instance, *m_plugins)) {
+ return *r;
+ }
+
+ if (m_instance->isPortable()) {
+ log::debug("this is a portable instance");
+ }
+
+ tt.start("MOApplication::doOneRun() OrganizerCore setup");
+
+ sanity::checkPaths(*m_instance->gamePlugin(), *m_settings);
+
+ // setting up organizer core
+ m_core->setManagedGame(m_instance->gamePlugin());
+ m_core->createDefaultProfile();
+
+ log::info(
+ "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}",
+ m_instance->gamePlugin()->gameName(),
+ m_instance->gamePlugin()->gameShortName(),
+ (m_settings->game().edition().value_or("").isEmpty() ?
+ "(none)" : *m_settings->game().edition()),
+ m_instance->gamePlugin()->steamAPPId(),
+ m_instance->gamePlugin()->gameDirectory().absolutePath());
+
+ CategoryFactory::instance().loadCategories();
+ m_core->updateExecutablesList();
+ m_core->updateModInfoFromDisc();
+ m_core->setCurrentProfile(m_instance->profileName());
+
+ return 0;
+}
+
+int MOApplication::run(MOMultiProcess& multiProcess)
+{
+ // checking command line
+ TimeThis tt("MOApplication::run()");
+
+ // show splash
+ tt.start("MOApplication::doOneRun() splash");
+
+ MOSplash splash(*m_settings, m_instance->directory(), m_instance->gamePlugin());
+
+ tt.start("MOApplication::doOneRun() finishing");
+
+ // start an api check
+ QString apiKey;
+ if (GlobalSettings::nexusApiKey(apiKey)) {
+ m_nexus->getAccessManager()->apiCheck(apiKey);
+ }
+
+ // tutorials
+ log::debug("initializing tutorials");
+ TutorialManager::init(
+ qApp->applicationDirPath() + "/"
+ + QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
+ m_core.get());
+
+ // styling
+ if (!setStyleFile(m_settings->interface().styleName().value_or(""))) {
+ // disable invalid stylesheet
+ m_settings->interface().setStyleName("");
+ }
+
+
+ int res = 1;
+
+ {
+ tt.start("MOApplication::doOneRun() MainWindow setup");
+ MainWindow mainWindow(*m_settings, *m_core, *m_plugins);
+
+ // the nexus interface can show dialogs, make sure they're parented to the
+ // main window
+ m_nexus->getAccessManager()->setTopLevelWidget(&mainWindow);
+
+ connect(
+ &mainWindow, &MainWindow::styleChanged, this,
+ [this](auto&& file){ setStyleFile(file); },
+ Qt::QueuedConnection);
+
+
+ log::debug("displaying main window");
+ mainWindow.show();
+ mainWindow.activateWindow();
+ splash.close();
+
+ tt.stop();
+
+ res = exec();
+ mainWindow.close();
+
+ // main window is about to be destroyed
+ m_nexus->getAccessManager()->setTopLevelWidget(nullptr);
+ }
+
+ // reset geometry if the flag was set from the settings dialog
+ m_settings->geometry().resetIfNeeded();
+
+ return res;
+}
+
+void MOApplication::externalMessage(const QString& message)
+{
+ log::debug("received external message '{}'", message);
+
+ MOShortcut moshortcut(message);
+
+ if (moshortcut.isValid()) {
+ if(moshortcut.hasExecutable()) {
+ try {
+ m_core->processRunner()
+ .setFromShortcut(moshortcut)
+ .setWaitForCompletion(ProcessRunner::TriggerRefresh)
+ .run();
+ } catch(std::exception&) {
+ // user was already warned
+ }
+ }
+ } else if (isNxmLink(message)) {
+ MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false);
+ m_core->downloadRequestedNXM(message);
+ } else {
+ cl::CommandLine cl;
+
+ if (auto r=cl.process(message.toStdWString())) {
+ log::debug(
+ "while processing external message, command line wants to "
+ "exit; ignoring");
+
+ return;
+ }
+
+ if (auto i=cl.instance()) {
+ const auto ci = InstanceManager::singleton().currentInstance();
+
+ if (*i != ci->displayName()) {
+ reportError(tr(
+ "This shortcut or command line is for instance '%1', but the current "
+ "instance is '%2'.")
+ .arg(*i).arg(ci->displayName()));
+
+ return;
+ }
+ }
+
+ if (auto p=cl.profile()) {
+ if (*p != m_core->profileName()) {
+ reportError(tr(
+ "This shortcut or command line is for profile '%1', but the current "
+ "profile is '%2'.")
+ .arg(*p).arg(m_core->profileName()));
+
+ return;
+ }
+ }
+
+ cl.runPostOrganizer(*m_core);
+ }
+}
+
+std::unique_ptr<Instance> MOApplication::getCurrentInstance(bool forceSelect)
+{
+ auto& m = InstanceManager::singleton();
+ auto currentInstance = m.currentInstance();
+
+ if (forceSelect || !currentInstance)
+ {
+ // clear any overrides that might have been given on the command line
+ m.clearOverrides();
+ currentInstance = selectInstance();
+ }
+ else
+ {
+ if (!QDir(currentInstance->directory()).exists()) {
+ // the previously used instance doesn't exist anymore
+
+ // clear any overrides that might have been given on the command line
+ m.clearOverrides();
+
+ if (m.hasAnyInstances()) {
+ reportError(QObject::tr(
+ "Instance at '%1' not found. Select another instance.")
+ .arg(currentInstance->directory()));
+ } else {
+ reportError(QObject::tr(
+ "Instance at '%1' not found. You must create a new instance")
+ .arg(currentInstance->directory()));
+ }
+
+ currentInstance = selectInstance();
+ }
+ }
+
+ return currentInstance;
+}
+
+std::optional<int> MOApplication::setupInstanceLoop(
+ Instance& currentInstance, PluginContainer& pc)
+{
+ for (;;)
+ {
+ const auto setupResult = setupInstance(currentInstance, pc);
+
+ if (setupResult == SetupInstanceResults::Okay) {
+ return {};
+ } else if (setupResult == SetupInstanceResults::TryAgain) {
+ continue;
+ } else if (setupResult == SetupInstanceResults::SelectAnother) {
+ InstanceManager::singleton().clearCurrentInstance();
+ return ReselectExitCode;
+ } else {
+ return 1;
+ }
+ }
+}
+
+void MOApplication::purgeOldFiles()
+{
+ // remove the temporary backup directory in case we're restarting after an
+ // update
+ QString backupDirectory = qApp->applicationDirPath() + "/update_backup";
+ if (QDir(backupDirectory).exists()) {
+ shellDelete(QStringList(backupDirectory));
+ }
+
+ // cycle log file
+ removeOldFiles(
+ qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()),
+ "usvfs*.log", 5, QDir::Name);
+}
+
+void MOApplication::resetForRestart()
+{
+ LogModel::instance().clear();
+ ResetExitFlag();
+
+ // make sure the log file isn't locked in case MO was restarted and
+ // the previous instance gets deleted
+ log::getDefault().setFile({});
+
+ // clear instance and profile overrides
+ InstanceManager::singleton().clearOverrides();
+
+ m_core = {};
+ m_plugins = {};
+ m_nexus = {};
+ m_settings = {};
+ m_instance = {};
+}
+
+bool MOApplication::setStyleFile(const QString& styleName)
+{
+ // remove all files from watch
+ QStringList currentWatch = m_styleWatcher.files();
+ if (currentWatch.count() != 0) {
+ m_styleWatcher.removePaths(currentWatch);
+ }
+ // set new stylesheet or clear it
+ if (styleName.length() != 0) {
+ QString styleSheetName = applicationDirPath() + "/" + MOBase::ToQString(AppConfig::stylesheetsPath()) + "/" + styleName;
+ if (QFile::exists(styleSheetName)) {
+ m_styleWatcher.addPath(styleSheetName);
+ updateStyle(styleSheetName);
+ } else {
+ updateStyle(styleName);
+ }
+ } else {
+ setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle)));
+ setStyleSheet("");
+ }
+ return true;
+}
+
+bool MOApplication::notify(QObject* receiver, QEvent* event)
+{
+ try {
+ return QApplication::notify(receiver, event);
+ } catch (const std::exception &e) {
+ log::error(
+ "uncaught exception in handler (object {}, eventtype {}): {}",
+ receiver->objectName(), event->type(), e.what());
+ reportError(tr("an error occurred: %1").arg(e.what()));
+ return false;
+ } catch (...) {
+ log::error(
+ "uncaught non-std exception in handler (object {}, eventtype {})",
+ receiver->objectName(), event->type());
+ reportError(tr("an error occurred"));
+ return false;
+ }
+}
+
+void MOApplication::updateStyle(const QString& fileName)
+{
+ if (QStyleFactory::keys().contains(fileName)) {
+ setStyleSheet("");
+ setStyle(new ProxyStyle(QStyleFactory::create(fileName)));
+ } else {
+ setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle)));
+ if (QFile::exists(fileName)) {
+ setStyleSheet(QString("file:///%1").arg(fileName));
+ } else {
+ log::warn("invalid stylesheet: {}", fileName);
+ }
+ }
+}
+
+
+MOSplash::MOSplash(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game)
+{
+ const auto splashPath = getSplashPath(settings, dataPath, game);
+ if (splashPath.isEmpty()) {
+ return;
+ }
+
+ QPixmap image(splashPath);
+ if (image.isNull()) {
+ log::error("failed to load splash from {}", splashPath);
+ return;
+ }
+
+ ss_.reset(new QSplashScreen(image));
+ settings.geometry().centerOnMainWindowMonitor(ss_.get());
+
+ ss_->show();
+ ss_->activateWindow();
+}
+
+void MOSplash::close()
+{
+ if (ss_) {
+ // don't pass mainwindow as it just waits half a second for it
+ // instead of proceding
+ ss_->finish(nullptr);
+ }
+}
+
+QString MOSplash::getSplashPath(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game) const
+{
+ if (!settings.useSplash()) {
+ return {};
+ }
+
+ // try splash from instance directory
+ const QString splashPath = dataPath + "/splash.png";
+ if (QFile::exists(dataPath + "/splash.png")) {
+ QImage image(splashPath);
+ if (!image.isNull()) {
+ return splashPath;
+ }
+ }
+
+ // try splash from plugin
+ QString pluginSplash = QString(":/%1/splash").arg(game->gameShortName());
+ if (QFile::exists(pluginSplash)) {
+ QImage image(pluginSplash);
+ if (!image.isNull()) {
+ image.save(splashPath);
+ return pluginSplash;
+ }
+ }
+
+ // try default splash from resource
+ QString defaultSplash = ":/MO/gui/splash";
+ if (QFile::exists(defaultSplash)) {
+ QImage image(defaultSplash);
+ if (!image.isNull()) {
+ return defaultSplash;
+ }
+ }
+
+ return splashPath;
+}
diff --git a/src/moapplication.h b/src/moapplication.h
index 7e8a8c6f..91090764 100644
--- a/src/moapplication.h
+++ b/src/moapplication.h
@@ -1,111 +1,111 @@
-/*
-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 MOAPPLICATION_H
-#define MOAPPLICATION_H
-
-#include <QApplication>
-#include <QFileSystemWatcher>
-#include "env.h"
-
-class Settings;
-class MOMultiProcess;
-class Instance;
-class PluginContainer;
-class OrganizerCore;
-class NexusInterface;
-
-namespace MOBase { class IPluginGame; }
-
-class MOApplication : public QApplication
-{
- Q_OBJECT
-
-public:
- MOApplication(int& argc, char** argv);
-
- // called from main() only once for stuff that persists across "restarts"
- //
- void firstTimeSetup(MOMultiProcess& multiProcess);
-
- // called from main() each time MO "restarts", loads settings, plugins,
- // OrganizerCore and the current instance
- //
- int setup(MOMultiProcess& multiProcess, bool forceSelect);
-
- // shows splash, starts an api check, shows the main window and blocks until
- // MO exits
- //
- int run(MOMultiProcess& multiProcess);
-
- // called from main() when MO "restarts", must clean up everything so setup()
- // starts fresh
- //
- void resetForRestart();
-
- // undefined if setup() wasn't called
- //
- OrganizerCore& core();
-
- // wraps QApplication::notify() in a catch, reports errors and ignores them
- //
- bool notify(QObject* receiver, QEvent* event) override;
-
-public slots:
- bool setStyleFile(const QString& style);
-
-private slots:
- void updateStyle(const QString& fileName);
-
-private:
- QFileSystemWatcher m_styleWatcher;
- QString m_defaultStyle;
- std::unique_ptr<env::ModuleNotification> m_modules;
-
- std::unique_ptr<Instance> m_instance;
- std::unique_ptr<Settings> m_settings;
- std::unique_ptr<NexusInterface> m_nexus;
- std::unique_ptr<PluginContainer> m_plugins;
- std::unique_ptr<OrganizerCore> m_core;
-
- void externalMessage(const QString& message);
- std::unique_ptr<Instance> getCurrentInstance(bool forceSelect);
- std::optional<int> setupInstanceLoop(Instance& currentInstance, PluginContainer& pc);
- void purgeOldFiles();
-};
-
-
-class MOSplash
-{
-public:
- MOSplash(
- const Settings& settings, const QString& dataPath,
- const MOBase::IPluginGame* game);
-
- void close();
-
-private:
- std::unique_ptr<QSplashScreen> ss_;
-
- QString getSplashPath(
- const Settings& settings, const QString& dataPath,
- const MOBase::IPluginGame* game) const;
-};
-
-#endif // MOAPPLICATION_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 MOAPPLICATION_H
+#define MOAPPLICATION_H
+
+#include <QApplication>
+#include <QFileSystemWatcher>
+#include "env.h"
+
+class Settings;
+class MOMultiProcess;
+class Instance;
+class PluginContainer;
+class OrganizerCore;
+class NexusInterface;
+
+namespace MOBase { class IPluginGame; }
+
+class MOApplication : public QApplication
+{
+ Q_OBJECT
+
+public:
+ MOApplication(int& argc, char** argv);
+
+ // called from main() only once for stuff that persists across "restarts"
+ //
+ void firstTimeSetup(MOMultiProcess& multiProcess);
+
+ // called from main() each time MO "restarts", loads settings, plugins,
+ // OrganizerCore and the current instance
+ //
+ int setup(MOMultiProcess& multiProcess, bool forceSelect);
+
+ // shows splash, starts an api check, shows the main window and blocks until
+ // MO exits
+ //
+ int run(MOMultiProcess& multiProcess);
+
+ // called from main() when MO "restarts", must clean up everything so setup()
+ // starts fresh
+ //
+ void resetForRestart();
+
+ // undefined if setup() wasn't called
+ //
+ OrganizerCore& core();
+
+ // wraps QApplication::notify() in a catch, reports errors and ignores them
+ //
+ bool notify(QObject* receiver, QEvent* event) override;
+
+public slots:
+ bool setStyleFile(const QString& style);
+
+private slots:
+ void updateStyle(const QString& fileName);
+
+private:
+ QFileSystemWatcher m_styleWatcher;
+ QString m_defaultStyle;
+ std::unique_ptr<env::ModuleNotification> m_modules;
+
+ std::unique_ptr<Instance> m_instance;
+ std::unique_ptr<Settings> m_settings;
+ std::unique_ptr<NexusInterface> m_nexus;
+ std::unique_ptr<PluginContainer> m_plugins;
+ std::unique_ptr<OrganizerCore> m_core;
+
+ void externalMessage(const QString& message);
+ std::unique_ptr<Instance> getCurrentInstance(bool forceSelect);
+ std::optional<int> setupInstanceLoop(Instance& currentInstance, PluginContainer& pc);
+ void purgeOldFiles();
+};
+
+
+class MOSplash
+{
+public:
+ MOSplash(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game);
+
+ void close();
+
+private:
+ std::unique_ptr<QSplashScreen> ss_;
+
+ QString getSplashPath(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game) const;
+};
+
+#endif // MOAPPLICATION_H
diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp
index 30a3c376..43976999 100644
--- a/src/modflagicondelegate.cpp
+++ b/src/modflagicondelegate.cpp
@@ -1,93 +1,93 @@
-#include "modflagicondelegate.h"
-#include "modlist.h"
-#include "modlistview.h"
-#include <log.h>
-#include <QList>
-
-using namespace MOBase;
-
-ModFlagIconDelegate::ModFlagIconDelegate(ModListView* view, int column, int compactSize)
- : IconDelegate(view, column, compactSize), m_view(view)
-{
-}
-
-QList<QString> ModFlagIconDelegate::getIconsForFlags(
- std::vector<ModInfo::EFlag> flags, bool compact)
-{
- QList<QString> result;
-
- // Don't do flags for overwrite
- if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())
- return result;
-
- for (auto iter = flags.begin(); iter != flags.end(); ++iter) {
- auto iconPath = getFlagIcon(*iter);
- if (!iconPath.isEmpty())
- result.append(iconPath);
- }
-
- return result;
-}
-
-QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const
-{
- QVariant modid = index.data(ModList::IndexRole);
-
- if (modid.isValid()) {
- bool compact;
- auto flags = m_view->modFlags(index, &compact);
- return getIconsForFlags(flags, compact || this->compact());
- }
-
- return {};
-}
-
-QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag)
-{
- switch (flag) {
- case ModInfo::FLAG_BACKUP: return QStringLiteral(":/MO/gui/emblem_backup");
- case ModInfo::FLAG_INVALID: return QStringLiteral(":/MO/gui/problem");
- case ModInfo::FLAG_NOTENDORSED: return QStringLiteral(":/MO/gui/emblem_notendorsed");
- case ModInfo::FLAG_NOTES: return QStringLiteral(":/MO/gui/emblem_notes");
- case ModInfo::FLAG_HIDDEN_FILES: return QStringLiteral(":/MO/gui/emblem_hidden_files");
- case ModInfo::FLAG_ALTERNATE_GAME: return QStringLiteral(":/MO/gui/alternate_game");
- case ModInfo::FLAG_FOREIGN: return QString();
- case ModInfo::FLAG_SEPARATOR: return QString();
- case ModInfo::FLAG_OVERWRITE: return QString();
- case ModInfo::FLAG_PLUGIN_SELECTED: return QString();
- case ModInfo::FLAG_TRACKED: return QStringLiteral(":/MO/gui/tracked");
- default:
- log::warn("ModInfo flag {} has no defined icon", flag);
- return QString();
- }
-}
-
-size_t ModFlagIconDelegate::getNumIcons(const QModelIndex &index) const
-{
- unsigned int modIdx = index.data(ModList::IndexRole).toInt();
- if (modIdx < ModInfo::getNumMods()) {
- ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
- std::vector<ModInfo::EFlag> flags = info->getFlags();
- return flags.size();
- } else {
- return 0;
- }
-}
-
-
-QSize ModFlagIconDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &modelIndex) const
-{
- size_t count = getNumIcons(modelIndex);
- unsigned int index = modelIndex.data(ModList::IndexRole).toInt();
- QSize result;
- if (index < ModInfo::getNumMods()) {
- result = QSize(static_cast<int>(count) * 40, 20);
- } else {
- result = QSize(1, 20);
- }
- if (option.rect.width() > 0) {
- result.setWidth(std::min(option.rect.width(), result.width()));
- }
- return result;
-}
-
+#include "modflagicondelegate.h"
+#include "modlist.h"
+#include "modlistview.h"
+#include <log.h>
+#include <QList>
+
+using namespace MOBase;
+
+ModFlagIconDelegate::ModFlagIconDelegate(ModListView* view, int column, int compactSize)
+ : IconDelegate(view, column, compactSize), m_view(view)
+{
+}
+
+QList<QString> ModFlagIconDelegate::getIconsForFlags(
+ std::vector<ModInfo::EFlag> flags, bool compact)
+{
+ QList<QString> result;
+
+ // Don't do flags for overwrite
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())
+ return result;
+
+ for (auto iter = flags.begin(); iter != flags.end(); ++iter) {
+ auto iconPath = getFlagIcon(*iter);
+ if (!iconPath.isEmpty())
+ result.append(iconPath);
+ }
+
+ return result;
+}
+
+QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const
+{
+ QVariant modid = index.data(ModList::IndexRole);
+
+ if (modid.isValid()) {
+ bool compact;
+ auto flags = m_view->modFlags(index, &compact);
+ return getIconsForFlags(flags, compact || this->compact());
+ }
+
+ return {};
+}
+
+QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag)
+{
+ switch (flag) {
+ case ModInfo::FLAG_BACKUP: return QStringLiteral(":/MO/gui/emblem_backup");
+ case ModInfo::FLAG_INVALID: return QStringLiteral(":/MO/gui/problem");
+ case ModInfo::FLAG_NOTENDORSED: return QStringLiteral(":/MO/gui/emblem_notendorsed");
+ case ModInfo::FLAG_NOTES: return QStringLiteral(":/MO/gui/emblem_notes");
+ case ModInfo::FLAG_HIDDEN_FILES: return QStringLiteral(":/MO/gui/emblem_hidden_files");
+ case ModInfo::FLAG_ALTERNATE_GAME: return QStringLiteral(":/MO/gui/alternate_game");
+ case ModInfo::FLAG_FOREIGN: return QString();
+ case ModInfo::FLAG_SEPARATOR: return QString();
+ case ModInfo::FLAG_OVERWRITE: return QString();
+ case ModInfo::FLAG_PLUGIN_SELECTED: return QString();
+ case ModInfo::FLAG_TRACKED: return QStringLiteral(":/MO/gui/tracked");
+ default:
+ log::warn("ModInfo flag {} has no defined icon", flag);
+ return QString();
+ }
+}
+
+size_t ModFlagIconDelegate::getNumIcons(const QModelIndex &index) const
+{
+ unsigned int modIdx = index.data(ModList::IndexRole).toInt();
+ if (modIdx < ModInfo::getNumMods()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
+ std::vector<ModInfo::EFlag> flags = info->getFlags();
+ return flags.size();
+ } else {
+ return 0;
+ }
+}
+
+
+QSize ModFlagIconDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &modelIndex) const
+{
+ size_t count = getNumIcons(modelIndex);
+ unsigned int index = modelIndex.data(ModList::IndexRole).toInt();
+ QSize result;
+ if (index < ModInfo::getNumMods()) {
+ result = QSize(static_cast<int>(count) * 40, 20);
+ } else {
+ result = QSize(1, 20);
+ }
+ if (option.rect.width() > 0) {
+ result.setWidth(std::min(option.rect.width(), result.width()));
+ }
+ return result;
+}
+
diff --git a/src/modflagicondelegate.h b/src/modflagicondelegate.h
index 24dd4a0e..982bfefd 100644
--- a/src/modflagicondelegate.h
+++ b/src/modflagicondelegate.h
@@ -1,35 +1,35 @@
-#ifndef MODFLAGICONDELEGATE_H
-#define MODFLAGICONDELEGATE_H
-
-#include <QTreeView>
-
-#include "icondelegate.h"
-#include "modinfo.h"
-
-class ModListView;
-
-class ModFlagIconDelegate : public IconDelegate
-{
- Q_OBJECT;
-
-public:
- explicit ModFlagIconDelegate(ModListView* view, int column = -1, int compactSize = 120);
- QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
-
-protected:
- static QList<QString> getIconsForFlags(std::vector<ModInfo::EFlag> flags, bool compact);
- static QString getFlagIcon(ModInfo::EFlag flag);
-
- QList<QString> getIcons(const QModelIndex &index) const override;
- size_t getNumIcons(const QModelIndex &index) const override;
-
- // constructor for color table
- //
- ModFlagIconDelegate() : ModFlagIconDelegate(nullptr) { }
-
-private:
- ModListView* m_view;
-
-};
-
-#endif // MODFLAGICONDELEGATE_H
+#ifndef MODFLAGICONDELEGATE_H
+#define MODFLAGICONDELEGATE_H
+
+#include <QTreeView>
+
+#include "icondelegate.h"
+#include "modinfo.h"
+
+class ModListView;
+
+class ModFlagIconDelegate : public IconDelegate
+{
+ Q_OBJECT;
+
+public:
+ explicit ModFlagIconDelegate(ModListView* view, int column = -1, int compactSize = 120);
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+
+protected:
+ static QList<QString> getIconsForFlags(std::vector<ModInfo::EFlag> flags, bool compact);
+ static QString getFlagIcon(ModInfo::EFlag flag);
+
+ QList<QString> getIcons(const QModelIndex &index) const override;
+ size_t getNumIcons(const QModelIndex &index) const override;
+
+ // constructor for color table
+ //
+ ModFlagIconDelegate() : ModFlagIconDelegate(nullptr) { }
+
+private:
+ ModListView* m_view;
+
+};
+
+#endif // MODFLAGICONDELEGATE_H
diff --git a/src/modidlineedit.cpp b/src/modidlineedit.cpp
index 7d1f0c38..50cb0f0b 100644
--- a/src/modidlineedit.cpp
+++ b/src/modidlineedit.cpp
@@ -16,4 +16,4 @@ bool ModIDLineEdit::event(QEvent *event)
}
return QLineEdit::event(event);
-} \ No newline at end of file
+}
diff --git a/src/modidlineedit.h b/src/modidlineedit.h
index 2a3db36c..8f945a8c 100644
--- a/src/modidlineedit.h
+++ b/src/modidlineedit.h
@@ -19,4 +19,4 @@ signals:
};
-#endif //MODIDLINEEDIT_H \ No newline at end of file
+#endif //MODIDLINEEDIT_H
diff --git a/src/modinfodialog.h b/src/modinfodialog.h
index 6d408f05..f25b740d 100644
--- a/src/modinfodialog.h
+++ b/src/modinfodialog.h
@@ -1,260 +1,260 @@
-/*
-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 MODINFODIALOG_H
-#define MODINFODIALOG_H
-
-
-#include "modinfo.h"
-#include "tutorabledialog.h"
-#include "filerenamer.h"
-#include "modinfodialogfwd.h"
-
-namespace Ui { class ModInfoDialog; }
-namespace MOShared { class FilesOrigin; }
-
-class PluginContainer;
-class OrganizerCore;
-class Settings;
-class ModInfoDialogTab;
-class ModListView;
-
-/**
- * this is a larger dialog used to visualise information about the mod.
- * @todo this would probably a good place for a plugin-system
- **/
-class ModInfoDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT;
-
- // creates a tab, it's a friend because it uses a bunch of member variables
- // to create ModInfoDialogTabContext
- //
- template <class T>
- friend std::unique_ptr<ModInfoDialogTab> createTab(
- ModInfoDialog& d, ModInfoTabIDs index);
-
-public:
- ModInfoDialog(
- OrganizerCore& core, PluginContainer& plugin,
- ModInfo::Ptr mod, ModListView* view, QWidget* parent = nullptr);
-
- ~ModInfoDialog();
-
- // switches to the tab with the given id
- //
- void selectTab(ModInfoTabIDs id);
-
- // updates all tabs, selects the initial tab, opens the dialog and
- // saves/restores geometry
- //
- int exec() override;
-
-signals:
- // emitted when a tab changes the origin
- //
- void originModified(int originID);
-
- // emitted when the mod of the dialog is changed
- //
- void modChanged(unsigned int modIndex);
-
-protected:
- // forwards to tryClose()
- //
- void closeEvent(QCloseEvent* e);
-
-private:
- // represents a single tab
- //
- struct TabInfo
- {
- // tab implementation
- std::unique_ptr<ModInfoDialogTab> tab;
-
- // actual position in the tab bar, updated every time a tab is moved
- int realPos;
-
- // widget used by the QTabWidget for this tab
- //
- // because QTabWidget doesn't support simply hiding tabs, they have to be
- // completely removed from the widget when they don't support the current
- // mod
- //
- // therefore, `widget, `caption` and `icon` are remembered so tabs can be
- // removed and re-added when navigating between mods
- //
- // `widget` is also used figure out which tab is where when they're
- // re-ordered
- QWidget* widget;
-
- // caption for this tab, see `widget`
- QString caption;
-
- // icon for this tab, see `widget`
- QIcon icon;
-
-
- TabInfo(std::unique_ptr<ModInfoDialogTab> tab);
-
- // returns whether this tab is part of the tab widget
- //
- bool isVisible() const;
- };
-
- std::unique_ptr<Ui::ModInfoDialog> ui;
- OrganizerCore& m_core;
- PluginContainer& m_plugin;
- ModListView* m_modListView;
- ModInfo::Ptr m_mod;
- std::vector<TabInfo> m_tabs;
-
- // initial tab requested by the main window when the dialog is opened; whether
- // the request can be honoured depends on what tabs are present
- ModInfoTabIDs m_initialTab;
-
- // set to true when tabs are being removed and re-added while navigating
- // between mods; since the current index changes while this is happening,
- // onTabSelectionChanged() will be called repeatedly
- //
- // however, it will check this flag and ignore the event so first activations
- // are not fired incorrectly
- bool m_arrangingTabs;
-
-
- // creates all the tabs and connects events
- //
- void createTabs();
-
-
- // saves the dialog state and calls saveState() on all tabs
- //
- void saveState() const;
-
- // restores the dialog state and calls restoreState() on all tabs
- //
- void restoreState();
-
-
- // sets the currently selected mod; resets first activation, but doesn't
- // update anything
- //
- void setMod(ModInfo::Ptr mod);
-
- // sets the currently selected mod, if found; forwards to setMod() above
- //
- void setMod(const QString& name);
-
- // returns the origin of the current mod, may be null
- //
- MOShared::FilesOrigin* getOrigin();
-
-
- // returns the currently selected tab, taking re-ordering in to account;
- // shouldn't be null, but could be
- //
- TabInfo* currentTab();
-
-
- // fully updates the dialog; sets the title, the tab visibility and updates
- // all the tabs; used when the current mod changes
- //
- // see setTabsVisibility() for firstTime
- //
- void update(bool firstTime=false);
-
- // builds the list of visible tabs; if the list is different from what's
- // currently displayed, or firstTime is true, forwards to reAddTabs()
- void setTabsVisibility(bool firstTime);
-
- // clears the tab widgets and re-adds the tabs having the visible flag in
- // the given vector, following the tab order from the settings
- //
- void reAddTabs(const std::vector<bool>& visibility, ModInfoTabIDs sel);
-
- // called by update(); clears tabs, feeds files and calls update() on all
- // tabs, then setTabsColors()
- //
- void updateTabs(bool becauseOriginChanged=false);
-
- // goes through all files on the filesystem for the current mod and calls
- // feedFile() on every tab until one accepts it
- //
- void feedFiles(std::vector<TabInfo*>& interestedTabs);
-
- // goes through all tabs and sets the tab text colour depending on whether
- // they have data or not
- //
- void setTabsColors();
-
-
- // called when the delete key is pressed anywhere in the dialog; forwards to
- // ModInfoDialogTab::deleteRequest() for the currently selected tab
- //
- void onDeleteShortcut();
-
-
- // finds the tab with the given id and selects it
- //
- void switchToTab(ModInfoTabIDs id);
-
-
- // saves the current tab order; used by saveState(), but also by
- // setTabsVisibility() to make sure any changes to order are saved before
- // re-adding tabs
- //
- void saveTabOrder() const;
-
- // asks all the tabs if they accept closing the dialog, returns false if one
- // objected
- //
- bool tryClose();
-
-
- // called when the user clicks the close button; closing the dialog by other
- // means ends up in closeEvent(); forwards to tryClose()
- //
- void onCloseButton();
-
- // called when the user clicks the previous button; asks the main window for
- // the previous mod in the list and loads it
- //
- void onPreviousMod();
-
- // called when the user clicks the next button; asks the main window for the
- // next mod in the list and loads it
- //
- void onNextMod();
-
- // called when the selects a tab; handles first activation
- //
- void onTabSelectionChanged();
-
- // called when the user re-orders tabs; sets the correct TabInfo::realPos for
- // all tabs
- //
- void onTabMoved();
-
- // called when a tab has modified the origin; emits originModified() and
- // updates all the tabs that use origin files
- //
- void onOriginModified(int originID);
-};
-
-#endif // MODINFODIALOG_H
+/*
+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 MODINFODIALOG_H
+#define MODINFODIALOG_H
+
+
+#include "modinfo.h"
+#include "tutorabledialog.h"
+#include "filerenamer.h"
+#include "modinfodialogfwd.h"
+
+namespace Ui { class ModInfoDialog; }
+namespace MOShared { class FilesOrigin; }
+
+class PluginContainer;
+class OrganizerCore;
+class Settings;
+class ModInfoDialogTab;
+class ModListView;
+
+/**
+ * this is a larger dialog used to visualise information about the mod.
+ * @todo this would probably a good place for a plugin-system
+ **/
+class ModInfoDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT;
+
+ // creates a tab, it's a friend because it uses a bunch of member variables
+ // to create ModInfoDialogTabContext
+ //
+ template <class T>
+ friend std::unique_ptr<ModInfoDialogTab> createTab(
+ ModInfoDialog& d, ModInfoTabIDs index);
+
+public:
+ ModInfoDialog(
+ OrganizerCore& core, PluginContainer& plugin,
+ ModInfo::Ptr mod, ModListView* view, QWidget* parent = nullptr);
+
+ ~ModInfoDialog();
+
+ // switches to the tab with the given id
+ //
+ void selectTab(ModInfoTabIDs id);
+
+ // updates all tabs, selects the initial tab, opens the dialog and
+ // saves/restores geometry
+ //
+ int exec() override;
+
+signals:
+ // emitted when a tab changes the origin
+ //
+ void originModified(int originID);
+
+ // emitted when the mod of the dialog is changed
+ //
+ void modChanged(unsigned int modIndex);
+
+protected:
+ // forwards to tryClose()
+ //
+ void closeEvent(QCloseEvent* e);
+
+private:
+ // represents a single tab
+ //
+ struct TabInfo
+ {
+ // tab implementation
+ std::unique_ptr<ModInfoDialogTab> tab;
+
+ // actual position in the tab bar, updated every time a tab is moved
+ int realPos;
+
+ // widget used by the QTabWidget for this tab
+ //
+ // because QTabWidget doesn't support simply hiding tabs, they have to be
+ // completely removed from the widget when they don't support the current
+ // mod
+ //
+ // therefore, `widget, `caption` and `icon` are remembered so tabs can be
+ // removed and re-added when navigating between mods
+ //
+ // `widget` is also used figure out which tab is where when they're
+ // re-ordered
+ QWidget* widget;
+
+ // caption for this tab, see `widget`
+ QString caption;
+
+ // icon for this tab, see `widget`
+ QIcon icon;
+
+
+ TabInfo(std::unique_ptr<ModInfoDialogTab> tab);
+
+ // returns whether this tab is part of the tab widget
+ //
+ bool isVisible() const;
+ };
+
+ std::unique_ptr<Ui::ModInfoDialog> ui;
+ OrganizerCore& m_core;
+ PluginContainer& m_plugin;
+ ModListView* m_modListView;
+ ModInfo::Ptr m_mod;
+ std::vector<TabInfo> m_tabs;
+
+ // initial tab requested by the main window when the dialog is opened; whether
+ // the request can be honoured depends on what tabs are present
+ ModInfoTabIDs m_initialTab;
+
+ // set to true when tabs are being removed and re-added while navigating
+ // between mods; since the current index changes while this is happening,
+ // onTabSelectionChanged() will be called repeatedly
+ //
+ // however, it will check this flag and ignore the event so first activations
+ // are not fired incorrectly
+ bool m_arrangingTabs;
+
+
+ // creates all the tabs and connects events
+ //
+ void createTabs();
+
+
+ // saves the dialog state and calls saveState() on all tabs
+ //
+ void saveState() const;
+
+ // restores the dialog state and calls restoreState() on all tabs
+ //
+ void restoreState();
+
+
+ // sets the currently selected mod; resets first activation, but doesn't
+ // update anything
+ //
+ void setMod(ModInfo::Ptr mod);
+
+ // sets the currently selected mod, if found; forwards to setMod() above
+ //
+ void setMod(const QString& name);
+
+ // returns the origin of the current mod, may be null
+ //
+ MOShared::FilesOrigin* getOrigin();
+
+
+ // returns the currently selected tab, taking re-ordering in to account;
+ // shouldn't be null, but could be
+ //
+ TabInfo* currentTab();
+
+
+ // fully updates the dialog; sets the title, the tab visibility and updates
+ // all the tabs; used when the current mod changes
+ //
+ // see setTabsVisibility() for firstTime
+ //
+ void update(bool firstTime=false);
+
+ // builds the list of visible tabs; if the list is different from what's
+ // currently displayed, or firstTime is true, forwards to reAddTabs()
+ void setTabsVisibility(bool firstTime);
+
+ // clears the tab widgets and re-adds the tabs having the visible flag in
+ // the given vector, following the tab order from the settings
+ //
+ void reAddTabs(const std::vector<bool>& visibility, ModInfoTabIDs sel);
+
+ // called by update(); clears tabs, feeds files and calls update() on all
+ // tabs, then setTabsColors()
+ //
+ void updateTabs(bool becauseOriginChanged=false);
+
+ // goes through all files on the filesystem for the current mod and calls
+ // feedFile() on every tab until one accepts it
+ //
+ void feedFiles(std::vector<TabInfo*>& interestedTabs);
+
+ // goes through all tabs and sets the tab text colour depending on whether
+ // they have data or not
+ //
+ void setTabsColors();
+
+
+ // called when the delete key is pressed anywhere in the dialog; forwards to
+ // ModInfoDialogTab::deleteRequest() for the currently selected tab
+ //
+ void onDeleteShortcut();
+
+
+ // finds the tab with the given id and selects it
+ //
+ void switchToTab(ModInfoTabIDs id);
+
+
+ // saves the current tab order; used by saveState(), but also by
+ // setTabsVisibility() to make sure any changes to order are saved before
+ // re-adding tabs
+ //
+ void saveTabOrder() const;
+
+ // asks all the tabs if they accept closing the dialog, returns false if one
+ // objected
+ //
+ bool tryClose();
+
+
+ // called when the user clicks the close button; closing the dialog by other
+ // means ends up in closeEvent(); forwards to tryClose()
+ //
+ void onCloseButton();
+
+ // called when the user clicks the previous button; asks the main window for
+ // the previous mod in the list and loads it
+ //
+ void onPreviousMod();
+
+ // called when the user clicks the next button; asks the main window for the
+ // next mod in the list and loads it
+ //
+ void onNextMod();
+
+ // called when the selects a tab; handles first activation
+ //
+ void onTabSelectionChanged();
+
+ // called when the user re-orders tabs; sets the correct TabInfo::realPos for
+ // all tabs
+ //
+ void onTabMoved();
+
+ // called when a tab has modified the origin; emits originModified() and
+ // updates all the tabs that use origin files
+ //
+ void onOriginModified(int originID);
+};
+
+#endif // MODINFODIALOG_H
diff --git a/src/modlist.h b/src/modlist.h
index 6a3a2901..f179d7ae 100644
--- a/src/modlist.h
+++ b/src/modlist.h
@@ -1,407 +1,407 @@
-/*
-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 MODLIST_H
-#define MODLIST_H
-
-#include "moddatacontent.h"
-#include "categories.h"
-#include "nexusinterface.h"
-#include "modinfo.h"
-#include "profile.h"
-
-#include <imodlist.h>
-
-#include <QFile>
-#include <QListWidget>
-#include <QNetworkReply>
-#include <QMetaEnum>
-#include <QNetworkAccessManager>
-#ifndef Q_MOC_RUN
-#include <boost/signals2.hpp>
-#endif
-#include <set>
-#include <vector>
-#include <QVector>
-
-
-class QSortFilterProxyModel;
-class PluginContainer;
-class OrganizerCore;
-class ModListDropInfo;
-
-/**
- * Model presenting an overview of the installed mod
- * This is used in a view in the main window of MO. It combines general information about
- * the mods from ModInfo with status information from the Profile
- **/
-class ModList : public QAbstractItemModel
-{
- Q_OBJECT
-
-public:
-
- enum ModListRole {
-
- // data(GroupingRole) contains the "group" role - This is used by the
- // category and Nexus ID grouping proxy (but not the ByPriority proxy)
- GroupingRole = Qt::UserRole,
-
- IndexRole = Qt::UserRole + 1,
-
- // data(AggrRole) contains aggregation information (for
- // grouping I assume?)
- AggrRole = Qt::UserRole + 2,
-
- GameNameRole = Qt::UserRole + 3,
- PriorityRole = Qt::UserRole + 4,
-
- // marking role for the scrollbar
- ScrollMarkRole = Qt::UserRole + 5,
-
- // this is the first available role
- ModUserRole = Qt::UserRole + 6
- };
-
- enum EColumn {
- COL_NAME,
- COL_CONFLICTFLAGS,
- COL_FLAGS,
- COL_CONTENT,
- COL_CATEGORY,
- COL_MODID,
- COL_GAME,
- COL_VERSION,
- COL_INSTALLTIME,
- COL_PRIORITY,
- COL_NOTES,
- COL_LASTCOLUMN = COL_NOTES,
- };
-
- using SignalModInstalled = boost::signals2::signal<void(MOBase::IModInterface*)>;
- using SignalModRemoved = boost::signals2::signal<void(QString const&)>;
- using SignalModStateChanged = boost::signals2::signal<void (const std::map<QString, MOBase::IModList::ModStates>&)>;
- using SignalModMoved = boost::signals2::signal<void (const QString &, int, int)>;
-
-public:
-
- /**
- * @brief constructor
- * @todo ensure this view works without a profile set, otherwise there are intransparent dependencies on the initialisation order
- **/
- ModList(PluginContainer *pluginContainer, OrganizerCore *parent);
-
- ~ModList();
-
- /**
- * @brief set the profile used for status information
- *
- * @param profile the profile to use
- **/
- void setProfile(Profile *profile);
-
- /**
- * @brief retrieve the current sorting mode
- * @note this is used to store the sorting mode between sessions
- * @return current sorting mode, encoded to be compatible to previous versions
- **/
- int getCurrentSortingMode() const;
-
- /**
- * @brief remove the specified mod without asking for confirmation
- * @param row the row to remove
- */
- void removeRowForce(int row, const QModelIndex &parent);
-
- void notifyChange(int rowStart, int rowEnd = -1);
- static QString getColumnName(int column);
-
- void changeModPriority(int sourceIndex, int newPriority);
- void changeModPriority(std::vector<int> sourceIndices, int newPriority);
-
- void setPluginContainer(PluginContainer *pluginContainer);
-
- bool modInfoAboutToChange(ModInfo::Ptr info);
- void modInfoChanged(ModInfo::Ptr info);
-
- void disconnectSlots();
-
- int timeElapsedSinceLastChecked() const;
-
-public:
-
- /**
- * @brief Notify the mod list that the given mod has been installed. This is used
- * to notify the plugin that registered through onModInstalled().
- *
- * @param mod The installed mod.
- */
- void notifyModInstalled(MOBase::IModInterface *mod) const;
-
- /**
- * @brief Notify the mod list that a mod has been removed. This is used
- * to notify the plugin that registered through onModRemoved().
- *
- * @param modName Name of the removed mod.
- */
- void notifyModRemoved(QString const& modName) const;
-
- /**
- * @brief Notify the mod list that the state of the specified mods has changed. This is used
- * to notify the plugin that registered through onModStateChanged().
- *
- * @param modIndices Indices of the mods that changed.
- */
- void notifyModStateChanged(QList<unsigned int> modIndices) const;
-
-public:
-
- /// \copydoc MOBase::IModList::displayName
- QString displayName(const QString &internalName) const;
-
- /// \copydoc MOBase::IModList::allMods
- QStringList allMods() const;
- QStringList allModsByProfilePriority(MOBase::IProfile* profile = nullptr) const;
-
- // \copydoc MOBase::IModList::getMod
- MOBase::IModInterface* getMod(const QString& name) const;
-
- // \copydoc MOBase::IModList::remove
- bool removeMod(MOBase::IModInterface* mod);
-
- // \copydoc MOBase::IModList::renameMod
- MOBase::IModInterface* renameMod(MOBase::IModInterface* mod, const QString& name);
-
- /// \copydoc MOBase::IModList::state
- MOBase::IModList::ModStates state(const QString &name) const;
-
- /// \copydoc MOBase::IModList::setActive
- bool setActive(const QString &name, bool active);
-
- /// \copydoc MOBase::IModList::setActive
- int setActive(const QStringList& names, bool active);
-
- /// \copydoc MOBase::IModList::priority
- int priority(const QString &name) const;
-
- /// \copydoc MOBase::IModList::setPriority
- bool setPriority(const QString &name, int newPriority);
-
- boost::signals2::connection onModInstalled(const std::function<void(MOBase::IModInterface*)>& func);
- boost::signals2::connection onModRemoved(const std::function<void(QString const&)>& func);
- boost::signals2::connection onModStateChanged(const std::function<void(const std::map<QString, MOBase::IModList::ModStates>&)>& func);
- boost::signals2::connection onModMoved(const std::function<void (const QString &, int, int)> &func);
-
-public: // implementation of virtual functions of QAbstractItemModel
-
- virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
- virtual bool hasChildren(const QModelIndex &parent = QModelIndex()) const;
- virtual int columnCount(const QModelIndex &parent = QModelIndex()) const;
- virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
- virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
- virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const;
- virtual bool removeRows(int row, int count, const QModelIndex& parent);
-
- Qt::DropActions supportedDropActions() const override { return Qt::MoveAction | Qt::CopyAction; }
- QStringList mimeTypes() const override;
- QMimeData *mimeData(const QModelIndexList &indexes) const override;
- bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override;
- bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
-
- virtual QModelIndex index(int row, int column,
- const QModelIndex &parent = QModelIndex()) const;
- virtual QModelIndex parent(const QModelIndex &child) const;
-
- virtual QMap<int, QVariant> itemData(const QModelIndex &index) const;
-
-public slots:
-
- void onDragEnter(const QMimeData* data);
-
- // enable/disable mods at the given indices.
- //
- void setActive(const QModelIndexList& indices, bool active);
-
- // shift the priority of mods at the given indices by the given offset
- //
- void shiftModsPriority(const QModelIndexList& indices, int offset);
-
- // change the priority of the mods specified by the given indices
- //
- void changeModsPriority(const QModelIndexList& indices, int priority);
-
- // toggle the active state of mods at the given indices
- //
- bool toggleState(const QModelIndexList& indices);
-
-signals:
-
- // emitted when the priority of one or multiple mods have changed
- //
- // the sorting of the list can only be manually changed if the list is sorted by priority
- // in which case the move is intended to change the priority of a mod.
- //
- void modPrioritiesChanged(const QModelIndexList& indices) const;
-
- // emitted when the state (active/inactive) of one or multiple mods have changed
- //
- void modStatesChanged(const QModelIndexList& indices) const;
-
- /**
- * @brief emitted when the model wants a text to be displayed by the UI
- *
- * @param message the message to display
- **/
- void showMessage(const QString &message);
-
- /**
- * @brief signals change to the count of headers
- */
- void resizeHeaders();
-
- /**
- * @brief emitted to remove a file origin
- * @param name name of the orign to remove
- */
- void removeOrigin(const QString &name);
-
- /**
- * @brief emitted after a mod has been renamed
- * This signal MUST be used to fix the mod names in profiles (except the active one) and to invalidate/refresh other
- * structures that may have become invalid with the rename
- *
- * @param oldName the old name of the mod
- * @param newName new name of the mod
- */
- void modRenamed(const QString &oldName, const QString &newName);
-
- /**
- * @brief emitted after a mod has been uninstalled
- * @param fileName filename of the mod being uninstalled
- */
- void modUninstalled(const QString &fileName);
-
- /**
- * @brief QML seems to handle overloaded signals poorly - create unique signal for tutorials
- */
- void tutorialModlistUpdate();
-
- /**
- * @brief fileMoved emitted when a file is moved from one mod to another
- * @param relativePath relative path of the file moved
- * @param oldOriginName name of the origin that previously contained the file
- * @param newOriginName name of the origin that now contains the file
- */
- void fileMoved(const QString &relativePath, const QString &oldOriginName, const QString &newOriginName);
-
- /**
- * @brief emitted to have the overwrite folder cleared
- */
- void clearOverwrite();
-
- void aboutToChangeData();
-
- void postDataChanged();
-
- // emitted when an item is dropped from the download list, the row is from the
- // download list
- //
- void downloadArchiveDropped(int row, int priority);
-
- // emitted when an external archive is dropped on the mod list
- //
- void externalArchiveDropped(const QUrl& url, int priority);
-
- // emitted when an external folder is dropped on the mod list
- //
- void externalFolderDropped(const QUrl& url, int priority);
-
-private:
-
- // retrieve the display name of a mod or convert from a user-provided
- // name to internal name
- //
- QString getDisplayName(ModInfo::Ptr info) const;
- QString makeInternalName(ModInfo::Ptr info, QString name) const;
-
- QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const;
-
- QString getConflictFlagText(ModInfo::EConflictFlag flag, ModInfo::Ptr modInfo) const;
-
- QString getColumnToolTip(int column) const;
-
- bool renameMod(int index, const QString &newName);
-
- MOBase::IModList::ModStates state(unsigned int modIndex) const;
-
- // handle dropping of local URLs files
- //
- bool dropLocalFiles(const ModListDropInfo& dropInfo, int row, const QModelIndex& parent);
-
- // return the priority of the mod for a drop event
- //
- int dropPriority(int row, const QModelIndex& parent) const;
-
-private:
-
- struct TModInfo {
- TModInfo(unsigned int index, ModInfo::Ptr modInfo)
- : modInfo(modInfo), nameOrder(index), priorityOrder(0), modIDOrder(0), categoryOrder(0) {}
- ModInfo::Ptr modInfo;
- unsigned int nameOrder;
- unsigned int priorityOrder;
- unsigned int modIDOrder;
- unsigned int categoryOrder;
- };
-
- struct TModInfoChange {
- QString name;
- QFlags<MOBase::IModList::ModState> state;
- };
-
-private:
-
- OrganizerCore *m_Organizer;
- Profile *m_Profile;
-
- NexusInterface *m_NexusInterface;
- std::set<int> m_RequestIDs;
-
- mutable bool m_Modified;
- bool m_InNotifyChange;
- bool m_DropOnMod = false;
-
- QFontMetrics m_FontMetrics;
-
- TModInfoChange m_ChangeInfo;
-
- SignalModInstalled m_ModInstalled;
- SignalModMoved m_ModMoved;
- SignalModRemoved m_ModRemoved;
- SignalModStateChanged m_ModStateChanged;
-
- QElapsedTimer m_LastCheck;
-
- PluginContainer *m_PluginContainer;
-
-};
-
-#endif // MODLIST_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 MODLIST_H
+#define MODLIST_H
+
+#include "moddatacontent.h"
+#include "categories.h"
+#include "nexusinterface.h"
+#include "modinfo.h"
+#include "profile.h"
+
+#include <imodlist.h>
+
+#include <QFile>
+#include <QListWidget>
+#include <QNetworkReply>
+#include <QMetaEnum>
+#include <QNetworkAccessManager>
+#ifndef Q_MOC_RUN
+#include <boost/signals2.hpp>
+#endif
+#include <set>
+#include <vector>
+#include <QVector>
+
+
+class QSortFilterProxyModel;
+class PluginContainer;
+class OrganizerCore;
+class ModListDropInfo;
+
+/**
+ * Model presenting an overview of the installed mod
+ * This is used in a view in the main window of MO. It combines general information about
+ * the mods from ModInfo with status information from the Profile
+ **/
+class ModList : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+
+ enum ModListRole {
+
+ // data(GroupingRole) contains the "group" role - This is used by the
+ // category and Nexus ID grouping proxy (but not the ByPriority proxy)
+ GroupingRole = Qt::UserRole,
+
+ IndexRole = Qt::UserRole + 1,
+
+ // data(AggrRole) contains aggregation information (for
+ // grouping I assume?)
+ AggrRole = Qt::UserRole + 2,
+
+ GameNameRole = Qt::UserRole + 3,
+ PriorityRole = Qt::UserRole + 4,
+
+ // marking role for the scrollbar
+ ScrollMarkRole = Qt::UserRole + 5,
+
+ // this is the first available role
+ ModUserRole = Qt::UserRole + 6
+ };
+
+ enum EColumn {
+ COL_NAME,
+ COL_CONFLICTFLAGS,
+ COL_FLAGS,
+ COL_CONTENT,
+ COL_CATEGORY,
+ COL_MODID,
+ COL_GAME,
+ COL_VERSION,
+ COL_INSTALLTIME,
+ COL_PRIORITY,
+ COL_NOTES,
+ COL_LASTCOLUMN = COL_NOTES,
+ };
+
+ using SignalModInstalled = boost::signals2::signal<void(MOBase::IModInterface*)>;
+ using SignalModRemoved = boost::signals2::signal<void(QString const&)>;
+ using SignalModStateChanged = boost::signals2::signal<void (const std::map<QString, MOBase::IModList::ModStates>&)>;
+ using SignalModMoved = boost::signals2::signal<void (const QString &, int, int)>;
+
+public:
+
+ /**
+ * @brief constructor
+ * @todo ensure this view works without a profile set, otherwise there are intransparent dependencies on the initialisation order
+ **/
+ ModList(PluginContainer *pluginContainer, OrganizerCore *parent);
+
+ ~ModList();
+
+ /**
+ * @brief set the profile used for status information
+ *
+ * @param profile the profile to use
+ **/
+ void setProfile(Profile *profile);
+
+ /**
+ * @brief retrieve the current sorting mode
+ * @note this is used to store the sorting mode between sessions
+ * @return current sorting mode, encoded to be compatible to previous versions
+ **/
+ int getCurrentSortingMode() const;
+
+ /**
+ * @brief remove the specified mod without asking for confirmation
+ * @param row the row to remove
+ */
+ void removeRowForce(int row, const QModelIndex &parent);
+
+ void notifyChange(int rowStart, int rowEnd = -1);
+ static QString getColumnName(int column);
+
+ void changeModPriority(int sourceIndex, int newPriority);
+ void changeModPriority(std::vector<int> sourceIndices, int newPriority);
+
+ void setPluginContainer(PluginContainer *pluginContainer);
+
+ bool modInfoAboutToChange(ModInfo::Ptr info);
+ void modInfoChanged(ModInfo::Ptr info);
+
+ void disconnectSlots();
+
+ int timeElapsedSinceLastChecked() const;
+
+public:
+
+ /**
+ * @brief Notify the mod list that the given mod has been installed. This is used
+ * to notify the plugin that registered through onModInstalled().
+ *
+ * @param mod The installed mod.
+ */
+ void notifyModInstalled(MOBase::IModInterface *mod) const;
+
+ /**
+ * @brief Notify the mod list that a mod has been removed. This is used
+ * to notify the plugin that registered through onModRemoved().
+ *
+ * @param modName Name of the removed mod.
+ */
+ void notifyModRemoved(QString const& modName) const;
+
+ /**
+ * @brief Notify the mod list that the state of the specified mods has changed. This is used
+ * to notify the plugin that registered through onModStateChanged().
+ *
+ * @param modIndices Indices of the mods that changed.
+ */
+ void notifyModStateChanged(QList<unsigned int> modIndices) const;
+
+public:
+
+ /// \copydoc MOBase::IModList::displayName
+ QString displayName(const QString &internalName) const;
+
+ /// \copydoc MOBase::IModList::allMods
+ QStringList allMods() const;
+ QStringList allModsByProfilePriority(MOBase::IProfile* profile = nullptr) const;
+
+ // \copydoc MOBase::IModList::getMod
+ MOBase::IModInterface* getMod(const QString& name) const;
+
+ // \copydoc MOBase::IModList::remove
+ bool removeMod(MOBase::IModInterface* mod);
+
+ // \copydoc MOBase::IModList::renameMod
+ MOBase::IModInterface* renameMod(MOBase::IModInterface* mod, const QString& name);
+
+ /// \copydoc MOBase::IModList::state
+ MOBase::IModList::ModStates state(const QString &name) const;
+
+ /// \copydoc MOBase::IModList::setActive
+ bool setActive(const QString &name, bool active);
+
+ /// \copydoc MOBase::IModList::setActive
+ int setActive(const QStringList& names, bool active);
+
+ /// \copydoc MOBase::IModList::priority
+ int priority(const QString &name) const;
+
+ /// \copydoc MOBase::IModList::setPriority
+ bool setPriority(const QString &name, int newPriority);
+
+ boost::signals2::connection onModInstalled(const std::function<void(MOBase::IModInterface*)>& func);
+ boost::signals2::connection onModRemoved(const std::function<void(QString const&)>& func);
+ boost::signals2::connection onModStateChanged(const std::function<void(const std::map<QString, MOBase::IModList::ModStates>&)>& func);
+ boost::signals2::connection onModMoved(const std::function<void (const QString &, int, int)> &func);
+
+public: // implementation of virtual functions of QAbstractItemModel
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual bool hasChildren(const QModelIndex &parent = QModelIndex()) const;
+ virtual int columnCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
+ virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const;
+ virtual bool removeRows(int row, int count, const QModelIndex& parent);
+
+ Qt::DropActions supportedDropActions() const override { return Qt::MoveAction | Qt::CopyAction; }
+ QStringList mimeTypes() const override;
+ QMimeData *mimeData(const QModelIndexList &indexes) const override;
+ bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override;
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
+
+ virtual QModelIndex index(int row, int column,
+ const QModelIndex &parent = QModelIndex()) const;
+ virtual QModelIndex parent(const QModelIndex &child) const;
+
+ virtual QMap<int, QVariant> itemData(const QModelIndex &index) const;
+
+public slots:
+
+ void onDragEnter(const QMimeData* data);
+
+ // enable/disable mods at the given indices.
+ //
+ void setActive(const QModelIndexList& indices, bool active);
+
+ // shift the priority of mods at the given indices by the given offset
+ //
+ void shiftModsPriority(const QModelIndexList& indices, int offset);
+
+ // change the priority of the mods specified by the given indices
+ //
+ void changeModsPriority(const QModelIndexList& indices, int priority);
+
+ // toggle the active state of mods at the given indices
+ //
+ bool toggleState(const QModelIndexList& indices);
+
+signals:
+
+ // emitted when the priority of one or multiple mods have changed
+ //
+ // the sorting of the list can only be manually changed if the list is sorted by priority
+ // in which case the move is intended to change the priority of a mod.
+ //
+ void modPrioritiesChanged(const QModelIndexList& indices) const;
+
+ // emitted when the state (active/inactive) of one or multiple mods have changed
+ //
+ void modStatesChanged(const QModelIndexList& indices) const;
+
+ /**
+ * @brief emitted when the model wants a text to be displayed by the UI
+ *
+ * @param message the message to display
+ **/
+ void showMessage(const QString &message);
+
+ /**
+ * @brief signals change to the count of headers
+ */
+ void resizeHeaders();
+
+ /**
+ * @brief emitted to remove a file origin
+ * @param name name of the orign to remove
+ */
+ void removeOrigin(const QString &name);
+
+ /**
+ * @brief emitted after a mod has been renamed
+ * This signal MUST be used to fix the mod names in profiles (except the active one) and to invalidate/refresh other
+ * structures that may have become invalid with the rename
+ *
+ * @param oldName the old name of the mod
+ * @param newName new name of the mod
+ */
+ void modRenamed(const QString &oldName, const QString &newName);
+
+ /**
+ * @brief emitted after a mod has been uninstalled
+ * @param fileName filename of the mod being uninstalled
+ */
+ void modUninstalled(const QString &fileName);
+
+ /**
+ * @brief QML seems to handle overloaded signals poorly - create unique signal for tutorials
+ */
+ void tutorialModlistUpdate();
+
+ /**
+ * @brief fileMoved emitted when a file is moved from one mod to another
+ * @param relativePath relative path of the file moved
+ * @param oldOriginName name of the origin that previously contained the file
+ * @param newOriginName name of the origin that now contains the file
+ */
+ void fileMoved(const QString &relativePath, const QString &oldOriginName, const QString &newOriginName);
+
+ /**
+ * @brief emitted to have the overwrite folder cleared
+ */
+ void clearOverwrite();
+
+ void aboutToChangeData();
+
+ void postDataChanged();
+
+ // emitted when an item is dropped from the download list, the row is from the
+ // download list
+ //
+ void downloadArchiveDropped(int row, int priority);
+
+ // emitted when an external archive is dropped on the mod list
+ //
+ void externalArchiveDropped(const QUrl& url, int priority);
+
+ // emitted when an external folder is dropped on the mod list
+ //
+ void externalFolderDropped(const QUrl& url, int priority);
+
+private:
+
+ // retrieve the display name of a mod or convert from a user-provided
+ // name to internal name
+ //
+ QString getDisplayName(ModInfo::Ptr info) const;
+ QString makeInternalName(ModInfo::Ptr info, QString name) const;
+
+ QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const;
+
+ QString getConflictFlagText(ModInfo::EConflictFlag flag, ModInfo::Ptr modInfo) const;
+
+ QString getColumnToolTip(int column) const;
+
+ bool renameMod(int index, const QString &newName);
+
+ MOBase::IModList::ModStates state(unsigned int modIndex) const;
+
+ // handle dropping of local URLs files
+ //
+ bool dropLocalFiles(const ModListDropInfo& dropInfo, int row, const QModelIndex& parent);
+
+ // return the priority of the mod for a drop event
+ //
+ int dropPriority(int row, const QModelIndex& parent) const;
+
+private:
+
+ struct TModInfo {
+ TModInfo(unsigned int index, ModInfo::Ptr modInfo)
+ : modInfo(modInfo), nameOrder(index), priorityOrder(0), modIDOrder(0), categoryOrder(0) {}
+ ModInfo::Ptr modInfo;
+ unsigned int nameOrder;
+ unsigned int priorityOrder;
+ unsigned int modIDOrder;
+ unsigned int categoryOrder;
+ };
+
+ struct TModInfoChange {
+ QString name;
+ QFlags<MOBase::IModList::ModState> state;
+ };
+
+private:
+
+ OrganizerCore *m_Organizer;
+ Profile *m_Profile;
+
+ NexusInterface *m_NexusInterface;
+ std::set<int> m_RequestIDs;
+
+ mutable bool m_Modified;
+ bool m_InNotifyChange;
+ bool m_DropOnMod = false;
+
+ QFontMetrics m_FontMetrics;
+
+ TModInfoChange m_ChangeInfo;
+
+ SignalModInstalled m_ModInstalled;
+ SignalModMoved m_ModMoved;
+ SignalModRemoved m_ModRemoved;
+ SignalModStateChanged m_ModStateChanged;
+
+ QElapsedTimer m_LastCheck;
+
+ PluginContainer *m_PluginContainer;
+
+};
+
+#endif // MODLIST_H
+
diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp
index a34f730f..1795ff3c 100644
--- a/src/modlistsortproxy.cpp
+++ b/src/modlistsortproxy.cpp
@@ -1,705 +1,705 @@
-/*
-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 "modlistsortproxy.h"
-#include "modlistbypriorityproxy.h"
-#include "modlistdropinfo.h"
-#include "modinfo.h"
-#include "profile.h"
-#include "messagedialog.h"
-#include "qtgroupingproxy.h"
-#include "organizercore.h"
-
-#include <log.h>
-#include <QMenu>
-#include <QCheckBox>
-#include <QWidgetAction>
-#include <QApplication>
-#include <QMimeData>
-#include <QDebug>
-#include <QTreeView>
-
-using namespace MOBase;
-
-ModListSortProxy::ModListSortProxy(Profile* profile, OrganizerCore * organizer)
- : QSortFilterProxyModel(organizer)
- , m_Organizer(organizer)
- , m_Profile(profile)
- , m_FilterActive(false)
- , m_FilterMode(FilterAnd)
- , m_FilterSeparators(SeparatorFilter)
-{
- setDynamicSortFilter(true); // this seems to work without dynamicsortfilter
- // but I don't know why. This should be necessary
-}
-
-void ModListSortProxy::setProfile(Profile *profile)
-{
- m_Profile = profile;
-}
-
-void ModListSortProxy::updateFilterActive()
-{
- m_FilterActive = (!m_Criteria.empty() || !m_Filter.isEmpty());
- emit filterActive(m_FilterActive);
-}
-
-void ModListSortProxy::setCriteria(const std::vector<Criteria>& criteria)
-{
- // avoid refreshing the filter unless we are checking all mods for update.
- const bool changed = (criteria != m_Criteria);
- const bool isForUpdates = (
- !criteria.empty() &&
- criteria[0].id == CategoryFactory::UpdateAvailable);
-
- if (changed || isForUpdates) {
- m_Criteria = criteria;
- updateFilterActive();
- invalidateFilter();
- emit filterInvalidated();
- }
-}
-
-unsigned long ModListSortProxy::flagsId(const std::vector<ModInfo::EFlag> &flags) const
-{
- unsigned long result = 0;
- for (ModInfo::EFlag flag : flags) {
- if ((flag != ModInfo::FLAG_FOREIGN)
- && (flag != ModInfo::FLAG_OVERWRITE)) {
- result += 1 << (int)flag;
- }
- }
- return result;
-}
-
-unsigned long ModListSortProxy::conflictFlagsId(const std::vector<ModInfo::EConflictFlag>& flags) const
-{
- unsigned long result = 0;
- for (ModInfo::EConflictFlag flag : flags) {
- if ((flag != ModInfo::FLAG_OVERWRITE_CONFLICT)) {
- result += 1 << (int)flag;
- }
- }
- return result;
-}
-
-bool ModListSortProxy::lessThan(const QModelIndex &left,
- const QModelIndex &right) const
-{
- if (sourceModel()->hasChildren(left) || sourceModel()->hasChildren(right)) {
- // when sorting by priority, we do not want to use the parent lessThan because
- // it uses the display role which can be inconsistent (e.g. for backups)
- if (sortColumn() != ModList::COL_PRIORITY) {
- return QSortFilterProxyModel::lessThan(left, right);
- }
- else if (qobject_cast<QtGroupingProxy*>(sourceModel())) {
- // if the underlying proxy is a QtGroupingProxy we need to rely on
- // Qt::DisplayRole because the other roles are not correctly handled
- // by that kind of proxy
- return left.data(Qt::DisplayRole).toInt() < right.data(Qt::DisplayRole).toInt();
- }
- }
-
- bool lOk, rOk;
- int leftIndex = left.data(ModList::IndexRole).toInt(&lOk);
- int rightIndex = right.data(ModList::IndexRole).toInt(&rOk);
-
- if (!lOk || !rOk) {
- return false;
- }
-
- ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex);
- ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex);
-
- bool lt = left.data(ModList::PriorityRole).toInt() < right.data(ModList::PriorityRole).toInt();
-
- switch (left.column()) {
- case ModList::COL_FLAGS: {
- std::vector<ModInfo::EFlag> leftFlags = leftMod->getFlags();
- std::vector<ModInfo::EFlag> rightFlags = rightMod->getFlags();
- if (leftFlags.size() != rightFlags.size()) {
- lt = leftFlags.size() < rightFlags.size();
- } else {
- lt = flagsId(leftFlags) < flagsId(rightFlags);
- }
- } break;
- case ModList::COL_CONFLICTFLAGS: {
- std::vector<ModInfo::EConflictFlag> leftFlags = leftMod->getConflictFlags();
- std::vector<ModInfo::EConflictFlag> rightFlags = rightMod->getConflictFlags();
- if (leftFlags.size() != rightFlags.size()) {
- lt = leftFlags.size() < rightFlags.size();
- }
- else {
- lt = conflictFlagsId(leftFlags) < conflictFlagsId(rightFlags);
- }
- } break;
- case ModList::COL_CONTENT: {
- const auto& lContents = leftMod->getContents();
- const auto& rContents = rightMod->getContents();
- unsigned int lValue = 0;
- unsigned int rValue = 0;
- m_Organizer->modDataContents().forEachContentIn(lContents, [&lValue](auto const& content) {
- lValue += 2U << static_cast<unsigned int>(content.id());
- });
- m_Organizer->modDataContents().forEachContentIn(rContents, [&rValue](auto const& content) {
- rValue += 2U << static_cast<unsigned int>(content.id());
- });
- lt = lValue < rValue;
- } break;
- case ModList::COL_NAME: {
- int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive);
- if (comp != 0)
- lt = comp < 0;
- } break;
- case ModList::COL_CATEGORY: {
- if (leftMod->primaryCategory() != rightMod->primaryCategory()) {
- if (leftMod->primaryCategory() < 0) lt = false;
- else if (rightMod->primaryCategory() < 0) lt = true;
- else {
- try {
- CategoryFactory &categories = CategoryFactory::instance();
- QString leftCatName = categories.getCategoryName(categories.getCategoryIndex(leftMod->primaryCategory()));
- QString rightCatName = categories.getCategoryName(categories.getCategoryIndex(rightMod->primaryCategory()));
- lt = leftCatName < rightCatName;
- } catch (const std::exception &e) {
- log::error("failed to compare categories: {}", e.what());
- }
- }
- }
- } break;
- case ModList::COL_MODID: {
- if (leftMod->nexusId() != rightMod->nexusId())
- lt = leftMod->nexusId() < rightMod->nexusId();
- } break;
- case ModList::COL_VERSION: {
- if (leftMod->version() != rightMod->version())
- lt = leftMod->version() < rightMod->version();
- } break;
- case ModList::COL_INSTALLTIME: {
- QDateTime leftTime = left.data().toDateTime();
- QDateTime rightTime = right.data().toDateTime();
- if (leftTime != rightTime)
- return leftTime < rightTime;
- } break;
- case ModList::COL_GAME: {
- if (leftMod->gameName() != rightMod->gameName()) {
- lt = leftMod->gameName() < rightMod->gameName();
- }
- else {
- int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive);
- if (comp != 0)
- lt = comp < 0;
- }
- } break;
- case ModList::COL_NOTES: {
- QString leftComments = leftMod->comments();
- QString rightComments = rightMod->comments();
- if (leftComments != rightComments) {
- if (leftComments.isEmpty()) {
- lt = sortOrder() == Qt::DescendingOrder;
- } else if (rightComments.isEmpty()) {
- lt = sortOrder() == Qt::AscendingOrder;
- } else {
- lt = leftComments < rightComments;
- }
- }
- } break;
- case ModList::COL_PRIORITY: {
- if (leftMod->isBackup() != rightMod->isBackup()) {
- lt = leftMod->isBackup();
- }
- else if (leftMod->isOverwrite() != rightMod->isOverwrite()) {
- lt = rightMod->isOverwrite();
- }
- } break;
- default: {
- log::warn("Sorting is not defined for column {}", left.column());
- } break;
- }
- return lt;
-}
-
-void ModListSortProxy::updateFilter(const QString& filter)
-{
- m_Filter = filter;
- updateFilterActive();
- invalidateFilter();
- emit filterInvalidated();
-}
-
-bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const
-{
- for (ModInfo::EConflictFlag flag : flags) {
- if ((flag == ModInfo::FLAG_CONFLICT_MIXED) ||
- (flag == ModInfo::FLAG_CONFLICT_OVERWRITE) ||
- (flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) ||
- (flag == ModInfo::FLAG_CONFLICT_REDUNDANT) ||
- (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE) ||
- (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN) ||
- (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED) ||
- (flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE) ||
- (flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN)) {
- return true;
- }
- }
-
- return false;
-}
-
-bool ModListSortProxy::filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const
-{
- for (auto&& c : m_Criteria) {
- if (!criteriaMatchMod(info, enabled, c)) {
- return false;
- }
- }
-
- return true;
-}
-
-bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const
-{
- for (auto&& c : m_Criteria) {
- if (criteriaMatchMod(info, enabled, c)) {
- return true;
- }
- }
-
- if (!m_Criteria.empty()) {
- // nothing matched
- return false;
- }
-
- return true;
-}
-
-bool ModListSortProxy::optionsMatchMod(ModInfo::Ptr info, bool) const
-{
- return true;
-}
-
-bool ModListSortProxy::criteriaMatchMod(
- ModInfo::Ptr info, bool enabled, const Criteria& c) const
-{
- bool b = false;
-
- switch (c.type)
- {
- case TypeSpecial: // fall-through
- case TypeCategory:
- {
- b = categoryMatchesMod(info, enabled, c.id);
- break;
- }
-
- case TypeContent:
- {
- b = contentMatchesMod(info, enabled, c.id);
- break;
- }
-
- default:
- {
- log::error("bad criteria type {}", c.type);
- break;
- }
- }
-
- if (c.inverse) {
- b = !b;
- }
-
- return b;
-}
-
-bool ModListSortProxy::categoryMatchesMod(
- ModInfo::Ptr info, bool enabled, int category) const
-{
- bool b = false;
-
- switch (category)
- {
- case CategoryFactory::Checked:
- {
- b = (enabled || info->alwaysEnabled());
- break;
- }
-
- case CategoryFactory::UpdateAvailable:
- {
- b = (info->updateAvailable() || info->downgradeAvailable());
- break;
- }
-
- case CategoryFactory::HasCategory:
- {
- b = !info->getCategories().empty();
- break;
- }
-
- case CategoryFactory::Conflict:
- {
- b = (hasConflictFlag(info->getConflictFlags()));
- break;
- }
-
- case CategoryFactory::HasHiddenFiles:
- {
- b = (info->hasFlag(ModInfo::FLAG_HIDDEN_FILES));
- break;
- }
-
- case CategoryFactory::Endorsed:
- {
- b = (info->endorsedState() == EndorsedState::ENDORSED_TRUE);
- break;
- }
-
- case CategoryFactory::Backup:
- {
- b = (info->hasFlag(ModInfo::FLAG_BACKUP));
- break;
- }
-
- case CategoryFactory::Managed:
- {
- b = (!info->hasFlag(ModInfo::FLAG_FOREIGN));
- break;
- }
-
- case CategoryFactory::HasGameData:
- {
- b = !info->hasFlag(ModInfo::FLAG_INVALID);
- break;
- }
-
- case CategoryFactory::HasNexusID:
- {
- // never show these
- if (
- info->hasFlag(ModInfo::FLAG_FOREIGN) ||
- info->hasFlag(ModInfo::FLAG_BACKUP) ||
- info->hasFlag(ModInfo::FLAG_OVERWRITE))
- {
- return false;
- }
-
- b = (info->nexusId() > 0);
- break;
- }
-
- case CategoryFactory::Tracked:
- {
- b = (info->trackedState() == TrackedState::TRACKED_TRUE);
- break;
- }
-
- default:
- {
- b = (info->categorySet(category));
- break;
- }
- }
-
- return b;
-}
-
-bool ModListSortProxy::contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const
-{
- return info->hasContent(content);
-}
-
-bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const
-{
- // don't check if there are no filters selected
- if (!m_FilterActive) {
- return true;
- }
-
-
- // special case for separators
- if (info->hasFlag(ModInfo::FLAG_SEPARATOR)) {
- switch (m_FilterSeparators)
- {
- case SeparatorFilter:
- {
- // filter normally
- break;
- }
-
- case SeparatorShow:
- {
- // force visible
- return true;
- }
-
- case SeparatorHide:
- {
- // force hide
- return false;
- }
- }
- }
-
-
- if (!m_Filter.isEmpty()) {
- bool display = false;
- QString filterCopy = QString(m_Filter);
- filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
- QStringList ORList = filterCopy.split(";", Qt::SkipEmptyParts);
-
- bool segmentGood = true;
-
- //split in ORSegments that internally use AND logic
- for (auto& ORSegment : ORList) {
- QStringList ANDKeywords = ORSegment.split(" ", Qt::SkipEmptyParts);
- segmentGood = true;
- bool foundKeyword = false;
-
- //check each word in the segment for match, each word needs to be matched but it doesn't matter where.
- for (auto& currentKeyword : ANDKeywords) {
- foundKeyword = false;
-
- //search keyword in name
- if (m_EnabledColumns[ModList::COL_NAME] &&
- info->name().contains(currentKeyword, Qt::CaseInsensitive)) {
- foundKeyword = true;
- }
-
- // Search by notes
- if (!foundKeyword &&
- m_EnabledColumns[ModList::COL_NOTES] &&
- info->comments().contains(currentKeyword, Qt::CaseInsensitive)) {
- foundKeyword = true;
- }
-
- // Search by categories
- if (!foundKeyword &&
- m_EnabledColumns[ModList::COL_CATEGORY]) {
- for (auto category : info->categories()) {
- if (category.contains(currentKeyword, Qt::CaseInsensitive)) {
- foundKeyword = true;
- break;
- }
- }
- }
-
- // Search by Nexus ID
- if (!foundKeyword &&
- m_EnabledColumns[ModList::COL_MODID]) {
- bool ok;
- int filterID = currentKeyword.toInt(&ok);
- if (ok) {
- int modID = info->nexusId();
- while (modID > 0) {
- if (modID == filterID) {
- foundKeyword = true;
- break;
- }
- modID = (int)(modID / 10);
- }
- }
- }
-
- if (!foundKeyword) {
- //currentKeword is missing from everything, AND fails and we need to check next ORsegment
- segmentGood = false;
- break;
- }
-
- }//for ANDKeywords loop
-
- if (segmentGood) {
- //the last AND loop didn't break so the ORSegments is true so mod matches filter
- display = true;
- break;
- }
-
- }//for ORList loop
-
- if (!display) {
- return false;
- }
- }//if (!m_CurrentFilter.isEmpty())
-
-
- if (m_FilterMode == FilterAnd) {
- return filterMatchesModAnd(info, enabled);
- }
- else {
- return filterMatchesModOr(info, enabled);
- }
-}
-
-void ModListSortProxy::setColumnVisible(int column, bool visible)
-{
- m_EnabledColumns[column] = visible;
-}
-
-void ModListSortProxy::setOptions(
- ModListSortProxy::FilterMode mode, SeparatorsMode separators)
-{
- if (m_FilterMode != mode || separators != m_FilterSeparators) {
- m_FilterMode = mode;
- m_FilterSeparators = separators;
- invalidateFilter();
- emit filterInvalidated();
- }
-}
-
-bool ModListSortProxy::filterAcceptsRow(int source_row, const QModelIndex &parent) const
-{
- if (m_Profile == nullptr) {
- return false;
- }
-
- if (source_row >= static_cast<int>(m_Profile->numMods())) {
- log::warn("invalid row index: {}", source_row);
- return false;
- }
-
- QModelIndex idx = sourceModel()->index(source_row, 0, parent);
- if (!idx.isValid()) {
- log::debug("invalid mod index");
- return false;
- }
-
- unsigned int index = ULONG_MAX;
- {
- bool ok = false;
- index = idx.data(ModList::IndexRole).toInt(&ok);
- if (!ok) {
- index = ULONG_MAX;
- }
- }
-
- if (sourceModel()->hasChildren(idx)) {
- // we need to check the separator itself first
- if (index < ModInfo::getNumMods() && ModInfo::getByIndex(index)->isSeparator()) {
- if (filterMatchesMod(ModInfo::getByIndex(index), false)) {
- return true;
- }
- }
- for (int i = 0; i < sourceModel()->rowCount(idx); ++i) {
- if (filterAcceptsRow(i, idx)) {
- return true;
- }
- }
-
- return false;
- } else {
- bool modEnabled = idx.sibling(source_row, 0).data(Qt::CheckStateRole).toInt() == Qt::Checked;
- return filterMatchesMod(ModInfo::getByIndex(index), modEnabled);
- }
-}
-
-bool ModListSortProxy::sourceIsByPriorityProxy() const
-{
- return dynamic_cast<ModListByPriorityProxy*>(sourceModel()) != nullptr;
-}
-
-bool ModListSortProxy::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const
-{
- ModListDropInfo dropInfo(data, *m_Organizer);
-
- if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
- return false;
- }
-
- // disable drop install with group proxy, except the one for collapsible separator
- // - it would be nice to be able to "install to category" or something like that but
- // it's a bit more complicated since the drop position is based on the category, so
- // just disabling for now
- if (dropInfo.isDownloadDrop()) {
- // maybe there is a cleaner way?
- if (qobject_cast<QtGroupingProxy*>(sourceModel())) {
- return false;
- }
- }
-
- // see dropMimeData for details
- if (sortOrder() == Qt::DescendingOrder && row != -1 && !sourceIsByPriorityProxy()) {
- --row;
- }
-
- return QSortFilterProxyModel::canDropMimeData(data, action, row, column, parent);
-}
-
-bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
- int row, int column, const QModelIndex &parent)
-{
- ModListDropInfo dropInfo(data, *m_Organizer);
-
- if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
- QWidget *wid = qApp->activeWindow()->findChild<QTreeView*>("modList");
- MessageDialog::showMessage(tr("Drag&Drop is only supported when sorting by priority"), wid);
- return false;
- }
-
- if (row == -1 && column == -1) {
- return sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
- }
-
- // in the regular model, when dropping between rows, the row-value passed to
- // the sourceModel is inconsistent between ascending and descending ordering
- //
- // we want to fix that, but we cannot do it for the by-priority proxy because
- // it messes up with non top-level items, so we simply forward the row and the
- // by-priority proxy will fix the row for us
- if (sortOrder() == Qt::DescendingOrder && row != -1 && !sourceIsByPriorityProxy()) {
- --row;
- }
-
- return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent);
-}
-
-void ModListSortProxy::setSourceModel(QAbstractItemModel *sourceModel)
-{
- QSortFilterProxyModel::setSourceModel(sourceModel);
- QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(sourceModel);
- if (proxy != nullptr) {
- sourceModel = proxy->sourceModel();
- }
- if (sourceModel) {
- connect(sourceModel, SIGNAL(aboutToChangeData()), this, SLOT(aboutToChangeData()), Qt::UniqueConnection);
- connect(sourceModel, SIGNAL(postDataChanged()), this, SLOT(postDataChanged()), Qt::UniqueConnection);
- }
-}
-
-void ModListSortProxy::aboutToChangeData()
-{
- // having a filter active when dataChanged is called caused a crash
- // (at least with some Qt versions)
- // this may be related to the fact that the item being edited may disappear from the view as a
- // result of the edit
- m_PreChangeCriteria = m_Criteria;
- setCriteria({});
-}
-
-void ModListSortProxy::postDataChanged()
-{
- // if the filter is re-activated right away the editor can't be deleted but becomes invisible
- // or at least the view continues to think it's being edited. As a result no new editor can be
- // opened
- QTimer::singleShot(10, [this] () {
- setCriteria(m_PreChangeCriteria);
- m_PreChangeCriteria.clear();
- });
-}
-
+/*
+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 "modlistsortproxy.h"
+#include "modlistbypriorityproxy.h"
+#include "modlistdropinfo.h"
+#include "modinfo.h"
+#include "profile.h"
+#include "messagedialog.h"
+#include "qtgroupingproxy.h"
+#include "organizercore.h"
+
+#include <log.h>
+#include <QMenu>
+#include <QCheckBox>
+#include <QWidgetAction>
+#include <QApplication>
+#include <QMimeData>
+#include <QDebug>
+#include <QTreeView>
+
+using namespace MOBase;
+
+ModListSortProxy::ModListSortProxy(Profile* profile, OrganizerCore * organizer)
+ : QSortFilterProxyModel(organizer)
+ , m_Organizer(organizer)
+ , m_Profile(profile)
+ , m_FilterActive(false)
+ , m_FilterMode(FilterAnd)
+ , m_FilterSeparators(SeparatorFilter)
+{
+ setDynamicSortFilter(true); // this seems to work without dynamicsortfilter
+ // but I don't know why. This should be necessary
+}
+
+void ModListSortProxy::setProfile(Profile *profile)
+{
+ m_Profile = profile;
+}
+
+void ModListSortProxy::updateFilterActive()
+{
+ m_FilterActive = (!m_Criteria.empty() || !m_Filter.isEmpty());
+ emit filterActive(m_FilterActive);
+}
+
+void ModListSortProxy::setCriteria(const std::vector<Criteria>& criteria)
+{
+ // avoid refreshing the filter unless we are checking all mods for update.
+ const bool changed = (criteria != m_Criteria);
+ const bool isForUpdates = (
+ !criteria.empty() &&
+ criteria[0].id == CategoryFactory::UpdateAvailable);
+
+ if (changed || isForUpdates) {
+ m_Criteria = criteria;
+ updateFilterActive();
+ invalidateFilter();
+ emit filterInvalidated();
+ }
+}
+
+unsigned long ModListSortProxy::flagsId(const std::vector<ModInfo::EFlag> &flags) const
+{
+ unsigned long result = 0;
+ for (ModInfo::EFlag flag : flags) {
+ if ((flag != ModInfo::FLAG_FOREIGN)
+ && (flag != ModInfo::FLAG_OVERWRITE)) {
+ result += 1 << (int)flag;
+ }
+ }
+ return result;
+}
+
+unsigned long ModListSortProxy::conflictFlagsId(const std::vector<ModInfo::EConflictFlag>& flags) const
+{
+ unsigned long result = 0;
+ for (ModInfo::EConflictFlag flag : flags) {
+ if ((flag != ModInfo::FLAG_OVERWRITE_CONFLICT)) {
+ result += 1 << (int)flag;
+ }
+ }
+ return result;
+}
+
+bool ModListSortProxy::lessThan(const QModelIndex &left,
+ const QModelIndex &right) const
+{
+ if (sourceModel()->hasChildren(left) || sourceModel()->hasChildren(right)) {
+ // when sorting by priority, we do not want to use the parent lessThan because
+ // it uses the display role which can be inconsistent (e.g. for backups)
+ if (sortColumn() != ModList::COL_PRIORITY) {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ else if (qobject_cast<QtGroupingProxy*>(sourceModel())) {
+ // if the underlying proxy is a QtGroupingProxy we need to rely on
+ // Qt::DisplayRole because the other roles are not correctly handled
+ // by that kind of proxy
+ return left.data(Qt::DisplayRole).toInt() < right.data(Qt::DisplayRole).toInt();
+ }
+ }
+
+ bool lOk, rOk;
+ int leftIndex = left.data(ModList::IndexRole).toInt(&lOk);
+ int rightIndex = right.data(ModList::IndexRole).toInt(&rOk);
+
+ if (!lOk || !rOk) {
+ return false;
+ }
+
+ ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex);
+ ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex);
+
+ bool lt = left.data(ModList::PriorityRole).toInt() < right.data(ModList::PriorityRole).toInt();
+
+ switch (left.column()) {
+ case ModList::COL_FLAGS: {
+ std::vector<ModInfo::EFlag> leftFlags = leftMod->getFlags();
+ std::vector<ModInfo::EFlag> rightFlags = rightMod->getFlags();
+ if (leftFlags.size() != rightFlags.size()) {
+ lt = leftFlags.size() < rightFlags.size();
+ } else {
+ lt = flagsId(leftFlags) < flagsId(rightFlags);
+ }
+ } break;
+ case ModList::COL_CONFLICTFLAGS: {
+ std::vector<ModInfo::EConflictFlag> leftFlags = leftMod->getConflictFlags();
+ std::vector<ModInfo::EConflictFlag> rightFlags = rightMod->getConflictFlags();
+ if (leftFlags.size() != rightFlags.size()) {
+ lt = leftFlags.size() < rightFlags.size();
+ }
+ else {
+ lt = conflictFlagsId(leftFlags) < conflictFlagsId(rightFlags);
+ }
+ } break;
+ case ModList::COL_CONTENT: {
+ const auto& lContents = leftMod->getContents();
+ const auto& rContents = rightMod->getContents();
+ unsigned int lValue = 0;
+ unsigned int rValue = 0;
+ m_Organizer->modDataContents().forEachContentIn(lContents, [&lValue](auto const& content) {
+ lValue += 2U << static_cast<unsigned int>(content.id());
+ });
+ m_Organizer->modDataContents().forEachContentIn(rContents, [&rValue](auto const& content) {
+ rValue += 2U << static_cast<unsigned int>(content.id());
+ });
+ lt = lValue < rValue;
+ } break;
+ case ModList::COL_NAME: {
+ int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive);
+ if (comp != 0)
+ lt = comp < 0;
+ } break;
+ case ModList::COL_CATEGORY: {
+ if (leftMod->primaryCategory() != rightMod->primaryCategory()) {
+ if (leftMod->primaryCategory() < 0) lt = false;
+ else if (rightMod->primaryCategory() < 0) lt = true;
+ else {
+ try {
+ CategoryFactory &categories = CategoryFactory::instance();
+ QString leftCatName = categories.getCategoryName(categories.getCategoryIndex(leftMod->primaryCategory()));
+ QString rightCatName = categories.getCategoryName(categories.getCategoryIndex(rightMod->primaryCategory()));
+ lt = leftCatName < rightCatName;
+ } catch (const std::exception &e) {
+ log::error("failed to compare categories: {}", e.what());
+ }
+ }
+ }
+ } break;
+ case ModList::COL_MODID: {
+ if (leftMod->nexusId() != rightMod->nexusId())
+ lt = leftMod->nexusId() < rightMod->nexusId();
+ } break;
+ case ModList::COL_VERSION: {
+ if (leftMod->version() != rightMod->version())
+ lt = leftMod->version() < rightMod->version();
+ } break;
+ case ModList::COL_INSTALLTIME: {
+ QDateTime leftTime = left.data().toDateTime();
+ QDateTime rightTime = right.data().toDateTime();
+ if (leftTime != rightTime)
+ return leftTime < rightTime;
+ } break;
+ case ModList::COL_GAME: {
+ if (leftMod->gameName() != rightMod->gameName()) {
+ lt = leftMod->gameName() < rightMod->gameName();
+ }
+ else {
+ int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive);
+ if (comp != 0)
+ lt = comp < 0;
+ }
+ } break;
+ case ModList::COL_NOTES: {
+ QString leftComments = leftMod->comments();
+ QString rightComments = rightMod->comments();
+ if (leftComments != rightComments) {
+ if (leftComments.isEmpty()) {
+ lt = sortOrder() == Qt::DescendingOrder;
+ } else if (rightComments.isEmpty()) {
+ lt = sortOrder() == Qt::AscendingOrder;
+ } else {
+ lt = leftComments < rightComments;
+ }
+ }
+ } break;
+ case ModList::COL_PRIORITY: {
+ if (leftMod->isBackup() != rightMod->isBackup()) {
+ lt = leftMod->isBackup();
+ }
+ else if (leftMod->isOverwrite() != rightMod->isOverwrite()) {
+ lt = rightMod->isOverwrite();
+ }
+ } break;
+ default: {
+ log::warn("Sorting is not defined for column {}", left.column());
+ } break;
+ }
+ return lt;
+}
+
+void ModListSortProxy::updateFilter(const QString& filter)
+{
+ m_Filter = filter;
+ updateFilterActive();
+ invalidateFilter();
+ emit filterInvalidated();
+}
+
+bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const
+{
+ for (ModInfo::EConflictFlag flag : flags) {
+ if ((flag == ModInfo::FLAG_CONFLICT_MIXED) ||
+ (flag == ModInfo::FLAG_CONFLICT_OVERWRITE) ||
+ (flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) ||
+ (flag == ModInfo::FLAG_CONFLICT_REDUNDANT) ||
+ (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE) ||
+ (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN) ||
+ (flag == ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED) ||
+ (flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE) ||
+ (flag == ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool ModListSortProxy::filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const
+{
+ for (auto&& c : m_Criteria) {
+ if (!criteriaMatchMod(info, enabled, c)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const
+{
+ for (auto&& c : m_Criteria) {
+ if (criteriaMatchMod(info, enabled, c)) {
+ return true;
+ }
+ }
+
+ if (!m_Criteria.empty()) {
+ // nothing matched
+ return false;
+ }
+
+ return true;
+}
+
+bool ModListSortProxy::optionsMatchMod(ModInfo::Ptr info, bool) const
+{
+ return true;
+}
+
+bool ModListSortProxy::criteriaMatchMod(
+ ModInfo::Ptr info, bool enabled, const Criteria& c) const
+{
+ bool b = false;
+
+ switch (c.type)
+ {
+ case TypeSpecial: // fall-through
+ case TypeCategory:
+ {
+ b = categoryMatchesMod(info, enabled, c.id);
+ break;
+ }
+
+ case TypeContent:
+ {
+ b = contentMatchesMod(info, enabled, c.id);
+ break;
+ }
+
+ default:
+ {
+ log::error("bad criteria type {}", c.type);
+ break;
+ }
+ }
+
+ if (c.inverse) {
+ b = !b;
+ }
+
+ return b;
+}
+
+bool ModListSortProxy::categoryMatchesMod(
+ ModInfo::Ptr info, bool enabled, int category) const
+{
+ bool b = false;
+
+ switch (category)
+ {
+ case CategoryFactory::Checked:
+ {
+ b = (enabled || info->alwaysEnabled());
+ break;
+ }
+
+ case CategoryFactory::UpdateAvailable:
+ {
+ b = (info->updateAvailable() || info->downgradeAvailable());
+ break;
+ }
+
+ case CategoryFactory::HasCategory:
+ {
+ b = !info->getCategories().empty();
+ break;
+ }
+
+ case CategoryFactory::Conflict:
+ {
+ b = (hasConflictFlag(info->getConflictFlags()));
+ break;
+ }
+
+ case CategoryFactory::HasHiddenFiles:
+ {
+ b = (info->hasFlag(ModInfo::FLAG_HIDDEN_FILES));
+ break;
+ }
+
+ case CategoryFactory::Endorsed:
+ {
+ b = (info->endorsedState() == EndorsedState::ENDORSED_TRUE);
+ break;
+ }
+
+ case CategoryFactory::Backup:
+ {
+ b = (info->hasFlag(ModInfo::FLAG_BACKUP));
+ break;
+ }
+
+ case CategoryFactory::Managed:
+ {
+ b = (!info->hasFlag(ModInfo::FLAG_FOREIGN));
+ break;
+ }
+
+ case CategoryFactory::HasGameData:
+ {
+ b = !info->hasFlag(ModInfo::FLAG_INVALID);
+ break;
+ }
+
+ case CategoryFactory::HasNexusID:
+ {
+ // never show these
+ if (
+ info->hasFlag(ModInfo::FLAG_FOREIGN) ||
+ info->hasFlag(ModInfo::FLAG_BACKUP) ||
+ info->hasFlag(ModInfo::FLAG_OVERWRITE))
+ {
+ return false;
+ }
+
+ b = (info->nexusId() > 0);
+ break;
+ }
+
+ case CategoryFactory::Tracked:
+ {
+ b = (info->trackedState() == TrackedState::TRACKED_TRUE);
+ break;
+ }
+
+ default:
+ {
+ b = (info->categorySet(category));
+ break;
+ }
+ }
+
+ return b;
+}
+
+bool ModListSortProxy::contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const
+{
+ return info->hasContent(content);
+}
+
+bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const
+{
+ // don't check if there are no filters selected
+ if (!m_FilterActive) {
+ return true;
+ }
+
+
+ // special case for separators
+ if (info->hasFlag(ModInfo::FLAG_SEPARATOR)) {
+ switch (m_FilterSeparators)
+ {
+ case SeparatorFilter:
+ {
+ // filter normally
+ break;
+ }
+
+ case SeparatorShow:
+ {
+ // force visible
+ return true;
+ }
+
+ case SeparatorHide:
+ {
+ // force hide
+ return false;
+ }
+ }
+ }
+
+
+ if (!m_Filter.isEmpty()) {
+ bool display = false;
+ QString filterCopy = QString(m_Filter);
+ filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
+ QStringList ORList = filterCopy.split(";", Qt::SkipEmptyParts);
+
+ bool segmentGood = true;
+
+ //split in ORSegments that internally use AND logic
+ for (auto& ORSegment : ORList) {
+ QStringList ANDKeywords = ORSegment.split(" ", Qt::SkipEmptyParts);
+ segmentGood = true;
+ bool foundKeyword = false;
+
+ //check each word in the segment for match, each word needs to be matched but it doesn't matter where.
+ for (auto& currentKeyword : ANDKeywords) {
+ foundKeyword = false;
+
+ //search keyword in name
+ if (m_EnabledColumns[ModList::COL_NAME] &&
+ info->name().contains(currentKeyword, Qt::CaseInsensitive)) {
+ foundKeyword = true;
+ }
+
+ // Search by notes
+ if (!foundKeyword &&
+ m_EnabledColumns[ModList::COL_NOTES] &&
+ info->comments().contains(currentKeyword, Qt::CaseInsensitive)) {
+ foundKeyword = true;
+ }
+
+ // Search by categories
+ if (!foundKeyword &&
+ m_EnabledColumns[ModList::COL_CATEGORY]) {
+ for (auto category : info->categories()) {
+ if (category.contains(currentKeyword, Qt::CaseInsensitive)) {
+ foundKeyword = true;
+ break;
+ }
+ }
+ }
+
+ // Search by Nexus ID
+ if (!foundKeyword &&
+ m_EnabledColumns[ModList::COL_MODID]) {
+ bool ok;
+ int filterID = currentKeyword.toInt(&ok);
+ if (ok) {
+ int modID = info->nexusId();
+ while (modID > 0) {
+ if (modID == filterID) {
+ foundKeyword = true;
+ break;
+ }
+ modID = (int)(modID / 10);
+ }
+ }
+ }
+
+ if (!foundKeyword) {
+ //currentKeword is missing from everything, AND fails and we need to check next ORsegment
+ segmentGood = false;
+ break;
+ }
+
+ }//for ANDKeywords loop
+
+ if (segmentGood) {
+ //the last AND loop didn't break so the ORSegments is true so mod matches filter
+ display = true;
+ break;
+ }
+
+ }//for ORList loop
+
+ if (!display) {
+ return false;
+ }
+ }//if (!m_CurrentFilter.isEmpty())
+
+
+ if (m_FilterMode == FilterAnd) {
+ return filterMatchesModAnd(info, enabled);
+ }
+ else {
+ return filterMatchesModOr(info, enabled);
+ }
+}
+
+void ModListSortProxy::setColumnVisible(int column, bool visible)
+{
+ m_EnabledColumns[column] = visible;
+}
+
+void ModListSortProxy::setOptions(
+ ModListSortProxy::FilterMode mode, SeparatorsMode separators)
+{
+ if (m_FilterMode != mode || separators != m_FilterSeparators) {
+ m_FilterMode = mode;
+ m_FilterSeparators = separators;
+ invalidateFilter();
+ emit filterInvalidated();
+ }
+}
+
+bool ModListSortProxy::filterAcceptsRow(int source_row, const QModelIndex &parent) const
+{
+ if (m_Profile == nullptr) {
+ return false;
+ }
+
+ if (source_row >= static_cast<int>(m_Profile->numMods())) {
+ log::warn("invalid row index: {}", source_row);
+ return false;
+ }
+
+ QModelIndex idx = sourceModel()->index(source_row, 0, parent);
+ if (!idx.isValid()) {
+ log::debug("invalid mod index");
+ return false;
+ }
+
+ unsigned int index = ULONG_MAX;
+ {
+ bool ok = false;
+ index = idx.data(ModList::IndexRole).toInt(&ok);
+ if (!ok) {
+ index = ULONG_MAX;
+ }
+ }
+
+ if (sourceModel()->hasChildren(idx)) {
+ // we need to check the separator itself first
+ if (index < ModInfo::getNumMods() && ModInfo::getByIndex(index)->isSeparator()) {
+ if (filterMatchesMod(ModInfo::getByIndex(index), false)) {
+ return true;
+ }
+ }
+ for (int i = 0; i < sourceModel()->rowCount(idx); ++i) {
+ if (filterAcceptsRow(i, idx)) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ bool modEnabled = idx.sibling(source_row, 0).data(Qt::CheckStateRole).toInt() == Qt::Checked;
+ return filterMatchesMod(ModInfo::getByIndex(index), modEnabled);
+ }
+}
+
+bool ModListSortProxy::sourceIsByPriorityProxy() const
+{
+ return dynamic_cast<ModListByPriorityProxy*>(sourceModel()) != nullptr;
+}
+
+bool ModListSortProxy::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const
+{
+ ModListDropInfo dropInfo(data, *m_Organizer);
+
+ if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
+ return false;
+ }
+
+ // disable drop install with group proxy, except the one for collapsible separator
+ // - it would be nice to be able to "install to category" or something like that but
+ // it's a bit more complicated since the drop position is based on the category, so
+ // just disabling for now
+ if (dropInfo.isDownloadDrop()) {
+ // maybe there is a cleaner way?
+ if (qobject_cast<QtGroupingProxy*>(sourceModel())) {
+ return false;
+ }
+ }
+
+ // see dropMimeData for details
+ if (sortOrder() == Qt::DescendingOrder && row != -1 && !sourceIsByPriorityProxy()) {
+ --row;
+ }
+
+ return QSortFilterProxyModel::canDropMimeData(data, action, row, column, parent);
+}
+
+bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int row, int column, const QModelIndex &parent)
+{
+ ModListDropInfo dropInfo(data, *m_Organizer);
+
+ if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
+ QWidget *wid = qApp->activeWindow()->findChild<QTreeView*>("modList");
+ MessageDialog::showMessage(tr("Drag&Drop is only supported when sorting by priority"), wid);
+ return false;
+ }
+
+ if (row == -1 && column == -1) {
+ return sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
+ }
+
+ // in the regular model, when dropping between rows, the row-value passed to
+ // the sourceModel is inconsistent between ascending and descending ordering
+ //
+ // we want to fix that, but we cannot do it for the by-priority proxy because
+ // it messes up with non top-level items, so we simply forward the row and the
+ // by-priority proxy will fix the row for us
+ if (sortOrder() == Qt::DescendingOrder && row != -1 && !sourceIsByPriorityProxy()) {
+ --row;
+ }
+
+ return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent);
+}
+
+void ModListSortProxy::setSourceModel(QAbstractItemModel *sourceModel)
+{
+ QSortFilterProxyModel::setSourceModel(sourceModel);
+ QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(sourceModel);
+ if (proxy != nullptr) {
+ sourceModel = proxy->sourceModel();
+ }
+ if (sourceModel) {
+ connect(sourceModel, SIGNAL(aboutToChangeData()), this, SLOT(aboutToChangeData()), Qt::UniqueConnection);
+ connect(sourceModel, SIGNAL(postDataChanged()), this, SLOT(postDataChanged()), Qt::UniqueConnection);
+ }
+}
+
+void ModListSortProxy::aboutToChangeData()
+{
+ // having a filter active when dataChanged is called caused a crash
+ // (at least with some Qt versions)
+ // this may be related to the fact that the item being edited may disappear from the view as a
+ // result of the edit
+ m_PreChangeCriteria = m_Criteria;
+ setCriteria({});
+}
+
+void ModListSortProxy::postDataChanged()
+{
+ // if the filter is re-activated right away the editor can't be deleted but becomes invisible
+ // or at least the view continues to think it's being edited. As a result no new editor can be
+ // opened
+ QTimer::singleShot(10, [this] () {
+ setCriteria(m_PreChangeCriteria);
+ m_PreChangeCriteria.clear();
+ });
+}
+
diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h
index 23be77ed..b59ab358 100644
--- a/src/modlistsortproxy.h
+++ b/src/modlistsortproxy.h
@@ -1,172 +1,172 @@
-/*
-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 MODLISTSORTPROXY_H
-#define MODLISTSORTPROXY_H
-
-#include <QSortFilterProxyModel>
-#include <bitset>
-#include "modlist.h"
-
-class Profile;
-class OrganizerCore;
-
-class ModListSortProxy : public QSortFilterProxyModel
-{
- Q_OBJECT
-
-public:
- enum FilterMode
- {
- FilterAnd,
- FilterOr
- };
-
- enum CriteriaType {
- TypeSpecial,
- TypeCategory,
- TypeContent
- };
-
- enum SeparatorsMode
- {
- SeparatorFilter,
- SeparatorShow,
- SeparatorHide
- };
-
- struct Criteria
- {
- CriteriaType type;
- int id;
- bool inverse;
-
- bool operator==(const Criteria& other) const
- {
- return
- (type == other.type) &&
- (id == other.id) &&
- (inverse == other.inverse);
- }
-
- bool operator!=(const Criteria& other) const
- {
- return !(*this == other);
- }
- };
-
-public:
-
- explicit ModListSortProxy(Profile *profile, OrganizerCore *organizer);
-
- void setProfile(Profile *profile);
-
- bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override;
- bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
-
- virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
-
- /**
- * @brief tests if a filtere matches for a mod
- * @param info mod information
- * @param enabled true if the mod is currently active
- * @return true if current active filters match for the specified mod
- */
- bool filterMatchesMod(ModInfo::Ptr info, bool enabled) const;
-
- /**
- * @return true if a filter is currently active
- */
- bool isFilterActive() const { return m_FilterActive; }
-
- void setCriteria(const std::vector<Criteria>& criteria);
- void setOptions(FilterMode mode, SeparatorsMode separators);
-
- auto filterMode() const { return m_FilterMode; }
- auto separatorsMode() const { return m_FilterSeparators; }
-
- /**
- * @brief tests if the specified index has child nodes
- * @param parent the node to test
- * @return true if there are child nodes
- */
- virtual bool hasChildren ( const QModelIndex & parent = QModelIndex() ) const {
- return rowCount(parent) > 0;
- }
-
- /**
- * @brief sets whether a column is visible
- * @param column the index of the column
- * @param visible the visibility of the column
- */
- void setColumnVisible(int column, bool visible);
-
-public slots:
-
- void updateFilter(const QString &filter);
-
-signals:
-
- void filterActive(bool active);
- void filterInvalidated();
-
-protected:
-
- virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
- virtual bool filterAcceptsRow(int row, const QModelIndex &parent) const;
-
-private:
-
- unsigned long flagsId(const std::vector<ModInfo::EFlag> &flags) const;
- unsigned long conflictFlagsId(const std::vector<ModInfo::EConflictFlag>& flags) const;
- bool hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const;
- void updateFilterActive();
- bool filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const;
- bool filterMatchesModOr(ModInfo::Ptr info, bool enabled) const;
-
- // check if the source model is the by-priority proxy
- //
- bool sourceIsByPriorityProxy() const;
-
-private slots:
-
- void aboutToChangeData();
- void postDataChanged();
-
-private:
- OrganizerCore* m_Organizer;
-
- Profile* m_Profile;
- std::vector<Criteria> m_Criteria;
- QString m_Filter;
- std::bitset<ModList::COL_LASTCOLUMN + 1> m_EnabledColumns;
-
- bool m_FilterActive;
- FilterMode m_FilterMode;
- SeparatorsMode m_FilterSeparators;
-
- std::vector<Criteria> m_PreChangeCriteria;
-
- bool optionsMatchMod(ModInfo::Ptr info, bool enabled) const;
- bool criteriaMatchMod(ModInfo::Ptr info, bool enabled, const Criteria& c) const;
- bool categoryMatchesMod(ModInfo::Ptr info, bool enabled, int category) const;
- bool contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const;
-};
-
-#endif // MODLISTSORTPROXY_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 MODLISTSORTPROXY_H
+#define MODLISTSORTPROXY_H
+
+#include <QSortFilterProxyModel>
+#include <bitset>
+#include "modlist.h"
+
+class Profile;
+class OrganizerCore;
+
+class ModListSortProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ enum FilterMode
+ {
+ FilterAnd,
+ FilterOr
+ };
+
+ enum CriteriaType {
+ TypeSpecial,
+ TypeCategory,
+ TypeContent
+ };
+
+ enum SeparatorsMode
+ {
+ SeparatorFilter,
+ SeparatorShow,
+ SeparatorHide
+ };
+
+ struct Criteria
+ {
+ CriteriaType type;
+ int id;
+ bool inverse;
+
+ bool operator==(const Criteria& other) const
+ {
+ return
+ (type == other.type) &&
+ (id == other.id) &&
+ (inverse == other.inverse);
+ }
+
+ bool operator!=(const Criteria& other) const
+ {
+ return !(*this == other);
+ }
+ };
+
+public:
+
+ explicit ModListSortProxy(Profile *profile, OrganizerCore *organizer);
+
+ void setProfile(Profile *profile);
+
+ bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override;
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
+
+ virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
+
+ /**
+ * @brief tests if a filtere matches for a mod
+ * @param info mod information
+ * @param enabled true if the mod is currently active
+ * @return true if current active filters match for the specified mod
+ */
+ bool filterMatchesMod(ModInfo::Ptr info, bool enabled) const;
+
+ /**
+ * @return true if a filter is currently active
+ */
+ bool isFilterActive() const { return m_FilterActive; }
+
+ void setCriteria(const std::vector<Criteria>& criteria);
+ void setOptions(FilterMode mode, SeparatorsMode separators);
+
+ auto filterMode() const { return m_FilterMode; }
+ auto separatorsMode() const { return m_FilterSeparators; }
+
+ /**
+ * @brief tests if the specified index has child nodes
+ * @param parent the node to test
+ * @return true if there are child nodes
+ */
+ virtual bool hasChildren ( const QModelIndex & parent = QModelIndex() ) const {
+ return rowCount(parent) > 0;
+ }
+
+ /**
+ * @brief sets whether a column is visible
+ * @param column the index of the column
+ * @param visible the visibility of the column
+ */
+ void setColumnVisible(int column, bool visible);
+
+public slots:
+
+ void updateFilter(const QString &filter);
+
+signals:
+
+ void filterActive(bool active);
+ void filterInvalidated();
+
+protected:
+
+ virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
+ virtual bool filterAcceptsRow(int row, const QModelIndex &parent) const;
+
+private:
+
+ unsigned long flagsId(const std::vector<ModInfo::EFlag> &flags) const;
+ unsigned long conflictFlagsId(const std::vector<ModInfo::EConflictFlag>& flags) const;
+ bool hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const;
+ void updateFilterActive();
+ bool filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const;
+ bool filterMatchesModOr(ModInfo::Ptr info, bool enabled) const;
+
+ // check if the source model is the by-priority proxy
+ //
+ bool sourceIsByPriorityProxy() const;
+
+private slots:
+
+ void aboutToChangeData();
+ void postDataChanged();
+
+private:
+ OrganizerCore* m_Organizer;
+
+ Profile* m_Profile;
+ std::vector<Criteria> m_Criteria;
+ QString m_Filter;
+ std::bitset<ModList::COL_LASTCOLUMN + 1> m_EnabledColumns;
+
+ bool m_FilterActive;
+ FilterMode m_FilterMode;
+ SeparatorsMode m_FilterSeparators;
+
+ std::vector<Criteria> m_PreChangeCriteria;
+
+ bool optionsMatchMod(ModInfo::Ptr info, bool enabled) const;
+ bool criteriaMatchMod(ModInfo::Ptr info, bool enabled, const Criteria& c) const;
+ bool categoryMatchesMod(ModInfo::Ptr info, bool enabled, int category) const;
+ bool contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const;
+};
+
+#endif // MODLISTSORTPROXY_H
diff --git a/src/modlistview.cpp b/src/modlistview.cpp
index 80ac5c28..7eb06f98 100644
--- a/src/modlistview.cpp
+++ b/src/modlistview.cpp
@@ -1,1465 +1,1465 @@
-#include "modlistview.h"
-#include <QUrl>
-#include <QMimeData>
-#include <QProxyStyle>
-
-#include <widgetutility.h>
-
-#include "filesystemutilities.h"
-#include <report.h>
-
-#include "ui_mainwindow.h"
-
-#include "filterlist.h"
-#include "organizercore.h"
-#include "modlist.h"
-#include "modlistsortproxy.h"
-#include "modlistbypriorityproxy.h"
-#include "log.h"
-#include "modflagicondelegate.h"
-#include "modconflicticondelegate.h"
-#include "modcontenticondelegate.h"
-#include "modlistversiondelegate.h"
-#include "modlistviewactions.h"
-#include "modlistdropinfo.h"
-#include "modlistcontextmenu.h"
-#include "genericicondelegate.h"
-#include "copyeventfilter.h"
-#include "shared/fileentry.h"
-#include "shared/directoryentry.h"
-#include "shared/filesorigin.h"
-#include "mainwindow.h"
-#include "modelutils.h"
-
-using namespace MOBase;
-using namespace MOShared;
-
-// delegate to remove indentation for mods when using collapsible
-// separator
-//
-// the delegate works by removing the indentation of the child items
-// before drawing, but unfortunately this normally breaks event
-// handling (e.g. checkbox, edit, etc.), so we also need to override
-// the visualRect() function from the mod list view.
-//
-class ModListStyledItemDelegate : public QStyledItemDelegate
-{
- ModListView* m_view;
-
-public:
-
- ModListStyledItemDelegate(ModListView* view) :
- QStyledItemDelegate(view), m_view(view) { }
-
- void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
- {
- // the parent version always overwrite the background brush, so
- // we need to save it and restore it
- auto backgroundColor = option->backgroundBrush.color();
- QStyledItemDelegate::initStyleOption(option, index);
-
- if (backgroundColor.isValid()) {
- option->backgroundBrush = backgroundColor;
- }
- }
-
- void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
- {
- QStyleOptionViewItem opt(option);
-
- // remove items indentaiton when using collapsible separators
- if (index.column() == 0 && m_view->hasCollapsibleSeparators()) {
- if (!index.model()->hasChildren(index) && index.parent().isValid()) {
- auto parentIndex = index.parent().data(ModList::IndexRole).toInt();
- if (ModInfo::getByIndex(parentIndex)->isSeparator()) {
- opt.rect.adjust(-m_view->indentation(), 0, 0, 0);
- }
- }
- }
-
- // compute required color from children, otherwise fallback to the
- // color from the model, and draw the background here
- auto color = m_view->markerColor(index);
- if (!color.isValid()) {
- color = index.data(Qt::BackgroundRole).value<QColor>();
- }
- opt.backgroundBrush = color;
-
- // we need to find the background color to compute the ideal text color
- // but the mod list view uses alternate color so we need to find the
- // right color
- auto bg = opt.palette.base().color();
- if (opt.features & QStyleOptionViewItem::Alternate) {
- bg = opt.palette.alternateBase().color();
- }
-
- // compute ideal foreground color for some rows
- if (color.isValid()) {
- if (((index.column() == ModList::COL_NAME || index.column() == ModList::COL_PRIORITY)
- && ModInfo::getByIndex(index.data(ModList::IndexRole).toInt())->isSeparator())
- || index.column() == ModList::COL_NOTES) {
-
- // combine the color with the background and then find the "ideal" text color
- const auto a = color.alpha() / 255.;
- int r = (1 - a) * bg.red() + a * color.red(),
- g = (1 - a) * bg.green() + a * color.green(),
- b = (1 - a) * bg.blue() + a * color.blue();
- opt.palette.setBrush(QPalette::Text, ColorSettings::idealTextColor(QColor(r, g, b)));
- }
- }
-
- QStyledItemDelegate::paint(painter, opt, index);
- }
-};
-
-class ModListViewMarkingScrollBar : public ViewMarkingScrollBar {
- ModListView* m_view;
-public:
- ModListViewMarkingScrollBar(ModListView* view) :
- ViewMarkingScrollBar(view, ModList::ScrollMarkRole), m_view(view) { }
-
-
- QColor color(const QModelIndex& index) const override
- {
- auto color = m_view->markerColor(index);
- if (!color.isValid()) {
- color = ViewMarkingScrollBar::color(index);
- }
- return color;
- }
-
-};
-
-ModListView::ModListView(QWidget* parent)
- : QTreeView(parent)
- , m_core(nullptr)
- , m_sortProxy(nullptr)
- , m_byPriorityProxy(nullptr)
- , m_byCategoryProxy(nullptr)
- , m_byNexusIdProxy(nullptr)
- , m_markers{ {}, {}, {}, {}, {}, {} }
- , m_scrollbar(new ModListViewMarkingScrollBar(this))
-{
- setVerticalScrollBar(m_scrollbar);
- MOBase::setCustomizableColumns(this);
- setAutoExpandDelay(750);
-
- setItemDelegate(new ModListStyledItemDelegate(this));
-
- connect(this, &ModListView::doubleClicked, this, &ModListView::onDoubleClicked);
- connect(this, &ModListView::customContextMenuRequested, this, &ModListView::onCustomContextMenuRequested);
-
- // the timeout is pretty small because its main purpose is to avoid
- // refreshing multiple times when calling expandAll() or collapseAll()
- // which emit a lots of expanded/collapsed signals in a very small
- // time window
- m_refreshMarkersTimer.setInterval(50);
- m_refreshMarkersTimer.setSingleShot(true);
- connect(&m_refreshMarkersTimer, &QTimer::timeout, [=] { refreshMarkersAndPlugins(); });
-
- installEventFilter(new CopyEventFilter(this, [=](auto& index) {
- QVariant mIndex = index.data(ModList::IndexRole);
- QString name = index.data(Qt::DisplayRole).toString();
- if (mIndex.isValid() && hasCollapsibleSeparators()) {
- ModInfo::Ptr info = ModInfo::getByIndex(mIndex.toInt());
- if (info->isSeparator()) {
- name = "[" + name + "]";
- }
- }
- else if (model()->hasChildren(index)) {
- name = "[" + name + "]";
- }
- return name;
- }));
-}
-
-void ModListView::refresh()
-{
- updateGroupByProxy();
-}
-
-void ModListView::onProfileChanged(Profile* oldProfile, Profile* newProfile)
-{
- const auto perProfileSeparators =
- m_core->settings().interface().collapsibleSeparatorsPerProfile();
-
- // save expanded/collapsed state of separators
- if (oldProfile && perProfileSeparators) {
- auto& collapsed = m_collapsed[m_byPriorityProxy];
- oldProfile->storeSetting("UserInterface", "collapsed_separators", QStringList(collapsed.begin(), collapsed.end()));
- }
-
- m_sortProxy->setProfile(newProfile);
- m_byPriorityProxy->setProfile(newProfile);
-
- if (newProfile && perProfileSeparators) {
- auto collapsed = newProfile->setting("UserInterface", "collapsed_separators", QStringList()).toStringList();
- m_collapsed[m_byPriorityProxy] = { collapsed.begin(), collapsed.end() };
- }
-}
-
-bool ModListView::hasCollapsibleSeparators() const
-{
- return groupByMode() == GroupByMode::SEPARATOR;
-}
-
-int ModListView::sortColumn() const
-{
- return m_sortProxy ? m_sortProxy->sortColumn() : -1;
-}
-
-Qt::SortOrder ModListView::sortOrder() const
-{
- return m_sortProxy ? m_sortProxy->sortOrder() : Qt::AscendingOrder;
-}
-
-bool ModListView::isFilterActive() const
-{
- return m_sortProxy && m_sortProxy->isFilterActive();
-}
-
-ModListView::GroupByMode ModListView::groupByMode() const
-{
- if (m_sortProxy == nullptr) {
- return GroupByMode::NONE;
- }
- else if (m_sortProxy->sourceModel() == m_byPriorityProxy) {
- return GroupByMode::SEPARATOR;
- }
- else if (m_sortProxy->sourceModel() == m_byCategoryProxy) {
- return GroupByMode::CATEGORY;
- }
- else if (m_sortProxy->sourceModel() == m_byNexusIdProxy) {
- return GroupByMode::NEXUS_ID;
- }
- else {
- return GroupByMode::NONE;
- }
-}
-
-ModListViewActions& ModListView::actions() const
-{
- return *m_actions;
-}
-
-std::optional<unsigned int> ModListView::nextMod(unsigned int modIndex) const
-{
- const QModelIndex start = indexModelToView(m_core->modList()->index(modIndex, 0));
-
- auto index = start;
-
- for (;;) {
- index = nextIndex(index);
-
- if (index == start || !index.isValid()) {
- // wrapped around, give up
- break;
- }
-
- modIndex = index.data(ModList::IndexRole).toInt();
-
- ModInfo::Ptr mod = ModInfo::getByIndex(modIndex);
-
- // skip overwrite, backups and separators
- if (mod->isOverwrite() || mod->isBackup() || mod->isSeparator()) {
- continue;
- }
-
- return modIndex;
- }
-
- return {};
-}
-
-std::optional<unsigned int> ModListView::prevMod(unsigned int modIndex) const
-{
- const QModelIndex start = indexModelToView(m_core->modList()->index(modIndex, 0));
-
- auto index = start;
-
- for (;;) {
- index = prevIndex(index);
-
- if (index == start || !index.isValid()) {
- // wrapped around, give up
- break;
- }
-
- modIndex = index.data(ModList::IndexRole).toInt();
-
- // skip overwrite, backups and separators
- ModInfo::Ptr mod = ModInfo::getByIndex(modIndex);
- if (mod->isOverwrite() || mod->isBackup() || mod->isSeparator()) {
- continue;
- }
-
- return modIndex;
- }
-
- return {};
-}
-
-void ModListView::invalidateFilter()
-{
- m_sortProxy->invalidate();
-}
-
-void ModListView::setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
-{
- m_sortProxy->setCriteria(criteria);
-}
-
-void ModListView::setFilterOptions(ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep)
-{
- m_sortProxy->setOptions(mode, sep);
-}
-
-bool ModListView::isModVisible(unsigned int index) const
-{
- return m_sortProxy->filterMatchesMod(ModInfo::getByIndex(index), m_core->currentProfile()->modEnabled(index));
-}
-
-bool ModListView::isModVisible(ModInfo::Ptr mod) const
-{
- return m_sortProxy->filterMatchesMod(mod, m_core->currentProfile()->modEnabled(ModInfo::getIndex(mod->name())));
-}
-
-QModelIndex ModListView::indexModelToView(const QModelIndex& index) const
-{
- return ::indexModelToView(index, this);
-}
-
-QModelIndexList ModListView::indexModelToView(const QModelIndexList& index) const
-{
- return ::indexModelToView(index, this);
-}
-
-QModelIndex ModListView::indexViewToModel(const QModelIndex& index) const
-{
- return ::indexViewToModel(index, m_core->modList());
-}
-
-QModelIndexList ModListView::indexViewToModel(const QModelIndexList& index) const
-{
- return ::indexViewToModel(index, m_core->modList());
-}
-
-QModelIndex ModListView::nextIndex(const QModelIndex& index) const
-{
- auto* model = index.model();
- if (!model) {
- return {};
- }
-
- if (model->rowCount(index) > 0) {
- return model->index(0, index.column(), index);
- }
-
- if (index.parent().isValid()) {
- if (index.row() + 1 < model->rowCount(index.parent())) {
- return index.model()->index(index.row() + 1, index.column(), index.parent());
- }
- else {
- return index.model()->index((index.parent().row() + 1) % model->rowCount(index.parent().parent()), index.column(), index.parent().parent());;
- }
- }
- else {
- return index.model()->index((index.row() + 1) % model->rowCount(index.parent()), index.column(), index.parent());
- }
-}
-
-QModelIndex ModListView::prevIndex(const QModelIndex& index) const
-{
- if (index.row() == 0 && index.parent().isValid()) {
- return index.parent();
- }
-
- auto* model = index.model();
- if (!model) {
- return {};
- }
-
- auto prev = model->index((index.row() - 1) % model->rowCount(index.parent()), index.column(), index.parent());
-
- if (model->rowCount(prev) > 0) {
- return model->index(model->rowCount(prev) - 1, index.column(), prev);
- }
-
- return prev;
-}
-
-std::pair<QModelIndex, QModelIndexList> ModListView::selected() const
-{
- return { indexViewToModel(currentIndex()), indexViewToModel(selectionModel()->selectedRows()) };
-}
-
-void ModListView::setSelected(const QModelIndex& current, const QModelIndexList& selected)
-{
- setCurrentIndex(indexModelToView(current));
- for (auto idx : selected) {
- selectionModel()->select(indexModelToView(idx), QItemSelectionModel::Select | QItemSelectionModel::Rows);
- }
-}
-
-void ModListView::scrollToAndSelect(const QModelIndex& index)
-{
- scrollToAndSelect(QModelIndexList{index});
-}
-
-void ModListView::scrollToAndSelect(const QModelIndexList& indexes, const QModelIndex& current)
-{
- // focus, scroll to and select
- if (!current.isValid() && indexes.isEmpty()) {
- return;
- }
- scrollTo(current.isValid() ? current : indexes.first());
- setCurrentIndex(current.isValid() ? current : indexes.first());
- QItemSelection selection;
- for (auto& idx : indexes) {
- selection.select(idx, idx);
- }
- selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
- QTimer::singleShot(50, [=] { setFocus(); });
-}
-
-void ModListView::refreshExpandedItems()
-{
- auto* model = m_sortProxy->sourceModel();
- for (auto i = 0; i < model->rowCount(); ++i) {
- auto idx = model->index(i, 0);
- if (!m_collapsed[model].contains(idx.data(Qt::DisplayRole).toString())) {
- setExpanded(m_sortProxy->mapFromSource(idx), true);
- }
- }
-}
-
-void ModListView::onModPrioritiesChanged(const QModelIndexList& indices)
-{
- // expand separator whose priority has changed and parents
- for (auto index : indices) {
- auto idx = indexModelToView(index);
- if (hasCollapsibleSeparators() && model()->hasChildren(idx)) {
- setExpanded(idx, true);
- }
- if (idx.parent().isValid()) {
- setExpanded(idx.parent(), true);
- }
- }
-
- setOverwriteMarkers(selectionModel()->selectedRows());
-}
-
-void ModListView::onModInstalled(const QString& modName)
-{
- unsigned int index = ModInfo::getIndex(modName);
-
- if (index == UINT_MAX) {
- return;
- }
-
- QModelIndex qIndex = indexModelToView(m_core->modList()->index(index, 0));
-
- if (hasCollapsibleSeparators() && qIndex.parent().isValid()) {
- setExpanded(qIndex.parent(), true);
- }
-
- scrollToAndSelect(qIndex);
-}
-
-void ModListView::onModFilterActive(bool filterActive)
-{
- ui.clearFilters->setVisible(filterActive);
- if (filterActive) {
- setStyleSheet("QTreeView { border: 2px ridge #f00; }");
- ui.counter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }");
- }
- else if (ui.groupBy->currentIndex() != GroupBy::NONE) {
- setStyleSheet("QTreeView { border: 2px ridge #337733; }");
- ui.counter->setStyleSheet("");
- }
- else {
- setStyleSheet("");
- ui.counter->setStyleSheet("");
- }
-}
-
-ModListView::ModCounters ModListView::counters() const
-{
- ModCounters c;
-
- auto hasFlag = [](std::vector<ModInfo::EFlag> flags, ModInfo::EFlag filter) {
- return std::find(flags.begin(), flags.end(), filter) != flags.end();
- };
-
-
- for (unsigned int index = 0; index < ModInfo::getNumMods(); ++index) {
- auto info = ModInfo::getByIndex(index);
- const auto flags = info->getFlags();
-
- const bool enabled = m_core->currentProfile()->modEnabled(index);
- const bool visible = m_sortProxy->filterMatchesMod(info, enabled);
-
- if (info->isBackup()) {
- c.backup++;
- if (visible) c.visible.backup++;
- }
- else if (info->isForeign()) {
- c.foreign++;
- if (visible) c.visible.foreign++;
- }
- else if (info->isSeparator()) {
- c.separator++;
- if (visible) c.visible.separator++;
- }
- else if (!info->isOverwrite()) {
- c.regular++;
- if (visible) c.visible.regular++;
- if (enabled) {
- c.active++;
- if (visible) c.visible.active++;
- }
- }
- }
-
- return c;
-}
-
-void ModListView::updateModCount()
-{
- const auto c = counters();
-
- ui.counter->display(c.visible.active);
- ui.counter->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(c.active)
- .arg(c.regular)
- .arg(c.visible.active)
- .arg(c.visible.regular)
- .arg(c.foreign)
- .arg(c.visible.foreign)
- .arg(c.backup)
- .arg(c.visible.backup)
- .arg(c.separator)
- .arg(c.visible.separator)
- );
-}
-
-void ModListView::refreshFilters()
-{
- auto [current, sourceRows] = selected();
-
- setCurrentIndex(QModelIndex());
- m_filters->refresh();
-
- setSelected(current, sourceRows);
-}
-
-void ModListView::onExternalFolderDropped(const QUrl& url, int priority)
-{
- setWindowState(Qt::WindowActive);
-
- QFileInfo fileInfo(url.toLocalFile());
-
- GuessedValue<QString> name;
- name.setFilter(&fixDirectoryName);
- name.update(fileInfo.fileName(), GUESS_PRESET);
-
- do {
- bool ok;
- name.update(QInputDialog::getText(this, tr("Copy Folder..."),
- tr("This will copy the content of %1 to a new mod.\n"
- "Please enter the name:").arg(fileInfo.fileName()), QLineEdit::Normal, name, &ok),
- GUESS_USER);
- if (!ok) {
- return;
- }
- } while (name->isEmpty());
-
- if (m_core->modList()->getMod(name) != nullptr) {
- reportError(tr("A mod with this name already exists."));
- return;
- }
-
- IModInterface* newMod = m_core->createMod(name);
- if (!newMod) {
- return;
- }
-
- // TODO: this is currently a silent copy, which can take some time, but there is
- // no clean method to do this in uibase
- if (!copyDir(fileInfo.absoluteFilePath(), newMod->absolutePath(), true)) {
- return;
- }
-
- m_core->refresh();
-
- const auto index = ModInfo::getIndex(name);
- if (priority != -1) {
- m_core->modList()->changeModPriority(index, priority);
- }
-
- scrollToAndSelect(indexModelToView(m_core->modList()->index(index, 0)));
-}
-
-bool ModListView::moveSelection(int key)
-{
- auto rows = selectionModel()->selectedRows();
- const QPersistentModelIndex current(key == Qt::Key_Up ? rows.first() : rows.last());
-
- int offset = key == Qt::Key_Up ? -1 : 1;
- if (m_sortProxy->sortOrder() == Qt::DescendingOrder) {
- offset = -offset;
- }
-
- m_core->modList()->shiftModsPriority(indexViewToModel(rows), offset);
- selectionModel()->setCurrentIndex(current, QItemSelectionModel::NoUpdate);
- scrollTo(current);
-
- return true;
-}
-
-bool ModListView::removeSelection()
-{
- m_actions->removeMods(indexViewToModel(selectionModel()->selectedRows()));
- return true;
-}
-
-bool ModListView::toggleSelectionState()
-{
- if (!selectionModel()->hasSelection()) {
- return true;
- }
- return m_core->modList()->toggleState(indexViewToModel(selectionModel()->selectedRows()));
-}
-
-void ModListView::updateGroupByProxy()
-{
- int groupIndex = ui.groupBy->currentIndex();
- auto* previousModel = m_sortProxy->sourceModel();
-
- QAbstractItemModel* nextModel = m_core->modList();
- if (groupIndex == GroupBy::CATEGORY) {
- nextModel = m_byCategoryProxy;
- }
- else if (groupIndex == GroupBy::NEXUS_ID) {
- nextModel = m_byNexusIdProxy;
- }
- else if (m_core->settings().interface().collapsibleSeparators(m_sortProxy->sortOrder())
- && m_sortProxy->sortColumn() == ModList::COL_PRIORITY) {
- m_byPriorityProxy->setSortOrder(m_sortProxy->sortOrder());
- nextModel = m_byPriorityProxy;
- }
-
- if (nextModel != previousModel) {
-
- if (auto* proxy = dynamic_cast<QAbstractProxyModel*>(nextModel)) {
- proxy->setSourceModel(m_core->modList());
- }
- m_sortProxy->setSourceModel(nextModel);
-
- // reset the source model of the old proxy because we do not want to
- // react to signals
- //
- if (auto* proxy = qobject_cast<QAbstractProxyModel*>(previousModel)) {
- proxy->setSourceModel(nullptr);
- }
-
- // expand items previously expanded
- refreshExpandedItems();
-
- if (hasCollapsibleSeparators()) {
- ui.filterSeparators->setCurrentIndex(ModListSortProxy::SeparatorFilter);
- ui.filterSeparators->setEnabled(false);
- }
- else {
- ui.filterSeparators->setEnabled(true);
- }
-
- }
-}
-
-void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw, Ui::MainWindow* mwui)
-{
- // attributes
- m_core = &core;
- m_filters.reset(new FilterList(mwui, core, factory));
- m_categories = &factory;
- m_actions = new ModListViewActions(core, *m_filters, factory, this, mwui->espList, mw);
- ui = {
- mwui->groupCombo, mwui->activeModsCounter, mwui->modFilterEdit,
- mwui->currentCategoryLabel, mwui->clearFiltersButton, mwui->filtersSeparators,
- mwui->espList
- };
-
-
- connect(m_core, &OrganizerCore::modInstalled, [=](auto&& name) { onModInstalled(name); });
- connect(m_core, &OrganizerCore::profileChanged, this, &ModListView::onProfileChanged);
- connect(core.modList(), &ModList::modPrioritiesChanged, [=](auto&& indices) { onModPrioritiesChanged(indices); });
- connect(core.modList(), &ModList::clearOverwrite, [=] { m_actions->clearOverwrite(); });
- connect(core.modList(), &ModList::modStatesChanged, [=] {
- updateModCount();
- setOverwriteMarkers(selectionModel()->selectedRows());
- });
- connect(core.modList(), &ModList::modelReset, [=] { clearOverwriteMarkers(); });
-
- // proxy for various group by
- m_byPriorityProxy = new ModListByPriorityProxy(core.currentProfile(), core, this);
- m_byCategoryProxy = new QtGroupingProxy(QModelIndex(), ModList::COL_CATEGORY, ModList::GroupingRole, 0, ModList::AggrRole);
- m_byNexusIdProxy = new QtGroupingProxy(QModelIndex(), ModList::COL_MODID, ModList::GroupingRole,
- QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE, ModList::AggrRole);
-
- // we need to store the expanded/collapsed state of all items and restore them 1) when
- // switching proxies, 2) when filtering and 3) when reseting the mod list.
- connect(this, &QTreeView::expanded, [=](const QModelIndex& index) {
- auto it = m_collapsed[m_sortProxy->sourceModel()].find(index.data(Qt::DisplayRole).toString());
- if (it != m_collapsed[m_sortProxy->sourceModel()].end()) {
- m_collapsed[m_sortProxy->sourceModel()].erase(it);
- }
- });
- connect(this, &QTreeView::collapsed, [=](const QModelIndex& index) {
- m_collapsed[m_sortProxy->sourceModel()].insert(index.data(Qt::DisplayRole).toString());
- });
-
- // the top-level proxy
- m_sortProxy = new ModListSortProxy(core.currentProfile(), &core);
- setModel(m_sortProxy);
- connect(m_sortProxy, &ModList::modelReset, [=] { refreshExpandedItems(); });
-
- // update the proxy when changing the sort column/direction and the group
- connect(m_sortProxy, &QAbstractItemModel::layoutAboutToBeChanged, [this](auto&& parents, auto&& hint) {
- if (hint == QAbstractItemModel::VerticalSortHint) {
- updateGroupByProxy();
- }
- });
- connect(ui.groupBy, QOverload<int>::of(&QComboBox::currentIndexChanged), [=](int index) {
- updateGroupByProxy();
- onModFilterActive(m_sortProxy->isFilterActive());
- });
- sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
-
- // inform the mod list about the type of item being dropped at the beginning of a drag
- // and the position of the drop indicator at the end (only for by-priority)
- connect(this, &ModListView::dragEntered, core.modList(), &ModList::onDragEnter);
- connect(this, &ModListView::dropEntered, m_byPriorityProxy, &ModListByPriorityProxy::onDropEnter);
-
- connect(m_sortProxy, &ModListSortProxy::filterInvalidated, this, &ModListView::updateModCount);
-
- connect(header(), &QHeaderView::sortIndicatorChanged, [=](int, Qt::SortOrder) { verticalScrollBar()->repaint(); });
- connect(header(), &QHeaderView::sectionResized, [=](int logicalIndex, int oldSize, int newSize) {
- m_sortProxy->setColumnVisible(logicalIndex, newSize != 0); });
-
- setItemDelegateForColumn(ModList::COL_FLAGS, new ModFlagIconDelegate(this, ModList::COL_FLAGS, 120));
- setItemDelegateForColumn(ModList::COL_CONFLICTFLAGS, new ModConflictIconDelegate(this, ModList::COL_CONFLICTFLAGS, 80));
- setItemDelegateForColumn(ModList::COL_CONTENT, new ModContentIconDelegate(this, ModList::COL_CONTENT, 150));
- setItemDelegateForColumn(ModList::COL_VERSION, new ModListVersionDelegate(this, core.settings()));
-
- if (m_core->settings().geometry().restoreState(header())) {
- // 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 = header()->sectionSize(column);
- header()->resizeSection(column, sectionSize + 1);
- header()->resizeSection(column, sectionSize);
- }
- }
- else {
- // hide these columns by default
- header()->setSectionHidden(ModList::COL_CONTENT, true);
- header()->setSectionHidden(ModList::COL_MODID, true);
- header()->setSectionHidden(ModList::COL_GAME, true);
- header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
- header()->setSectionHidden(ModList::COL_NOTES, true);
-
- // resize mod list to fit content
- for (int i = 0; i < header()->count(); ++i) {
- header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
- }
-
- header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
- }
-
- // prevent the name-column from being hidden
- header()->setSectionHidden(ModList::COL_NAME, false);
-
- // we need QueuedConnection for the download/archive dropped otherwise the
- // installation starts within the drop-event and it's not possible to drag&drop
- // in the manual installer
- connect(m_core->modList(), &ModList::downloadArchiveDropped, this, [=](int row, int priority) {
- m_core->installDownload(row, priority);
- }, Qt::QueuedConnection);
- connect(m_core->modList(), &ModList::externalArchiveDropped, this, [=](const QUrl& url, int priority) {
- setWindowState(Qt::WindowActive);
- m_core->installArchive(url.toLocalFile(), priority, false, nullptr);
- }, Qt::QueuedConnection);
- connect(m_core->modList(), &ModList::externalFolderDropped, this, &ModListView::onExternalFolderDropped);
-
- connect(selectionModel(), &QItemSelectionModel::selectionChanged, [=] { m_refreshMarkersTimer.start(); });
- connect(this, &QTreeView::collapsed, [=] { m_refreshMarkersTimer.start(); });
- connect(this, &QTreeView::expanded, [=] { m_refreshMarkersTimer.start(); });
-
- // filters
- connect(m_sortProxy, &ModListSortProxy::filterActive, this, &ModListView::onModFilterActive);
- connect(m_filters.get(), &FilterList::criteriaChanged, [=](auto&& v) { onFiltersCriteria(v); });
- connect(m_filters.get(), &FilterList::optionsChanged, [=](auto&& mode, auto&& sep) { setFilterOptions(mode, sep); });
- connect(ui.filter, &QLineEdit::textChanged, m_sortProxy, &ModListSortProxy::updateFilter);
- connect(ui.clearFilters, &QPushButton::clicked, [=]() {
- ui.filter->clear();
- m_filters->clearSelection();
- });
- connect(m_sortProxy, &ModListSortProxy::filterInvalidated, [=]() {
- if (hasCollapsibleSeparators()) {
- refreshExpandedItems();
- }
- });
-}
-
-void ModListView::restoreState(const Settings& s)
-{
- s.geometry().restoreState(header());
-
- s.widgets().restoreIndex(ui.groupBy);
- s.widgets().restoreTreeExpandState(this);
-
- m_filters->restoreState(s);
-}
-
-void ModListView::saveState(Settings& s) const
-{
- s.geometry().saveState(header());
-
- s.widgets().saveIndex(ui.groupBy);
- s.widgets().saveTreeExpandState(this);
-
- m_filters->saveState(s);
-}
-
-QRect ModListView::visualRect(const QModelIndex& index) const
-{
- // this shift the visualRect() from QTreeView to match the new actual
- // zone after removing indentation (see the ModListStyledItemDelegate)
- QRect rect = QTreeView::visualRect(index);
- if (hasCollapsibleSeparators()
- && index.column() == 0 && index.isValid()
- && index.parent().isValid()) {
- rect.adjust(-indentation(), 0, 0, 0);
- }
- return rect;
-}
-
-void ModListView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const
-{
- // the branches are the small indicator left to the row (there are none in the default style, and
- // the VS dark style only has background for these)
- //
- // the branches are not shifted left with the visualRect() change and since MO2 uses stylesheet,
- // it is not possible to shift those in the proxy style so we have to shift it here.
- //
- QRect r(rect);
- if (hasCollapsibleSeparators() && index.parent().isValid()) {
- r.adjust(-indentation(), 0, 0 -indentation(), 0);
- }
- QTreeView::drawBranches(painter, r, index);
-}
-
-void ModListView::commitData(QWidget* editor)
-{
- // maintain the selection when changing priority
- if (currentIndex().column() == ModList::COL_PRIORITY) {
- auto [current, selected] = this->selected();
- QTreeView::commitData(editor);
- setSelected(current, selected);
- }
- else {
- QTreeView::commitData(editor);
- }
-}
-
-QModelIndexList ModListView::selectedIndexes() const
-{
- // during drag&drop events, we fake the return value of selectedIndexes()
- // to allow drag&drop of a parent into its children
- //
- // this is only "active" during the actual dragXXXEvent and dropEvent method,
- // not during the whole drag&drop event
- //
- // selectedIndexes() is a protected method from QTreeView which is little
- // used so this should not break anything
- //
- return m_inDragMoveEvent ? QModelIndexList() : QTreeView::selectedIndexes();
-}
-
-void ModListView::onCustomContextMenuRequested(const QPoint& pos)
-{
- try {
- QModelIndex contextIdx = indexViewToModel(indexAt(pos));
-
- if (!contextIdx.isValid()) {
- // no selection
- ModListGlobalContextMenu(*m_core, this).exec(viewport()->mapToGlobal(pos));
- }
- else {
- ModListContextMenu(contextIdx, *m_core, *m_categories, this).exec(viewport()->mapToGlobal(pos));
- }
- }
- catch (const std::exception& e) {
- reportError(tr("Exception: ").arg(e.what()));
- }
- catch (...) {
- reportError(tr("Unknown exception"));
- }
-}
-
-void ModListView::onDoubleClicked(const QModelIndex& index)
-{
- if (!index.isValid()) {
- return;
- }
-
- if (m_core->modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) {
- // don't interpret double click if we only just checked a mod
- return;
- }
-
- bool indexOk = false;
- int modIndex = index.data(ModList::IndexRole).toInt(&indexOk);
-
- if (!indexOk || modIndex < 0 || modIndex >= ModInfo::getNumMods()) {
- return;
- }
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
-
- const auto modifiers = QApplication::queryKeyboardModifiers();
- if (modifiers.testFlag(Qt::ControlModifier)) {
- try {
- shell::Explore(modInfo->absolutePath());
- }
- catch (const std::exception& e) {
- reportError(e.what());
- }
- }
- else if (modifiers.testFlag(Qt::ShiftModifier)) {
- try {
- actions().visitNexusOrWebPage({ indexViewToModel(index) });
- }
- catch (const std::exception& e) {
- reportError(e.what());
- }
- }
- else if (hasCollapsibleSeparators() && modInfo->isSeparator()) {
- setExpanded(index, !isExpanded(index));
- }
- else {
- try {
- auto tab = ModInfoTabIDs::None;
-
- switch (index.column()) {
- case ModList::COL_NOTES: tab = ModInfoTabIDs::Notes; break;
- case ModList::COL_VERSION: tab = ModInfoTabIDs::Nexus; break;
- case ModList::COL_MODID: tab = ModInfoTabIDs::Nexus; break;
- case ModList::COL_GAME: tab = ModInfoTabIDs::Nexus; break;
- case ModList::COL_CATEGORY: tab = ModInfoTabIDs::Categories; break;
- case ModList::COL_CONFLICTFLAGS: tab = ModInfoTabIDs::Conflicts; break;
- }
-
- actions().displayModInformation(modIndex, tab);
- }
- catch (const std::exception& e) {
- reportError(e.what());
- }
- }
-
- // workaround to cancel the editor that might have opened because of
- // selection-click
- closePersistentEditor(index);
-}
-
-void ModListView::clearOverwriteMarkers()
-{
- m_markers.overwrite.clear();
- m_markers.overwritten.clear();
- m_markers.archiveOverwrite.clear();
- m_markers.archiveOverwritten.clear();
- m_markers.archiveLooseOverwrite.clear();
- m_markers.archiveLooseOverwritten.clear();
-}
-
-void ModListView::setOverwriteMarkers(const QModelIndexList& indexes)
-{
- const auto insert = [](auto& dest, const auto& from) {
- dest.insert(from.begin(), from.end());
- };
- clearOverwriteMarkers();
- for (auto& idx : indexes) {
- auto mIndex = idx.data(ModList::IndexRole);
- if (mIndex.isValid()) {
- auto info = ModInfo::getByIndex(mIndex.toInt());
- insert(m_markers.overwrite, info->getModOverwrite());
- insert(m_markers.overwritten, info->getModOverwritten());
- insert(m_markers.archiveOverwrite, info->getModArchiveOverwrite());
- insert(m_markers.archiveOverwritten, info->getModArchiveOverwritten());
- insert(m_markers.archiveLooseOverwrite, info->getModArchiveLooseOverwrite());
- insert(m_markers.archiveLooseOverwritten, info->getModArchiveLooseOverwritten());
- }
- }
- dataChanged(model()->index(0, 0), model()->index(model()->rowCount(), model()->columnCount()));
- verticalScrollBar()->repaint();
-}
-
-void ModListView::refreshMarkersAndPlugins()
-{
- QModelIndexList indexes = selectionModel()->selectedRows();
-
- if (m_core->settings().interface().collapsibleSeparatorsHighlightFrom()) {
- for (auto& idx : selectionModel()->selectedRows()) {
- if (hasCollapsibleSeparators()
- && model()->hasChildren(idx)
- && !isExpanded(idx)) {
- for (int i = 0; i < model()->rowCount(idx); ++i) {
- indexes.append(model()->index(i, idx.column(), idx));
- }
- }
- }
- }
-
- setOverwriteMarkers(indexes);
-
- // highligth plugins
- std::vector<unsigned int> modIndices;
- for (auto& idx : indexes) {
- modIndices.push_back(idx.data(ModList::IndexRole).toInt());
- }
- m_core->pluginList()->highlightPlugins(modIndices, *m_core->directoryStructure());
- ui.pluginList->verticalScrollBar()->repaint();
-}
-
-
-void ModListView::setHighlightedMods(const std::vector<unsigned int>& pluginIndices)
-{
- m_markers.highlight.clear();
- auto& directoryEntry = *m_core->directoryStructure();
- for (auto idx : pluginIndices) {
- QString pluginName = m_core->pluginList()->getName(idx);
-
- const MOShared::FileEntryPtr fileEntry = directoryEntry.findFile(pluginName.toStdWString());
- if (fileEntry.get() != nullptr) {
- QString originName = QString::fromStdWString(directoryEntry.getOriginByID(fileEntry->getOrigin()).getName());
- const auto index = ModInfo::getIndex(originName);
- if (index != UINT_MAX) {
- m_markers.highlight.insert(index);
- }
- }
- }
- dataChanged(model()->index(0, 0), model()->index(model()->rowCount(), model()->columnCount()));
- verticalScrollBar()->repaint();
-}
-
-QColor ModListView::markerColor(const QModelIndex& index) const
-{
- unsigned int modIndex = index.data(ModList::IndexRole).toInt();
- bool highligth = m_markers.highlight.find(modIndex) != m_markers.highlight.end();
- bool overwrite = m_markers.overwrite.find(modIndex) != m_markers.overwrite.end();
- bool archiveOverwrite = m_markers.archiveOverwrite.find(modIndex) != m_markers.archiveOverwrite.end();
- bool archiveLooseOverwrite = m_markers.archiveLooseOverwrite.find(modIndex) != m_markers.archiveLooseOverwrite.end();
- bool overwritten = m_markers.overwritten.find(modIndex) != m_markers.overwritten.end();
- bool archiveOverwritten = m_markers.archiveOverwritten.find(modIndex) != m_markers.archiveOverwritten.end();
- bool archiveLooseOverwritten = m_markers.archiveLooseOverwritten.find(modIndex) != m_markers.archiveLooseOverwritten.end();
-
- if (highligth) {
- return Settings::instance().colors().modlistContainsPlugin();
- }
- else if (overwritten || archiveLooseOverwritten) {
- return Settings::instance().colors().modlistOverwritingLoose();
- }
- else if (overwrite || archiveLooseOverwrite) {
- return Settings::instance().colors().modlistOverwrittenLoose();
- }
- else if (archiveOverwritten) {
- return Settings::instance().colors().modlistOverwritingArchive();
- }
- else if (archiveOverwrite) {
- return Settings::instance().colors().modlistOverwrittenArchive();
- }
-
- // collapsed separator
- auto rowIndex = index.sibling(index.row(), 0);
- if (hasCollapsibleSeparators()
- && m_core->settings().interface().collapsibleSeparatorsHighlightTo()
- && model()->hasChildren(rowIndex) && !isExpanded(rowIndex)) {
-
- std::vector<QColor> colors;
- for (int i = 0; i < model()->rowCount(rowIndex); ++i) {
- auto childColor = markerColor(model()->index(i, index.column(), rowIndex));
- if (childColor.isValid()) {
- colors.push_back(childColor);
- }
- }
-
- if (colors.empty()) {
- return QColor();
- }
-
- int r = 0, g = 0, b = 0, a = 0;
- for (auto& color : colors) {
- r += color.red();
- g += color.green();
- b += color.blue();
- a += color.alpha();
- }
-
- return QColor(r / colors.size(), g / colors.size(), b / colors.size(), a / colors.size());
- }
-
- return QColor();
-}
-
-std::vector<ModInfo::EFlag> ModListView::modFlags(const QModelIndex& index, bool* forceCompact) const
-{
- ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
-
- auto flags = info->getFlags();
- bool compact = false;
- if (info->isSeparator()
- && hasCollapsibleSeparators()
- && m_core->settings().interface().collapsibleSeparatorsIcons(ModList::COL_FLAGS)
- && !isExpanded(index.sibling(index.row(), 0))) {
-
- // combine the child conflicts
- std::set eFlags(flags.begin(), flags.end());
- for (int i = 0; i < model()->rowCount(index); ++i) {
- auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
- auto cFlags = ModInfo::getByIndex(cIndex)->getFlags();
- eFlags.insert(cFlags.begin(), cFlags.end());
- }
- flags = { eFlags.begin(), eFlags.end() };
-
- // force compact because there can be a lots of flags here
- compact = true;
- }
-
- if (forceCompact) {
- *forceCompact = true;
- }
-
- return flags;
-}
-
-std::vector<ModInfo::EConflictFlag> ModListView::conflictFlags(const QModelIndex& index, bool* forceCompact) const
-{
- ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
-
- auto flags = info->getConflictFlags();
- bool compact = false;
- if (info->isSeparator()
- && hasCollapsibleSeparators()
- && m_core->settings().interface().collapsibleSeparatorsIcons(ModList::COL_CONFLICTFLAGS)
- && !isExpanded(index.sibling(index.row(), 0))) {
-
- // combine the child conflicts
- std::set<ModInfo::EConflictFlag> eFlags(flags.begin(), flags.end());
- for (int i = 0; i < model()->rowCount(index); ++i) {
- auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
- auto cFlags = ModInfo::getByIndex(cIndex)->getConflictFlags();
- eFlags.insert(cFlags.begin(), cFlags.end());
- }
- flags = { eFlags.begin(), eFlags.end() };
-
- // force compact because there can be a lots of flags here
- compact = true;
- }
-
- if (forceCompact) {
- *forceCompact = true;
- }
-
- return flags;
-}
-
-std::set<int> ModListView::contents(const QModelIndex& index, bool* includeChildren) const
-{
- auto modIndex = index.data(ModList::IndexRole);
- if (!modIndex.isValid()) {
- return {};
- }
- ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
- auto contents = info->getContents();
- bool children = false;
-
- if (info->isSeparator()
- && hasCollapsibleSeparators()
- && m_core->settings().interface().collapsibleSeparatorsIcons(ModList::COL_CONTENT)
- && !isExpanded(index.sibling(index.row(), 0))) {
-
- // combine the child contents
- std::set eContents(contents.begin(), contents.end());
- for (int i = 0; i < model()->rowCount(index); ++i) {
- auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
- auto cContents = ModInfo::getByIndex(cIndex)->getContents();
- eContents.insert(cContents.begin(), cContents.end());
- }
- contents = { eContents.begin(), eContents.end() };
- children = true;
- }
-
- if (includeChildren) {
- *includeChildren = children;
- }
-
- return contents;
-}
-
-QList<QString> ModListView::contentsIcons(const QModelIndex& index, bool* forceCompact) const
-{
- auto contents = this->contents(index, forceCompact);
- QList<QString> result;
- m_core->modDataContents().forEachContentInOrOut(
- contents,
- [&result](auto const& content) { result.append(content.icon()); },
- [&result](auto const&) { result.append(QString()); });
- return result;
-}
-
-QString ModListView::contentsTooltip(const QModelIndex& index) const
-{
- auto contents = this->contents(index, nullptr);
- if (contents.empty()) {
- return {};
- }
- QString result("<table cellspacing=7>");
- m_core->modDataContents().forEachContentIn(contents, [&result](auto const& content) {
- result.append(QString("<tr><td><img src=\"%1\" width=32/></td>"
- "<td valign=\"middle\">%2</td></tr>")
- .arg(content.icon()).arg(content.name()));
- });
- result.append("</table>");
- return result;
-}
-
-void ModListView::onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
-{
- setFilterCriteria(criteria);
-
- QString label = "?";
-
- if (criteria.empty()) {
- label = "";
- }
- else if (criteria.size() == 1) {
- const auto& c = criteria[0];
-
- if (c.type == ModListSortProxy::TypeContent) {
- const auto* content = m_core->modDataContents().findById(c.id);
- label = content ? content->name() : QString();
- }
- else {
- label = m_categories->getCategoryNameByID(c.id);
- }
-
- if (label.isEmpty()) {
- log::error("category {}:{} not found", c.type, c.id);
- }
- }
- else {
- label = tr("<Multiple>");
- }
-
- ui.currentCategory->setText(label);
-}
-
-void ModListView::dragEnterEvent(QDragEnterEvent* event)
-{
- // this event is used by the modlist to check if we are draggin
- // to a mod (local files) or to a priority (mods, downloads, external
- // files)
- emit dragEntered(event->mimeData());
- QTreeView::dragEnterEvent(event);
-
- // there is no drop event for invalid data since canDropMimeData
- // returns false, so we notify user on drag enter
- ModListDropInfo dropInfo(event->mimeData(), *m_core);
-
- if (dropInfo.isValid() && !dropInfo.isLocalFileDrop()
- && sortColumn() != ModList::COL_PRIORITY) {
- log::warn("Drag&Drop is only supported when sorting by priority.");
- }
-}
-
-void ModListView::dragMoveEvent(QDragMoveEvent* event)
-{
- // this replace the openTimer from QTreeView to prevent
- // auto-collapse of items
- if (autoExpandDelay() >= 0) {
- m_openTimer.start(autoExpandDelay(), this);
- }
-
- // see selectedIndexes()
- m_inDragMoveEvent = true;
- QAbstractItemView::dragMoveEvent(event);
- m_inDragMoveEvent = false;
-}
-
-void ModListView::dropEvent(QDropEvent* event)
-{
- // from Qt source
- QModelIndex index;
- if (viewport()->rect().contains(event->pos())) {
- index = indexAt(event->pos());
- if (!index.isValid() || !visualRect(index).contains(event->pos()))
- index = QModelIndex();
- }
-
- // this event is used by the byPriorityProxy to know if allow
- // dropping mod between a separator and its first mod (there
- // is no way to deduce this except using dropIndicatorPosition())
- emit dropEntered(event->mimeData(), isExpanded(index), static_cast<DropPosition>(dropIndicatorPosition()));
-
- // see selectedIndexes()
- m_inDragMoveEvent = true;
- QTreeView::dropEvent(event);
- m_inDragMoveEvent = false;
-}
-
-void ModListView::timerEvent(QTimerEvent* event)
-{
- // prevent auto-collapse, see dragMoveEvent()
- if (event->timerId() == m_openTimer.timerId()) {
- QPoint pos = viewport()->mapFromGlobal(QCursor::pos());
- if (state() == QAbstractItemView::DraggingState
- && viewport()->rect().contains(pos)) {
- QModelIndex index = indexAt(pos);
- setExpanded(index, !m_core->settings().interface().autoCollapseOnHover() || !isExpanded(index));
- }
- m_openTimer.stop();
- }
- else {
- QTreeView::timerEvent(event);
- }
-}
-
-void ModListView::mousePressEvent(QMouseEvent* event)
-{
- // allow alt+click to select all mods inside a separator
- // when using collapsible separators
- //
- // similar code is also present in mouseReleaseEvent to
- // avoid missing events
-
- // disable edit if Alt is pressed
- auto triggers = editTriggers();
- if (event->modifiers() & Qt::AltModifier) {
- setEditTriggers(NoEditTriggers);
- }
-
- // we call the parent class first so that we can use the actual
- // selection state of the item after
- QTreeView::mousePressEvent(event);
-
- // restore triggers
- setEditTriggers(triggers);
-
- const auto index = indexAt(event->pos());
-
- if (event->isAccepted()
- && hasCollapsibleSeparators()
- && index.isValid() && model()->hasChildren(indexAt(event->pos()))
- && (event->modifiers() & Qt::AltModifier)) {
-
- const auto flag = selectionModel()->isSelected(index) ?
- QItemSelectionModel::Select : QItemSelectionModel::Deselect;
- const QItemSelection selection(
- model()->index(0, index.column(), index),
- model()->index(model()->rowCount(index) - 1, index.column(), index));
- selectionModel()->select(selection, flag | QItemSelectionModel::Rows);
- }
-}
-
-void ModListView::mouseReleaseEvent(QMouseEvent* event)
-{
- // this is a duplicate of mousePressEvent because for some reason
- // the selection is not always triggered in mousePressEvent and only
- // doing it here create a small lag between the selection of the
- // separator and the children
-
- // disable edit if Alt is pressed
- auto triggers = editTriggers();
- if (event->modifiers() & Qt::AltModifier) {
- setEditTriggers(NoEditTriggers);
- }
-
- // we call the parent class first so that we can use the actual
- // selection state of the item after
- QTreeView::mouseReleaseEvent(event);
-
- const auto index = indexAt(event->pos());
-
- // restore triggers
- setEditTriggers(triggers);
-
- if (event->isAccepted()
- && hasCollapsibleSeparators()
- && index.isValid() && model()->hasChildren(indexAt(event->pos()))
- && (event->modifiers() & Qt::AltModifier)) {
-
- const auto flag = selectionModel()->isSelected(index) ?
- QItemSelectionModel::Select : QItemSelectionModel::Deselect;
- const QItemSelection selection(
- model()->index(0, index.column(), index),
- model()->index(model()->rowCount(index) - 1, index.column(), index));
- selectionModel()->select(selection, flag | QItemSelectionModel::Rows);
- }
-}
-
-bool ModListView::event(QEvent* event)
-{
- if (event->type() == QEvent::KeyPress
- && m_core->currentProfile()
- && selectionModel()->hasSelection()) {
- QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
-
- auto index = selectionModel()->currentIndex();
-
- if (keyEvent->modifiers() == Qt::ControlModifier) {
- // ctrl+enter open explorer
- if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
- if (selectionModel()->selectedRows().count() == 1) {
- m_actions->openExplorer({ indexViewToModel(index) });
- return true;
- }
- }
- // ctrl+up/down move selection
- else if (sortColumn() == ModList::COL_PRIORITY
- && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) {
- return moveSelection(keyEvent->key());
- }
- }
- else if (keyEvent->modifiers() == Qt::ShiftModifier) {
- // shift+enter expand
- if ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
- && selectionModel()->selectedRows().count() == 1) {
- if (model()->hasChildren(index)) {
- setExpanded(index, !isExpanded(index));
- }
- else if (index.parent().isValid()) {
- setExpanded(index.parent(), false);
- selectionModel()->select(index.parent(),
- QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
- setCurrentIndex(index.parent());
- }
- }
- }
- else {
- if (keyEvent->key() == Qt::Key_Delete) {
- return removeSelection();
- }
- else if (keyEvent->key() == Qt::Key_Space) {
- return toggleSelectionState();
- }
- }
- return QTreeView::event(event);
- }
- return QTreeView::event(event);
-}
+#include "modlistview.h"
+#include <QUrl>
+#include <QMimeData>
+#include <QProxyStyle>
+
+#include <widgetutility.h>
+
+#include "filesystemutilities.h"
+#include <report.h>
+
+#include "ui_mainwindow.h"
+
+#include "filterlist.h"
+#include "organizercore.h"
+#include "modlist.h"
+#include "modlistsortproxy.h"
+#include "modlistbypriorityproxy.h"
+#include "log.h"
+#include "modflagicondelegate.h"
+#include "modconflicticondelegate.h"
+#include "modcontenticondelegate.h"
+#include "modlistversiondelegate.h"
+#include "modlistviewactions.h"
+#include "modlistdropinfo.h"
+#include "modlistcontextmenu.h"
+#include "genericicondelegate.h"
+#include "copyeventfilter.h"
+#include "shared/fileentry.h"
+#include "shared/directoryentry.h"
+#include "shared/filesorigin.h"
+#include "mainwindow.h"
+#include "modelutils.h"
+
+using namespace MOBase;
+using namespace MOShared;
+
+// delegate to remove indentation for mods when using collapsible
+// separator
+//
+// the delegate works by removing the indentation of the child items
+// before drawing, but unfortunately this normally breaks event
+// handling (e.g. checkbox, edit, etc.), so we also need to override
+// the visualRect() function from the mod list view.
+//
+class ModListStyledItemDelegate : public QStyledItemDelegate
+{
+ ModListView* m_view;
+
+public:
+
+ ModListStyledItemDelegate(ModListView* view) :
+ QStyledItemDelegate(view), m_view(view) { }
+
+ void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
+ {
+ // the parent version always overwrite the background brush, so
+ // we need to save it and restore it
+ auto backgroundColor = option->backgroundBrush.color();
+ QStyledItemDelegate::initStyleOption(option, index);
+
+ if (backgroundColor.isValid()) {
+ option->backgroundBrush = backgroundColor;
+ }
+ }
+
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
+ {
+ QStyleOptionViewItem opt(option);
+
+ // remove items indentaiton when using collapsible separators
+ if (index.column() == 0 && m_view->hasCollapsibleSeparators()) {
+ if (!index.model()->hasChildren(index) && index.parent().isValid()) {
+ auto parentIndex = index.parent().data(ModList::IndexRole).toInt();
+ if (ModInfo::getByIndex(parentIndex)->isSeparator()) {
+ opt.rect.adjust(-m_view->indentation(), 0, 0, 0);
+ }
+ }
+ }
+
+ // compute required color from children, otherwise fallback to the
+ // color from the model, and draw the background here
+ auto color = m_view->markerColor(index);
+ if (!color.isValid()) {
+ color = index.data(Qt::BackgroundRole).value<QColor>();
+ }
+ opt.backgroundBrush = color;
+
+ // we need to find the background color to compute the ideal text color
+ // but the mod list view uses alternate color so we need to find the
+ // right color
+ auto bg = opt.palette.base().color();
+ if (opt.features & QStyleOptionViewItem::Alternate) {
+ bg = opt.palette.alternateBase().color();
+ }
+
+ // compute ideal foreground color for some rows
+ if (color.isValid()) {
+ if (((index.column() == ModList::COL_NAME || index.column() == ModList::COL_PRIORITY)
+ && ModInfo::getByIndex(index.data(ModList::IndexRole).toInt())->isSeparator())
+ || index.column() == ModList::COL_NOTES) {
+
+ // combine the color with the background and then find the "ideal" text color
+ const auto a = color.alpha() / 255.;
+ int r = (1 - a) * bg.red() + a * color.red(),
+ g = (1 - a) * bg.green() + a * color.green(),
+ b = (1 - a) * bg.blue() + a * color.blue();
+ opt.palette.setBrush(QPalette::Text, ColorSettings::idealTextColor(QColor(r, g, b)));
+ }
+ }
+
+ QStyledItemDelegate::paint(painter, opt, index);
+ }
+};
+
+class ModListViewMarkingScrollBar : public ViewMarkingScrollBar {
+ ModListView* m_view;
+public:
+ ModListViewMarkingScrollBar(ModListView* view) :
+ ViewMarkingScrollBar(view, ModList::ScrollMarkRole), m_view(view) { }
+
+
+ QColor color(const QModelIndex& index) const override
+ {
+ auto color = m_view->markerColor(index);
+ if (!color.isValid()) {
+ color = ViewMarkingScrollBar::color(index);
+ }
+ return color;
+ }
+
+};
+
+ModListView::ModListView(QWidget* parent)
+ : QTreeView(parent)
+ , m_core(nullptr)
+ , m_sortProxy(nullptr)
+ , m_byPriorityProxy(nullptr)
+ , m_byCategoryProxy(nullptr)
+ , m_byNexusIdProxy(nullptr)
+ , m_markers{ {}, {}, {}, {}, {}, {} }
+ , m_scrollbar(new ModListViewMarkingScrollBar(this))
+{
+ setVerticalScrollBar(m_scrollbar);
+ MOBase::setCustomizableColumns(this);
+ setAutoExpandDelay(750);
+
+ setItemDelegate(new ModListStyledItemDelegate(this));
+
+ connect(this, &ModListView::doubleClicked, this, &ModListView::onDoubleClicked);
+ connect(this, &ModListView::customContextMenuRequested, this, &ModListView::onCustomContextMenuRequested);
+
+ // the timeout is pretty small because its main purpose is to avoid
+ // refreshing multiple times when calling expandAll() or collapseAll()
+ // which emit a lots of expanded/collapsed signals in a very small
+ // time window
+ m_refreshMarkersTimer.setInterval(50);
+ m_refreshMarkersTimer.setSingleShot(true);
+ connect(&m_refreshMarkersTimer, &QTimer::timeout, [=] { refreshMarkersAndPlugins(); });
+
+ installEventFilter(new CopyEventFilter(this, [=](auto& index) {
+ QVariant mIndex = index.data(ModList::IndexRole);
+ QString name = index.data(Qt::DisplayRole).toString();
+ if (mIndex.isValid() && hasCollapsibleSeparators()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(mIndex.toInt());
+ if (info->isSeparator()) {
+ name = "[" + name + "]";
+ }
+ }
+ else if (model()->hasChildren(index)) {
+ name = "[" + name + "]";
+ }
+ return name;
+ }));
+}
+
+void ModListView::refresh()
+{
+ updateGroupByProxy();
+}
+
+void ModListView::onProfileChanged(Profile* oldProfile, Profile* newProfile)
+{
+ const auto perProfileSeparators =
+ m_core->settings().interface().collapsibleSeparatorsPerProfile();
+
+ // save expanded/collapsed state of separators
+ if (oldProfile && perProfileSeparators) {
+ auto& collapsed = m_collapsed[m_byPriorityProxy];
+ oldProfile->storeSetting("UserInterface", "collapsed_separators", QStringList(collapsed.begin(), collapsed.end()));
+ }
+
+ m_sortProxy->setProfile(newProfile);
+ m_byPriorityProxy->setProfile(newProfile);
+
+ if (newProfile && perProfileSeparators) {
+ auto collapsed = newProfile->setting("UserInterface", "collapsed_separators", QStringList()).toStringList();
+ m_collapsed[m_byPriorityProxy] = { collapsed.begin(), collapsed.end() };
+ }
+}
+
+bool ModListView::hasCollapsibleSeparators() const
+{
+ return groupByMode() == GroupByMode::SEPARATOR;
+}
+
+int ModListView::sortColumn() const
+{
+ return m_sortProxy ? m_sortProxy->sortColumn() : -1;
+}
+
+Qt::SortOrder ModListView::sortOrder() const
+{
+ return m_sortProxy ? m_sortProxy->sortOrder() : Qt::AscendingOrder;
+}
+
+bool ModListView::isFilterActive() const
+{
+ return m_sortProxy && m_sortProxy->isFilterActive();
+}
+
+ModListView::GroupByMode ModListView::groupByMode() const
+{
+ if (m_sortProxy == nullptr) {
+ return GroupByMode::NONE;
+ }
+ else if (m_sortProxy->sourceModel() == m_byPriorityProxy) {
+ return GroupByMode::SEPARATOR;
+ }
+ else if (m_sortProxy->sourceModel() == m_byCategoryProxy) {
+ return GroupByMode::CATEGORY;
+ }
+ else if (m_sortProxy->sourceModel() == m_byNexusIdProxy) {
+ return GroupByMode::NEXUS_ID;
+ }
+ else {
+ return GroupByMode::NONE;
+ }
+}
+
+ModListViewActions& ModListView::actions() const
+{
+ return *m_actions;
+}
+
+std::optional<unsigned int> ModListView::nextMod(unsigned int modIndex) const
+{
+ const QModelIndex start = indexModelToView(m_core->modList()->index(modIndex, 0));
+
+ auto index = start;
+
+ for (;;) {
+ index = nextIndex(index);
+
+ if (index == start || !index.isValid()) {
+ // wrapped around, give up
+ break;
+ }
+
+ modIndex = index.data(ModList::IndexRole).toInt();
+
+ ModInfo::Ptr mod = ModInfo::getByIndex(modIndex);
+
+ // skip overwrite, backups and separators
+ if (mod->isOverwrite() || mod->isBackup() || mod->isSeparator()) {
+ continue;
+ }
+
+ return modIndex;
+ }
+
+ return {};
+}
+
+std::optional<unsigned int> ModListView::prevMod(unsigned int modIndex) const
+{
+ const QModelIndex start = indexModelToView(m_core->modList()->index(modIndex, 0));
+
+ auto index = start;
+
+ for (;;) {
+ index = prevIndex(index);
+
+ if (index == start || !index.isValid()) {
+ // wrapped around, give up
+ break;
+ }
+
+ modIndex = index.data(ModList::IndexRole).toInt();
+
+ // skip overwrite, backups and separators
+ ModInfo::Ptr mod = ModInfo::getByIndex(modIndex);
+ if (mod->isOverwrite() || mod->isBackup() || mod->isSeparator()) {
+ continue;
+ }
+
+ return modIndex;
+ }
+
+ return {};
+}
+
+void ModListView::invalidateFilter()
+{
+ m_sortProxy->invalidate();
+}
+
+void ModListView::setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
+{
+ m_sortProxy->setCriteria(criteria);
+}
+
+void ModListView::setFilterOptions(ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep)
+{
+ m_sortProxy->setOptions(mode, sep);
+}
+
+bool ModListView::isModVisible(unsigned int index) const
+{
+ return m_sortProxy->filterMatchesMod(ModInfo::getByIndex(index), m_core->currentProfile()->modEnabled(index));
+}
+
+bool ModListView::isModVisible(ModInfo::Ptr mod) const
+{
+ return m_sortProxy->filterMatchesMod(mod, m_core->currentProfile()->modEnabled(ModInfo::getIndex(mod->name())));
+}
+
+QModelIndex ModListView::indexModelToView(const QModelIndex& index) const
+{
+ return ::indexModelToView(index, this);
+}
+
+QModelIndexList ModListView::indexModelToView(const QModelIndexList& index) const
+{
+ return ::indexModelToView(index, this);
+}
+
+QModelIndex ModListView::indexViewToModel(const QModelIndex& index) const
+{
+ return ::indexViewToModel(index, m_core->modList());
+}
+
+QModelIndexList ModListView::indexViewToModel(const QModelIndexList& index) const
+{
+ return ::indexViewToModel(index, m_core->modList());
+}
+
+QModelIndex ModListView::nextIndex(const QModelIndex& index) const
+{
+ auto* model = index.model();
+ if (!model) {
+ return {};
+ }
+
+ if (model->rowCount(index) > 0) {
+ return model->index(0, index.column(), index);
+ }
+
+ if (index.parent().isValid()) {
+ if (index.row() + 1 < model->rowCount(index.parent())) {
+ return index.model()->index(index.row() + 1, index.column(), index.parent());
+ }
+ else {
+ return index.model()->index((index.parent().row() + 1) % model->rowCount(index.parent().parent()), index.column(), index.parent().parent());;
+ }
+ }
+ else {
+ return index.model()->index((index.row() + 1) % model->rowCount(index.parent()), index.column(), index.parent());
+ }
+}
+
+QModelIndex ModListView::prevIndex(const QModelIndex& index) const
+{
+ if (index.row() == 0 && index.parent().isValid()) {
+ return index.parent();
+ }
+
+ auto* model = index.model();
+ if (!model) {
+ return {};
+ }
+
+ auto prev = model->index((index.row() - 1) % model->rowCount(index.parent()), index.column(), index.parent());
+
+ if (model->rowCount(prev) > 0) {
+ return model->index(model->rowCount(prev) - 1, index.column(), prev);
+ }
+
+ return prev;
+}
+
+std::pair<QModelIndex, QModelIndexList> ModListView::selected() const
+{
+ return { indexViewToModel(currentIndex()), indexViewToModel(selectionModel()->selectedRows()) };
+}
+
+void ModListView::setSelected(const QModelIndex& current, const QModelIndexList& selected)
+{
+ setCurrentIndex(indexModelToView(current));
+ for (auto idx : selected) {
+ selectionModel()->select(indexModelToView(idx), QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
+}
+
+void ModListView::scrollToAndSelect(const QModelIndex& index)
+{
+ scrollToAndSelect(QModelIndexList{index});
+}
+
+void ModListView::scrollToAndSelect(const QModelIndexList& indexes, const QModelIndex& current)
+{
+ // focus, scroll to and select
+ if (!current.isValid() && indexes.isEmpty()) {
+ return;
+ }
+ scrollTo(current.isValid() ? current : indexes.first());
+ setCurrentIndex(current.isValid() ? current : indexes.first());
+ QItemSelection selection;
+ for (auto& idx : indexes) {
+ selection.select(idx, idx);
+ }
+ selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ QTimer::singleShot(50, [=] { setFocus(); });
+}
+
+void ModListView::refreshExpandedItems()
+{
+ auto* model = m_sortProxy->sourceModel();
+ for (auto i = 0; i < model->rowCount(); ++i) {
+ auto idx = model->index(i, 0);
+ if (!m_collapsed[model].contains(idx.data(Qt::DisplayRole).toString())) {
+ setExpanded(m_sortProxy->mapFromSource(idx), true);
+ }
+ }
+}
+
+void ModListView::onModPrioritiesChanged(const QModelIndexList& indices)
+{
+ // expand separator whose priority has changed and parents
+ for (auto index : indices) {
+ auto idx = indexModelToView(index);
+ if (hasCollapsibleSeparators() && model()->hasChildren(idx)) {
+ setExpanded(idx, true);
+ }
+ if (idx.parent().isValid()) {
+ setExpanded(idx.parent(), true);
+ }
+ }
+
+ setOverwriteMarkers(selectionModel()->selectedRows());
+}
+
+void ModListView::onModInstalled(const QString& modName)
+{
+ unsigned int index = ModInfo::getIndex(modName);
+
+ if (index == UINT_MAX) {
+ return;
+ }
+
+ QModelIndex qIndex = indexModelToView(m_core->modList()->index(index, 0));
+
+ if (hasCollapsibleSeparators() && qIndex.parent().isValid()) {
+ setExpanded(qIndex.parent(), true);
+ }
+
+ scrollToAndSelect(qIndex);
+}
+
+void ModListView::onModFilterActive(bool filterActive)
+{
+ ui.clearFilters->setVisible(filterActive);
+ if (filterActive) {
+ setStyleSheet("QTreeView { border: 2px ridge #f00; }");
+ ui.counter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }");
+ }
+ else if (ui.groupBy->currentIndex() != GroupBy::NONE) {
+ setStyleSheet("QTreeView { border: 2px ridge #337733; }");
+ ui.counter->setStyleSheet("");
+ }
+ else {
+ setStyleSheet("");
+ ui.counter->setStyleSheet("");
+ }
+}
+
+ModListView::ModCounters ModListView::counters() const
+{
+ ModCounters c;
+
+ auto hasFlag = [](std::vector<ModInfo::EFlag> flags, ModInfo::EFlag filter) {
+ return std::find(flags.begin(), flags.end(), filter) != flags.end();
+ };
+
+
+ for (unsigned int index = 0; index < ModInfo::getNumMods(); ++index) {
+ auto info = ModInfo::getByIndex(index);
+ const auto flags = info->getFlags();
+
+ const bool enabled = m_core->currentProfile()->modEnabled(index);
+ const bool visible = m_sortProxy->filterMatchesMod(info, enabled);
+
+ if (info->isBackup()) {
+ c.backup++;
+ if (visible) c.visible.backup++;
+ }
+ else if (info->isForeign()) {
+ c.foreign++;
+ if (visible) c.visible.foreign++;
+ }
+ else if (info->isSeparator()) {
+ c.separator++;
+ if (visible) c.visible.separator++;
+ }
+ else if (!info->isOverwrite()) {
+ c.regular++;
+ if (visible) c.visible.regular++;
+ if (enabled) {
+ c.active++;
+ if (visible) c.visible.active++;
+ }
+ }
+ }
+
+ return c;
+}
+
+void ModListView::updateModCount()
+{
+ const auto c = counters();
+
+ ui.counter->display(c.visible.active);
+ ui.counter->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(c.active)
+ .arg(c.regular)
+ .arg(c.visible.active)
+ .arg(c.visible.regular)
+ .arg(c.foreign)
+ .arg(c.visible.foreign)
+ .arg(c.backup)
+ .arg(c.visible.backup)
+ .arg(c.separator)
+ .arg(c.visible.separator)
+ );
+}
+
+void ModListView::refreshFilters()
+{
+ auto [current, sourceRows] = selected();
+
+ setCurrentIndex(QModelIndex());
+ m_filters->refresh();
+
+ setSelected(current, sourceRows);
+}
+
+void ModListView::onExternalFolderDropped(const QUrl& url, int priority)
+{
+ setWindowState(Qt::WindowActive);
+
+ QFileInfo fileInfo(url.toLocalFile());
+
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+ name.update(fileInfo.fileName(), GUESS_PRESET);
+
+ do {
+ bool ok;
+ name.update(QInputDialog::getText(this, tr("Copy Folder..."),
+ tr("This will copy the content of %1 to a new mod.\n"
+ "Please enter the name:").arg(fileInfo.fileName()), QLineEdit::Normal, name, &ok),
+ GUESS_USER);
+ if (!ok) {
+ return;
+ }
+ } while (name->isEmpty());
+
+ if (m_core->modList()->getMod(name) != nullptr) {
+ reportError(tr("A mod with this name already exists."));
+ return;
+ }
+
+ IModInterface* newMod = m_core->createMod(name);
+ if (!newMod) {
+ return;
+ }
+
+ // TODO: this is currently a silent copy, which can take some time, but there is
+ // no clean method to do this in uibase
+ if (!copyDir(fileInfo.absoluteFilePath(), newMod->absolutePath(), true)) {
+ return;
+ }
+
+ m_core->refresh();
+
+ const auto index = ModInfo::getIndex(name);
+ if (priority != -1) {
+ m_core->modList()->changeModPriority(index, priority);
+ }
+
+ scrollToAndSelect(indexModelToView(m_core->modList()->index(index, 0)));
+}
+
+bool ModListView::moveSelection(int key)
+{
+ auto rows = selectionModel()->selectedRows();
+ const QPersistentModelIndex current(key == Qt::Key_Up ? rows.first() : rows.last());
+
+ int offset = key == Qt::Key_Up ? -1 : 1;
+ if (m_sortProxy->sortOrder() == Qt::DescendingOrder) {
+ offset = -offset;
+ }
+
+ m_core->modList()->shiftModsPriority(indexViewToModel(rows), offset);
+ selectionModel()->setCurrentIndex(current, QItemSelectionModel::NoUpdate);
+ scrollTo(current);
+
+ return true;
+}
+
+bool ModListView::removeSelection()
+{
+ m_actions->removeMods(indexViewToModel(selectionModel()->selectedRows()));
+ return true;
+}
+
+bool ModListView::toggleSelectionState()
+{
+ if (!selectionModel()->hasSelection()) {
+ return true;
+ }
+ return m_core->modList()->toggleState(indexViewToModel(selectionModel()->selectedRows()));
+}
+
+void ModListView::updateGroupByProxy()
+{
+ int groupIndex = ui.groupBy->currentIndex();
+ auto* previousModel = m_sortProxy->sourceModel();
+
+ QAbstractItemModel* nextModel = m_core->modList();
+ if (groupIndex == GroupBy::CATEGORY) {
+ nextModel = m_byCategoryProxy;
+ }
+ else if (groupIndex == GroupBy::NEXUS_ID) {
+ nextModel = m_byNexusIdProxy;
+ }
+ else if (m_core->settings().interface().collapsibleSeparators(m_sortProxy->sortOrder())
+ && m_sortProxy->sortColumn() == ModList::COL_PRIORITY) {
+ m_byPriorityProxy->setSortOrder(m_sortProxy->sortOrder());
+ nextModel = m_byPriorityProxy;
+ }
+
+ if (nextModel != previousModel) {
+
+ if (auto* proxy = dynamic_cast<QAbstractProxyModel*>(nextModel)) {
+ proxy->setSourceModel(m_core->modList());
+ }
+ m_sortProxy->setSourceModel(nextModel);
+
+ // reset the source model of the old proxy because we do not want to
+ // react to signals
+ //
+ if (auto* proxy = qobject_cast<QAbstractProxyModel*>(previousModel)) {
+ proxy->setSourceModel(nullptr);
+ }
+
+ // expand items previously expanded
+ refreshExpandedItems();
+
+ if (hasCollapsibleSeparators()) {
+ ui.filterSeparators->setCurrentIndex(ModListSortProxy::SeparatorFilter);
+ ui.filterSeparators->setEnabled(false);
+ }
+ else {
+ ui.filterSeparators->setEnabled(true);
+ }
+
+ }
+}
+
+void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw, Ui::MainWindow* mwui)
+{
+ // attributes
+ m_core = &core;
+ m_filters.reset(new FilterList(mwui, core, factory));
+ m_categories = &factory;
+ m_actions = new ModListViewActions(core, *m_filters, factory, this, mwui->espList, mw);
+ ui = {
+ mwui->groupCombo, mwui->activeModsCounter, mwui->modFilterEdit,
+ mwui->currentCategoryLabel, mwui->clearFiltersButton, mwui->filtersSeparators,
+ mwui->espList
+ };
+
+
+ connect(m_core, &OrganizerCore::modInstalled, [=](auto&& name) { onModInstalled(name); });
+ connect(m_core, &OrganizerCore::profileChanged, this, &ModListView::onProfileChanged);
+ connect(core.modList(), &ModList::modPrioritiesChanged, [=](auto&& indices) { onModPrioritiesChanged(indices); });
+ connect(core.modList(), &ModList::clearOverwrite, [=] { m_actions->clearOverwrite(); });
+ connect(core.modList(), &ModList::modStatesChanged, [=] {
+ updateModCount();
+ setOverwriteMarkers(selectionModel()->selectedRows());
+ });
+ connect(core.modList(), &ModList::modelReset, [=] { clearOverwriteMarkers(); });
+
+ // proxy for various group by
+ m_byPriorityProxy = new ModListByPriorityProxy(core.currentProfile(), core, this);
+ m_byCategoryProxy = new QtGroupingProxy(QModelIndex(), ModList::COL_CATEGORY, ModList::GroupingRole, 0, ModList::AggrRole);
+ m_byNexusIdProxy = new QtGroupingProxy(QModelIndex(), ModList::COL_MODID, ModList::GroupingRole,
+ QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE, ModList::AggrRole);
+
+ // we need to store the expanded/collapsed state of all items and restore them 1) when
+ // switching proxies, 2) when filtering and 3) when reseting the mod list.
+ connect(this, &QTreeView::expanded, [=](const QModelIndex& index) {
+ auto it = m_collapsed[m_sortProxy->sourceModel()].find(index.data(Qt::DisplayRole).toString());
+ if (it != m_collapsed[m_sortProxy->sourceModel()].end()) {
+ m_collapsed[m_sortProxy->sourceModel()].erase(it);
+ }
+ });
+ connect(this, &QTreeView::collapsed, [=](const QModelIndex& index) {
+ m_collapsed[m_sortProxy->sourceModel()].insert(index.data(Qt::DisplayRole).toString());
+ });
+
+ // the top-level proxy
+ m_sortProxy = new ModListSortProxy(core.currentProfile(), &core);
+ setModel(m_sortProxy);
+ connect(m_sortProxy, &ModList::modelReset, [=] { refreshExpandedItems(); });
+
+ // update the proxy when changing the sort column/direction and the group
+ connect(m_sortProxy, &QAbstractItemModel::layoutAboutToBeChanged, [this](auto&& parents, auto&& hint) {
+ if (hint == QAbstractItemModel::VerticalSortHint) {
+ updateGroupByProxy();
+ }
+ });
+ connect(ui.groupBy, QOverload<int>::of(&QComboBox::currentIndexChanged), [=](int index) {
+ updateGroupByProxy();
+ onModFilterActive(m_sortProxy->isFilterActive());
+ });
+ sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
+
+ // inform the mod list about the type of item being dropped at the beginning of a drag
+ // and the position of the drop indicator at the end (only for by-priority)
+ connect(this, &ModListView::dragEntered, core.modList(), &ModList::onDragEnter);
+ connect(this, &ModListView::dropEntered, m_byPriorityProxy, &ModListByPriorityProxy::onDropEnter);
+
+ connect(m_sortProxy, &ModListSortProxy::filterInvalidated, this, &ModListView::updateModCount);
+
+ connect(header(), &QHeaderView::sortIndicatorChanged, [=](int, Qt::SortOrder) { verticalScrollBar()->repaint(); });
+ connect(header(), &QHeaderView::sectionResized, [=](int logicalIndex, int oldSize, int newSize) {
+ m_sortProxy->setColumnVisible(logicalIndex, newSize != 0); });
+
+ setItemDelegateForColumn(ModList::COL_FLAGS, new ModFlagIconDelegate(this, ModList::COL_FLAGS, 120));
+ setItemDelegateForColumn(ModList::COL_CONFLICTFLAGS, new ModConflictIconDelegate(this, ModList::COL_CONFLICTFLAGS, 80));
+ setItemDelegateForColumn(ModList::COL_CONTENT, new ModContentIconDelegate(this, ModList::COL_CONTENT, 150));
+ setItemDelegateForColumn(ModList::COL_VERSION, new ModListVersionDelegate(this, core.settings()));
+
+ if (m_core->settings().geometry().restoreState(header())) {
+ // 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 = header()->sectionSize(column);
+ header()->resizeSection(column, sectionSize + 1);
+ header()->resizeSection(column, sectionSize);
+ }
+ }
+ else {
+ // hide these columns by default
+ header()->setSectionHidden(ModList::COL_CONTENT, true);
+ header()->setSectionHidden(ModList::COL_MODID, true);
+ header()->setSectionHidden(ModList::COL_GAME, true);
+ header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
+ header()->setSectionHidden(ModList::COL_NOTES, true);
+
+ // resize mod list to fit content
+ for (int i = 0; i < header()->count(); ++i) {
+ header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+
+ header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
+ }
+
+ // prevent the name-column from being hidden
+ header()->setSectionHidden(ModList::COL_NAME, false);
+
+ // we need QueuedConnection for the download/archive dropped otherwise the
+ // installation starts within the drop-event and it's not possible to drag&drop
+ // in the manual installer
+ connect(m_core->modList(), &ModList::downloadArchiveDropped, this, [=](int row, int priority) {
+ m_core->installDownload(row, priority);
+ }, Qt::QueuedConnection);
+ connect(m_core->modList(), &ModList::externalArchiveDropped, this, [=](const QUrl& url, int priority) {
+ setWindowState(Qt::WindowActive);
+ m_core->installArchive(url.toLocalFile(), priority, false, nullptr);
+ }, Qt::QueuedConnection);
+ connect(m_core->modList(), &ModList::externalFolderDropped, this, &ModListView::onExternalFolderDropped);
+
+ connect(selectionModel(), &QItemSelectionModel::selectionChanged, [=] { m_refreshMarkersTimer.start(); });
+ connect(this, &QTreeView::collapsed, [=] { m_refreshMarkersTimer.start(); });
+ connect(this, &QTreeView::expanded, [=] { m_refreshMarkersTimer.start(); });
+
+ // filters
+ connect(m_sortProxy, &ModListSortProxy::filterActive, this, &ModListView::onModFilterActive);
+ connect(m_filters.get(), &FilterList::criteriaChanged, [=](auto&& v) { onFiltersCriteria(v); });
+ connect(m_filters.get(), &FilterList::optionsChanged, [=](auto&& mode, auto&& sep) { setFilterOptions(mode, sep); });
+ connect(ui.filter, &QLineEdit::textChanged, m_sortProxy, &ModListSortProxy::updateFilter);
+ connect(ui.clearFilters, &QPushButton::clicked, [=]() {
+ ui.filter->clear();
+ m_filters->clearSelection();
+ });
+ connect(m_sortProxy, &ModListSortProxy::filterInvalidated, [=]() {
+ if (hasCollapsibleSeparators()) {
+ refreshExpandedItems();
+ }
+ });
+}
+
+void ModListView::restoreState(const Settings& s)
+{
+ s.geometry().restoreState(header());
+
+ s.widgets().restoreIndex(ui.groupBy);
+ s.widgets().restoreTreeExpandState(this);
+
+ m_filters->restoreState(s);
+}
+
+void ModListView::saveState(Settings& s) const
+{
+ s.geometry().saveState(header());
+
+ s.widgets().saveIndex(ui.groupBy);
+ s.widgets().saveTreeExpandState(this);
+
+ m_filters->saveState(s);
+}
+
+QRect ModListView::visualRect(const QModelIndex& index) const
+{
+ // this shift the visualRect() from QTreeView to match the new actual
+ // zone after removing indentation (see the ModListStyledItemDelegate)
+ QRect rect = QTreeView::visualRect(index);
+ if (hasCollapsibleSeparators()
+ && index.column() == 0 && index.isValid()
+ && index.parent().isValid()) {
+ rect.adjust(-indentation(), 0, 0, 0);
+ }
+ return rect;
+}
+
+void ModListView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const
+{
+ // the branches are the small indicator left to the row (there are none in the default style, and
+ // the VS dark style only has background for these)
+ //
+ // the branches are not shifted left with the visualRect() change and since MO2 uses stylesheet,
+ // it is not possible to shift those in the proxy style so we have to shift it here.
+ //
+ QRect r(rect);
+ if (hasCollapsibleSeparators() && index.parent().isValid()) {
+ r.adjust(-indentation(), 0, 0 -indentation(), 0);
+ }
+ QTreeView::drawBranches(painter, r, index);
+}
+
+void ModListView::commitData(QWidget* editor)
+{
+ // maintain the selection when changing priority
+ if (currentIndex().column() == ModList::COL_PRIORITY) {
+ auto [current, selected] = this->selected();
+ QTreeView::commitData(editor);
+ setSelected(current, selected);
+ }
+ else {
+ QTreeView::commitData(editor);
+ }
+}
+
+QModelIndexList ModListView::selectedIndexes() const
+{
+ // during drag&drop events, we fake the return value of selectedIndexes()
+ // to allow drag&drop of a parent into its children
+ //
+ // this is only "active" during the actual dragXXXEvent and dropEvent method,
+ // not during the whole drag&drop event
+ //
+ // selectedIndexes() is a protected method from QTreeView which is little
+ // used so this should not break anything
+ //
+ return m_inDragMoveEvent ? QModelIndexList() : QTreeView::selectedIndexes();
+}
+
+void ModListView::onCustomContextMenuRequested(const QPoint& pos)
+{
+ try {
+ QModelIndex contextIdx = indexViewToModel(indexAt(pos));
+
+ if (!contextIdx.isValid()) {
+ // no selection
+ ModListGlobalContextMenu(*m_core, this).exec(viewport()->mapToGlobal(pos));
+ }
+ else {
+ ModListContextMenu(contextIdx, *m_core, *m_categories, this).exec(viewport()->mapToGlobal(pos));
+ }
+ }
+ catch (const std::exception& e) {
+ reportError(tr("Exception: ").arg(e.what()));
+ }
+ catch (...) {
+ reportError(tr("Unknown exception"));
+ }
+}
+
+void ModListView::onDoubleClicked(const QModelIndex& index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ if (m_core->modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) {
+ // don't interpret double click if we only just checked a mod
+ return;
+ }
+
+ bool indexOk = false;
+ int modIndex = index.data(ModList::IndexRole).toInt(&indexOk);
+
+ if (!indexOk || modIndex < 0 || modIndex >= ModInfo::getNumMods()) {
+ return;
+ }
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+
+ const auto modifiers = QApplication::queryKeyboardModifiers();
+ if (modifiers.testFlag(Qt::ControlModifier)) {
+ try {
+ shell::Explore(modInfo->absolutePath());
+ }
+ catch (const std::exception& e) {
+ reportError(e.what());
+ }
+ }
+ else if (modifiers.testFlag(Qt::ShiftModifier)) {
+ try {
+ actions().visitNexusOrWebPage({ indexViewToModel(index) });
+ }
+ catch (const std::exception& e) {
+ reportError(e.what());
+ }
+ }
+ else if (hasCollapsibleSeparators() && modInfo->isSeparator()) {
+ setExpanded(index, !isExpanded(index));
+ }
+ else {
+ try {
+ auto tab = ModInfoTabIDs::None;
+
+ switch (index.column()) {
+ case ModList::COL_NOTES: tab = ModInfoTabIDs::Notes; break;
+ case ModList::COL_VERSION: tab = ModInfoTabIDs::Nexus; break;
+ case ModList::COL_MODID: tab = ModInfoTabIDs::Nexus; break;
+ case ModList::COL_GAME: tab = ModInfoTabIDs::Nexus; break;
+ case ModList::COL_CATEGORY: tab = ModInfoTabIDs::Categories; break;
+ case ModList::COL_CONFLICTFLAGS: tab = ModInfoTabIDs::Conflicts; break;
+ }
+
+ actions().displayModInformation(modIndex, tab);
+ }
+ catch (const std::exception& e) {
+ reportError(e.what());
+ }
+ }
+
+ // workaround to cancel the editor that might have opened because of
+ // selection-click
+ closePersistentEditor(index);
+}
+
+void ModListView::clearOverwriteMarkers()
+{
+ m_markers.overwrite.clear();
+ m_markers.overwritten.clear();
+ m_markers.archiveOverwrite.clear();
+ m_markers.archiveOverwritten.clear();
+ m_markers.archiveLooseOverwrite.clear();
+ m_markers.archiveLooseOverwritten.clear();
+}
+
+void ModListView::setOverwriteMarkers(const QModelIndexList& indexes)
+{
+ const auto insert = [](auto& dest, const auto& from) {
+ dest.insert(from.begin(), from.end());
+ };
+ clearOverwriteMarkers();
+ for (auto& idx : indexes) {
+ auto mIndex = idx.data(ModList::IndexRole);
+ if (mIndex.isValid()) {
+ auto info = ModInfo::getByIndex(mIndex.toInt());
+ insert(m_markers.overwrite, info->getModOverwrite());
+ insert(m_markers.overwritten, info->getModOverwritten());
+ insert(m_markers.archiveOverwrite, info->getModArchiveOverwrite());
+ insert(m_markers.archiveOverwritten, info->getModArchiveOverwritten());
+ insert(m_markers.archiveLooseOverwrite, info->getModArchiveLooseOverwrite());
+ insert(m_markers.archiveLooseOverwritten, info->getModArchiveLooseOverwritten());
+ }
+ }
+ dataChanged(model()->index(0, 0), model()->index(model()->rowCount(), model()->columnCount()));
+ verticalScrollBar()->repaint();
+}
+
+void ModListView::refreshMarkersAndPlugins()
+{
+ QModelIndexList indexes = selectionModel()->selectedRows();
+
+ if (m_core->settings().interface().collapsibleSeparatorsHighlightFrom()) {
+ for (auto& idx : selectionModel()->selectedRows()) {
+ if (hasCollapsibleSeparators()
+ && model()->hasChildren(idx)
+ && !isExpanded(idx)) {
+ for (int i = 0; i < model()->rowCount(idx); ++i) {
+ indexes.append(model()->index(i, idx.column(), idx));
+ }
+ }
+ }
+ }
+
+ setOverwriteMarkers(indexes);
+
+ // highligth plugins
+ std::vector<unsigned int> modIndices;
+ for (auto& idx : indexes) {
+ modIndices.push_back(idx.data(ModList::IndexRole).toInt());
+ }
+ m_core->pluginList()->highlightPlugins(modIndices, *m_core->directoryStructure());
+ ui.pluginList->verticalScrollBar()->repaint();
+}
+
+
+void ModListView::setHighlightedMods(const std::vector<unsigned int>& pluginIndices)
+{
+ m_markers.highlight.clear();
+ auto& directoryEntry = *m_core->directoryStructure();
+ for (auto idx : pluginIndices) {
+ QString pluginName = m_core->pluginList()->getName(idx);
+
+ const MOShared::FileEntryPtr fileEntry = directoryEntry.findFile(pluginName.toStdWString());
+ if (fileEntry.get() != nullptr) {
+ QString originName = QString::fromStdWString(directoryEntry.getOriginByID(fileEntry->getOrigin()).getName());
+ const auto index = ModInfo::getIndex(originName);
+ if (index != UINT_MAX) {
+ m_markers.highlight.insert(index);
+ }
+ }
+ }
+ dataChanged(model()->index(0, 0), model()->index(model()->rowCount(), model()->columnCount()));
+ verticalScrollBar()->repaint();
+}
+
+QColor ModListView::markerColor(const QModelIndex& index) const
+{
+ unsigned int modIndex = index.data(ModList::IndexRole).toInt();
+ bool highligth = m_markers.highlight.find(modIndex) != m_markers.highlight.end();
+ bool overwrite = m_markers.overwrite.find(modIndex) != m_markers.overwrite.end();
+ bool archiveOverwrite = m_markers.archiveOverwrite.find(modIndex) != m_markers.archiveOverwrite.end();
+ bool archiveLooseOverwrite = m_markers.archiveLooseOverwrite.find(modIndex) != m_markers.archiveLooseOverwrite.end();
+ bool overwritten = m_markers.overwritten.find(modIndex) != m_markers.overwritten.end();
+ bool archiveOverwritten = m_markers.archiveOverwritten.find(modIndex) != m_markers.archiveOverwritten.end();
+ bool archiveLooseOverwritten = m_markers.archiveLooseOverwritten.find(modIndex) != m_markers.archiveLooseOverwritten.end();
+
+ if (highligth) {
+ return Settings::instance().colors().modlistContainsPlugin();
+ }
+ else if (overwritten || archiveLooseOverwritten) {
+ return Settings::instance().colors().modlistOverwritingLoose();
+ }
+ else if (overwrite || archiveLooseOverwrite) {
+ return Settings::instance().colors().modlistOverwrittenLoose();
+ }
+ else if (archiveOverwritten) {
+ return Settings::instance().colors().modlistOverwritingArchive();
+ }
+ else if (archiveOverwrite) {
+ return Settings::instance().colors().modlistOverwrittenArchive();
+ }
+
+ // collapsed separator
+ auto rowIndex = index.sibling(index.row(), 0);
+ if (hasCollapsibleSeparators()
+ && m_core->settings().interface().collapsibleSeparatorsHighlightTo()
+ && model()->hasChildren(rowIndex) && !isExpanded(rowIndex)) {
+
+ std::vector<QColor> colors;
+ for (int i = 0; i < model()->rowCount(rowIndex); ++i) {
+ auto childColor = markerColor(model()->index(i, index.column(), rowIndex));
+ if (childColor.isValid()) {
+ colors.push_back(childColor);
+ }
+ }
+
+ if (colors.empty()) {
+ return QColor();
+ }
+
+ int r = 0, g = 0, b = 0, a = 0;
+ for (auto& color : colors) {
+ r += color.red();
+ g += color.green();
+ b += color.blue();
+ a += color.alpha();
+ }
+
+ return QColor(r / colors.size(), g / colors.size(), b / colors.size(), a / colors.size());
+ }
+
+ return QColor();
+}
+
+std::vector<ModInfo::EFlag> ModListView::modFlags(const QModelIndex& index, bool* forceCompact) const
+{
+ ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+
+ auto flags = info->getFlags();
+ bool compact = false;
+ if (info->isSeparator()
+ && hasCollapsibleSeparators()
+ && m_core->settings().interface().collapsibleSeparatorsIcons(ModList::COL_FLAGS)
+ && !isExpanded(index.sibling(index.row(), 0))) {
+
+ // combine the child conflicts
+ std::set eFlags(flags.begin(), flags.end());
+ for (int i = 0; i < model()->rowCount(index); ++i) {
+ auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
+ auto cFlags = ModInfo::getByIndex(cIndex)->getFlags();
+ eFlags.insert(cFlags.begin(), cFlags.end());
+ }
+ flags = { eFlags.begin(), eFlags.end() };
+
+ // force compact because there can be a lots of flags here
+ compact = true;
+ }
+
+ if (forceCompact) {
+ *forceCompact = true;
+ }
+
+ return flags;
+}
+
+std::vector<ModInfo::EConflictFlag> ModListView::conflictFlags(const QModelIndex& index, bool* forceCompact) const
+{
+ ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+
+ auto flags = info->getConflictFlags();
+ bool compact = false;
+ if (info->isSeparator()
+ && hasCollapsibleSeparators()
+ && m_core->settings().interface().collapsibleSeparatorsIcons(ModList::COL_CONFLICTFLAGS)
+ && !isExpanded(index.sibling(index.row(), 0))) {
+
+ // combine the child conflicts
+ std::set<ModInfo::EConflictFlag> eFlags(flags.begin(), flags.end());
+ for (int i = 0; i < model()->rowCount(index); ++i) {
+ auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
+ auto cFlags = ModInfo::getByIndex(cIndex)->getConflictFlags();
+ eFlags.insert(cFlags.begin(), cFlags.end());
+ }
+ flags = { eFlags.begin(), eFlags.end() };
+
+ // force compact because there can be a lots of flags here
+ compact = true;
+ }
+
+ if (forceCompact) {
+ *forceCompact = true;
+ }
+
+ return flags;
+}
+
+std::set<int> ModListView::contents(const QModelIndex& index, bool* includeChildren) const
+{
+ auto modIndex = index.data(ModList::IndexRole);
+ if (!modIndex.isValid()) {
+ return {};
+ }
+ ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+ auto contents = info->getContents();
+ bool children = false;
+
+ if (info->isSeparator()
+ && hasCollapsibleSeparators()
+ && m_core->settings().interface().collapsibleSeparatorsIcons(ModList::COL_CONTENT)
+ && !isExpanded(index.sibling(index.row(), 0))) {
+
+ // combine the child contents
+ std::set eContents(contents.begin(), contents.end());
+ for (int i = 0; i < model()->rowCount(index); ++i) {
+ auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
+ auto cContents = ModInfo::getByIndex(cIndex)->getContents();
+ eContents.insert(cContents.begin(), cContents.end());
+ }
+ contents = { eContents.begin(), eContents.end() };
+ children = true;
+ }
+
+ if (includeChildren) {
+ *includeChildren = children;
+ }
+
+ return contents;
+}
+
+QList<QString> ModListView::contentsIcons(const QModelIndex& index, bool* forceCompact) const
+{
+ auto contents = this->contents(index, forceCompact);
+ QList<QString> result;
+ m_core->modDataContents().forEachContentInOrOut(
+ contents,
+ [&result](auto const& content) { result.append(content.icon()); },
+ [&result](auto const&) { result.append(QString()); });
+ return result;
+}
+
+QString ModListView::contentsTooltip(const QModelIndex& index) const
+{
+ auto contents = this->contents(index, nullptr);
+ if (contents.empty()) {
+ return {};
+ }
+ QString result("<table cellspacing=7>");
+ m_core->modDataContents().forEachContentIn(contents, [&result](auto const& content) {
+ result.append(QString("<tr><td><img src=\"%1\" width=32/></td>"
+ "<td valign=\"middle\">%2</td></tr>")
+ .arg(content.icon()).arg(content.name()));
+ });
+ result.append("</table>");
+ return result;
+}
+
+void ModListView::onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
+{
+ setFilterCriteria(criteria);
+
+ QString label = "?";
+
+ if (criteria.empty()) {
+ label = "";
+ }
+ else if (criteria.size() == 1) {
+ const auto& c = criteria[0];
+
+ if (c.type == ModListSortProxy::TypeContent) {
+ const auto* content = m_core->modDataContents().findById(c.id);
+ label = content ? content->name() : QString();
+ }
+ else {
+ label = m_categories->getCategoryNameByID(c.id);
+ }
+
+ if (label.isEmpty()) {
+ log::error("category {}:{} not found", c.type, c.id);
+ }
+ }
+ else {
+ label = tr("<Multiple>");
+ }
+
+ ui.currentCategory->setText(label);
+}
+
+void ModListView::dragEnterEvent(QDragEnterEvent* event)
+{
+ // this event is used by the modlist to check if we are draggin
+ // to a mod (local files) or to a priority (mods, downloads, external
+ // files)
+ emit dragEntered(event->mimeData());
+ QTreeView::dragEnterEvent(event);
+
+ // there is no drop event for invalid data since canDropMimeData
+ // returns false, so we notify user on drag enter
+ ModListDropInfo dropInfo(event->mimeData(), *m_core);
+
+ if (dropInfo.isValid() && !dropInfo.isLocalFileDrop()
+ && sortColumn() != ModList::COL_PRIORITY) {
+ log::warn("Drag&Drop is only supported when sorting by priority.");
+ }
+}
+
+void ModListView::dragMoveEvent(QDragMoveEvent* event)
+{
+ // this replace the openTimer from QTreeView to prevent
+ // auto-collapse of items
+ if (autoExpandDelay() >= 0) {
+ m_openTimer.start(autoExpandDelay(), this);
+ }
+
+ // see selectedIndexes()
+ m_inDragMoveEvent = true;
+ QAbstractItemView::dragMoveEvent(event);
+ m_inDragMoveEvent = false;
+}
+
+void ModListView::dropEvent(QDropEvent* event)
+{
+ // from Qt source
+ QModelIndex index;
+ if (viewport()->rect().contains(event->pos())) {
+ index = indexAt(event->pos());
+ if (!index.isValid() || !visualRect(index).contains(event->pos()))
+ index = QModelIndex();
+ }
+
+ // this event is used by the byPriorityProxy to know if allow
+ // dropping mod between a separator and its first mod (there
+ // is no way to deduce this except using dropIndicatorPosition())
+ emit dropEntered(event->mimeData(), isExpanded(index), static_cast<DropPosition>(dropIndicatorPosition()));
+
+ // see selectedIndexes()
+ m_inDragMoveEvent = true;
+ QTreeView::dropEvent(event);
+ m_inDragMoveEvent = false;
+}
+
+void ModListView::timerEvent(QTimerEvent* event)
+{
+ // prevent auto-collapse, see dragMoveEvent()
+ if (event->timerId() == m_openTimer.timerId()) {
+ QPoint pos = viewport()->mapFromGlobal(QCursor::pos());
+ if (state() == QAbstractItemView::DraggingState
+ && viewport()->rect().contains(pos)) {
+ QModelIndex index = indexAt(pos);
+ setExpanded(index, !m_core->settings().interface().autoCollapseOnHover() || !isExpanded(index));
+ }
+ m_openTimer.stop();
+ }
+ else {
+ QTreeView::timerEvent(event);
+ }
+}
+
+void ModListView::mousePressEvent(QMouseEvent* event)
+{
+ // allow alt+click to select all mods inside a separator
+ // when using collapsible separators
+ //
+ // similar code is also present in mouseReleaseEvent to
+ // avoid missing events
+
+ // disable edit if Alt is pressed
+ auto triggers = editTriggers();
+ if (event->modifiers() & Qt::AltModifier) {
+ setEditTriggers(NoEditTriggers);
+ }
+
+ // we call the parent class first so that we can use the actual
+ // selection state of the item after
+ QTreeView::mousePressEvent(event);
+
+ // restore triggers
+ setEditTriggers(triggers);
+
+ const auto index = indexAt(event->pos());
+
+ if (event->isAccepted()
+ && hasCollapsibleSeparators()
+ && index.isValid() && model()->hasChildren(indexAt(event->pos()))
+ && (event->modifiers() & Qt::AltModifier)) {
+
+ const auto flag = selectionModel()->isSelected(index) ?
+ QItemSelectionModel::Select : QItemSelectionModel::Deselect;
+ const QItemSelection selection(
+ model()->index(0, index.column(), index),
+ model()->index(model()->rowCount(index) - 1, index.column(), index));
+ selectionModel()->select(selection, flag | QItemSelectionModel::Rows);
+ }
+}
+
+void ModListView::mouseReleaseEvent(QMouseEvent* event)
+{
+ // this is a duplicate of mousePressEvent because for some reason
+ // the selection is not always triggered in mousePressEvent and only
+ // doing it here create a small lag between the selection of the
+ // separator and the children
+
+ // disable edit if Alt is pressed
+ auto triggers = editTriggers();
+ if (event->modifiers() & Qt::AltModifier) {
+ setEditTriggers(NoEditTriggers);
+ }
+
+ // we call the parent class first so that we can use the actual
+ // selection state of the item after
+ QTreeView::mouseReleaseEvent(event);
+
+ const auto index = indexAt(event->pos());
+
+ // restore triggers
+ setEditTriggers(triggers);
+
+ if (event->isAccepted()
+ && hasCollapsibleSeparators()
+ && index.isValid() && model()->hasChildren(indexAt(event->pos()))
+ && (event->modifiers() & Qt::AltModifier)) {
+
+ const auto flag = selectionModel()->isSelected(index) ?
+ QItemSelectionModel::Select : QItemSelectionModel::Deselect;
+ const QItemSelection selection(
+ model()->index(0, index.column(), index),
+ model()->index(model()->rowCount(index) - 1, index.column(), index));
+ selectionModel()->select(selection, flag | QItemSelectionModel::Rows);
+ }
+}
+
+bool ModListView::event(QEvent* event)
+{
+ if (event->type() == QEvent::KeyPress
+ && m_core->currentProfile()
+ && selectionModel()->hasSelection()) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+
+ auto index = selectionModel()->currentIndex();
+
+ if (keyEvent->modifiers() == Qt::ControlModifier) {
+ // ctrl+enter open explorer
+ if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
+ if (selectionModel()->selectedRows().count() == 1) {
+ m_actions->openExplorer({ indexViewToModel(index) });
+ return true;
+ }
+ }
+ // ctrl+up/down move selection
+ else if (sortColumn() == ModList::COL_PRIORITY
+ && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) {
+ return moveSelection(keyEvent->key());
+ }
+ }
+ else if (keyEvent->modifiers() == Qt::ShiftModifier) {
+ // shift+enter expand
+ if ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
+ && selectionModel()->selectedRows().count() == 1) {
+ if (model()->hasChildren(index)) {
+ setExpanded(index, !isExpanded(index));
+ }
+ else if (index.parent().isValid()) {
+ setExpanded(index.parent(), false);
+ selectionModel()->select(index.parent(),
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ setCurrentIndex(index.parent());
+ }
+ }
+ }
+ else {
+ if (keyEvent->key() == Qt::Key_Delete) {
+ return removeSelection();
+ }
+ else if (keyEvent->key() == Qt::Key_Space) {
+ return toggleSelectionState();
+ }
+ }
+ return QTreeView::event(event);
+ }
+ return QTreeView::event(event);
+}
diff --git a/src/modlistview.h b/src/modlistview.h
index a306c955..aaef97af 100644
--- a/src/modlistview.h
+++ b/src/modlistview.h
@@ -1,344 +1,344 @@
-#ifndef MODLISTVIEW_H
-#define MODLISTVIEW_H
-
-#include <map>
-#include <set>
-#include <vector>
-
-#include <QLabel>
-#include <QTreeView>
-#include <QDragEnterEvent>
-#include <QLCDNumber>
-
-#include "qtgroupingproxy.h"
-#include "viewmarkingscrollbar.h"
-#include "modlistsortproxy.h"
-
-namespace Ui { class MainWindow; }
-
-class CategoryFactory;
-class FilterList;
-class OrganizerCore;
-class MainWindow;
-class Profile;
-class ModListByPriorityProxy;
-class ModListViewActions;
-class PluginListView;
-
-class ModListView : public QTreeView
-{
- Q_OBJECT
-
-public:
-
- // this is a public version of DropIndicatorPosition
- enum DropPosition {
- OnItem = DropIndicatorPosition::OnItem,
- AboveItem = DropIndicatorPosition::AboveItem,
- BelowItem = DropIndicatorPosition::BelowItem,
- OnViewport = DropIndicatorPosition::OnViewport
- };
-
- // indiucate the groupby mode
- enum class GroupByMode {
- NONE,
- SEPARATOR,
- CATEGORY,
- NEXUS_ID
- };
-
-public:
- explicit ModListView(QWidget* parent = 0);
-
- void setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw, Ui::MainWindow* mwui);
-
- // restore/save the state between session
- //
- void restoreState(const Settings& s);
- void saveState(Settings& s) const;
-
- // check if collapsible separators are currently used
- //
- bool hasCollapsibleSeparators() const;
-
- // the column/order by which the mod list is currently sorted
- //
- int sortColumn() const;
- Qt::SortOrder sortOrder() const;
-
- // check if a filter is currently active
- //
- bool isFilterActive() const;
-
- // the current group mode
- //
- GroupByMode groupByMode() const;
-
- // retrieve the actions from the view
- //
- ModListViewActions& actions() const;
-
- // retrieve the next/previous mod in the current view, the given index
- // should be a mod index (not a model row)
- //
- std::optional<unsigned int> nextMod(unsigned int index) const;
- std::optional<unsigned int> prevMod(unsigned int index) const;
-
- // check if the given mod is visible, i.e. not filtered (returns true
- // for collapsed mods)
- //
- bool isModVisible(unsigned int index) const;
- bool isModVisible(ModInfo::Ptr mod) const;
-
- // focus the view, select the given index and scroll to it
- //
- void scrollToAndSelect(const QModelIndex& index);
- void scrollToAndSelect(const QModelIndexList& indexes, const QModelIndex& current = QModelIndex());
-
- // refresh the view (to call when settings have been changed)
- //
- void refresh();
-
-signals:
-
- // emitted for dragEnter events
- //
- void dragEntered(const QMimeData* mimeData);
-
- // emitted for dropEnter events, the boolean indicates if the drop target
- // is expanded and the position of the indicator
- //
- void dropEntered(const QMimeData* mimeData, bool dropExpanded, DropPosition position);
-
-public slots:
-
- // invalidate (refresh) the filter (similar to a layout changed event)
- //
- void invalidateFilter();
-
- // set the filter criteria/options for mods
- //
- void setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria);
- void setFilterOptions(ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep);
-
- // update the mod counter
- //
- void updateModCount();
-
- // refresh the filters
- //
- void refreshFilters();
-
- // set highligth markers
- //
- void setHighlightedMods(const std::vector<unsigned int>& pluginIndices);
-
-protected:
-
- // map from/to the view indexes to the model
- //
- QModelIndex indexModelToView(const QModelIndex& index) const;
- QModelIndexList indexModelToView(const QModelIndexList& index) const;
- QModelIndex indexViewToModel(const QModelIndex& index) const;
- QModelIndexList indexViewToModel(const QModelIndexList& index) const;
-
- // returns the next/previous index of the given index
- //
- QModelIndex nextIndex(const QModelIndex& index) const;
- QModelIndex prevIndex(const QModelIndex& index) const;
-
- // re-implemented to fake the return value to allow drag-and-drop on
- // itself for separators
- //
- QModelIndexList selectedIndexes() const;
-
- // drop from external folder
- //
- void onExternalFolderDropped(const QUrl& url, int priority);
-
- // method to react to various key events
- //
- bool moveSelection(int key);
- bool removeSelection();
- bool toggleSelectionState();
-
- // re-implemented to fix indentation with collapsible separators
- //
- QRect visualRect(const QModelIndex& index) const override;
- void drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const override;
-
- void timerEvent(QTimerEvent* event) override;
- void dragEnterEvent(QDragEnterEvent* event) override;
- void dragMoveEvent(QDragMoveEvent* event) override;
- void dropEvent(QDropEvent* event) override;
- void mousePressEvent(QMouseEvent* event) override;
- void mouseReleaseEvent(QMouseEvent* event) override;
- bool event(QEvent* event) override;
-
-protected slots:
-
- void onCustomContextMenuRequested(const QPoint& pos);
- void onDoubleClicked(const QModelIndex& index);
- void onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& filters);
- void onProfileChanged(Profile* oldProfile, Profile* newProfile);
-
- void commitData(QWidget* editor) override;
-
-private: // friend classes
-
- friend class ModConflictIconDelegate;
- friend class ModContentIconDelegate;
- friend class ModFlagIconDelegate;
- friend class ModListContextMenu;
- friend class ModListStyledItemDelegate;
- friend class ModListViewActions;
- friend class ModListViewMarkingScrollBar;
-
-private: // private structures
-
- struct ModListViewUi {
- // the group by combo box
- QComboBox* groupBy;
-
- // the mod counter
- QLCDNumber* counter;
-
- // filters related
- QLineEdit* filter;
- QLabel* currentCategory;
- QPushButton* clearFilters;
- QComboBox* filterSeparators;
-
- // the plugin list (for highligths)
- PluginListView* pluginList;
- };
-
- struct MarkerInfos {
- // conflicts
- std::set<unsigned int> overwrite;
- std::set<unsigned int> overwritten;
- std::set<unsigned int> archiveOverwrite;
- std::set<unsigned int> archiveOverwritten;
- std::set<unsigned int> archiveLooseOverwrite;
- std::set<unsigned int> archiveLooseOverwritten;
-
- // selected plugins
- std::set<unsigned int> highlight;
- };
-
- struct ModCounters {
- int active = 0;
- int backup = 0;
- int foreign = 0;
- int separator = 0;
- int regular = 0;
-
- struct {
- int active = 0;
- int backup = 0;
- int foreign = 0;
- int separator = 0;
- int regular = 0;
- } visible;
- };
-
- // index in the groupby combo
- //
- enum GroupBy {
- NONE = 0,
- CATEGORY = 1,
- NEXUS_ID = 2
- };
-
-private: // private functions
-
- void onModPrioritiesChanged(const QModelIndexList& indices);
- void onModInstalled(const QString& modName);
- void onModFilterActive(bool filterActive);
-
- // refresh the overwrite markers and the highligthed plugins from
- // the current selection
- //
- void refreshMarkersAndPlugins();
-
- // clear overwrite markers (without repainting)
- //
- void clearOverwriteMarkers();
-
- // set overwrite markers from the mod in the given list and repaint (if the list
- // is empty, clear overwrite and repaint)
- //
- void setOverwriteMarkers(const QModelIndexList& indexes);
-
- // retrieve the marker color for the given index
- //
- QColor markerColor(const QModelIndex& index) const;
-
- // retrieve the mod flags for the given index
- //
- std::vector<ModInfo::EFlag> modFlags(
- const QModelIndex& index, bool* forceCompact = nullptr) const;
-
- // retrieve the conflicts flags for the given index
- //
- std::vector<ModInfo::EConflictFlag> conflictFlags(
- const QModelIndex& index, bool* forceCompact = nullptr) const;
-
- // retrieve the content icons and tooltip for the given index
- //
- std::set<int> contents(const QModelIndex& index, bool* includeChildren) const;
- QList<QString> contentsIcons(const QModelIndex& index, bool* forceCompact = nullptr) const;
- QString contentsTooltip(const QModelIndex& index) const;
-
- // compute the counters for mods according to the current filter
- //
- ModCounters counters() const;
-
- // get/set the selected items on the view, this method return/take indices
- // from the mod list model, not the view, so it's safe to restore
- //
- std::pair<QModelIndex, QModelIndexList> selected() const;
- void setSelected(const QModelIndex& current, const QModelIndexList& selected);
-
- // refresh stored expanded items for the current intermediate proxy
- //
- void refreshExpandedItems();
-
- // refresh the group-by proxy, if the index is -1 will refresh the
- // current one (e.g. when changing the sort column)
- //
- void updateGroupByProxy();
-
-public: // member variables
-
- OrganizerCore* m_core;
- std::unique_ptr<FilterList> m_filters;
- CategoryFactory* m_categories;
- ModListViewUi ui;
- ModListViewActions* m_actions;
-
- ModListSortProxy* m_sortProxy;
- ModListByPriorityProxy* m_byPriorityProxy;
- QtGroupingProxy* m_byCategoryProxy;
- QtGroupingProxy* m_byNexusIdProxy;
-
- // marker used to avoid calling refreshing markers to many
- // time in a row
- QTimer m_refreshMarkersTimer;
-
- // maintain collapsed items for each proxy to avoid
- // losing them on model reset
- std::map<QAbstractItemModel*, std::set<QString>> m_collapsed;
-
- MarkerInfos m_markers;
- ViewMarkingScrollBar* m_scrollbar;
-
- bool m_inDragMoveEvent = false;
-
- // replace the auto-expand timer from QTreeView to avoid
- // auto-collapsing
- QBasicTimer m_openTimer;
-
-};
-
-#endif // MODLISTVIEW_H
+#ifndef MODLISTVIEW_H
+#define MODLISTVIEW_H
+
+#include <map>
+#include <set>
+#include <vector>
+
+#include <QLabel>
+#include <QTreeView>
+#include <QDragEnterEvent>
+#include <QLCDNumber>
+
+#include "qtgroupingproxy.h"
+#include "viewmarkingscrollbar.h"
+#include "modlistsortproxy.h"
+
+namespace Ui { class MainWindow; }
+
+class CategoryFactory;
+class FilterList;
+class OrganizerCore;
+class MainWindow;
+class Profile;
+class ModListByPriorityProxy;
+class ModListViewActions;
+class PluginListView;
+
+class ModListView : public QTreeView
+{
+ Q_OBJECT
+
+public:
+
+ // this is a public version of DropIndicatorPosition
+ enum DropPosition {
+ OnItem = DropIndicatorPosition::OnItem,
+ AboveItem = DropIndicatorPosition::AboveItem,
+ BelowItem = DropIndicatorPosition::BelowItem,
+ OnViewport = DropIndicatorPosition::OnViewport
+ };
+
+ // indiucate the groupby mode
+ enum class GroupByMode {
+ NONE,
+ SEPARATOR,
+ CATEGORY,
+ NEXUS_ID
+ };
+
+public:
+ explicit ModListView(QWidget* parent = 0);
+
+ void setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw, Ui::MainWindow* mwui);
+
+ // restore/save the state between session
+ //
+ void restoreState(const Settings& s);
+ void saveState(Settings& s) const;
+
+ // check if collapsible separators are currently used
+ //
+ bool hasCollapsibleSeparators() const;
+
+ // the column/order by which the mod list is currently sorted
+ //
+ int sortColumn() const;
+ Qt::SortOrder sortOrder() const;
+
+ // check if a filter is currently active
+ //
+ bool isFilterActive() const;
+
+ // the current group mode
+ //
+ GroupByMode groupByMode() const;
+
+ // retrieve the actions from the view
+ //
+ ModListViewActions& actions() const;
+
+ // retrieve the next/previous mod in the current view, the given index
+ // should be a mod index (not a model row)
+ //
+ std::optional<unsigned int> nextMod(unsigned int index) const;
+ std::optional<unsigned int> prevMod(unsigned int index) const;
+
+ // check if the given mod is visible, i.e. not filtered (returns true
+ // for collapsed mods)
+ //
+ bool isModVisible(unsigned int index) const;
+ bool isModVisible(ModInfo::Ptr mod) const;
+
+ // focus the view, select the given index and scroll to it
+ //
+ void scrollToAndSelect(const QModelIndex& index);
+ void scrollToAndSelect(const QModelIndexList& indexes, const QModelIndex& current = QModelIndex());
+
+ // refresh the view (to call when settings have been changed)
+ //
+ void refresh();
+
+signals:
+
+ // emitted for dragEnter events
+ //
+ void dragEntered(const QMimeData* mimeData);
+
+ // emitted for dropEnter events, the boolean indicates if the drop target
+ // is expanded and the position of the indicator
+ //
+ void dropEntered(const QMimeData* mimeData, bool dropExpanded, DropPosition position);
+
+public slots:
+
+ // invalidate (refresh) the filter (similar to a layout changed event)
+ //
+ void invalidateFilter();
+
+ // set the filter criteria/options for mods
+ //
+ void setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria);
+ void setFilterOptions(ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep);
+
+ // update the mod counter
+ //
+ void updateModCount();
+
+ // refresh the filters
+ //
+ void refreshFilters();
+
+ // set highligth markers
+ //
+ void setHighlightedMods(const std::vector<unsigned int>& pluginIndices);
+
+protected:
+
+ // map from/to the view indexes to the model
+ //
+ QModelIndex indexModelToView(const QModelIndex& index) const;
+ QModelIndexList indexModelToView(const QModelIndexList& index) const;
+ QModelIndex indexViewToModel(const QModelIndex& index) const;
+ QModelIndexList indexViewToModel(const QModelIndexList& index) const;
+
+ // returns the next/previous index of the given index
+ //
+ QModelIndex nextIndex(const QModelIndex& index) const;
+ QModelIndex prevIndex(const QModelIndex& index) const;
+
+ // re-implemented to fake the return value to allow drag-and-drop on
+ // itself for separators
+ //
+ QModelIndexList selectedIndexes() const;
+
+ // drop from external folder
+ //
+ void onExternalFolderDropped(const QUrl& url, int priority);
+
+ // method to react to various key events
+ //
+ bool moveSelection(int key);
+ bool removeSelection();
+ bool toggleSelectionState();
+
+ // re-implemented to fix indentation with collapsible separators
+ //
+ QRect visualRect(const QModelIndex& index) const override;
+ void drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const override;
+
+ void timerEvent(QTimerEvent* event) override;
+ void dragEnterEvent(QDragEnterEvent* event) override;
+ void dragMoveEvent(QDragMoveEvent* event) override;
+ void dropEvent(QDropEvent* event) override;
+ void mousePressEvent(QMouseEvent* event) override;
+ void mouseReleaseEvent(QMouseEvent* event) override;
+ bool event(QEvent* event) override;
+
+protected slots:
+
+ void onCustomContextMenuRequested(const QPoint& pos);
+ void onDoubleClicked(const QModelIndex& index);
+ void onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& filters);
+ void onProfileChanged(Profile* oldProfile, Profile* newProfile);
+
+ void commitData(QWidget* editor) override;
+
+private: // friend classes
+
+ friend class ModConflictIconDelegate;
+ friend class ModContentIconDelegate;
+ friend class ModFlagIconDelegate;
+ friend class ModListContextMenu;
+ friend class ModListStyledItemDelegate;
+ friend class ModListViewActions;
+ friend class ModListViewMarkingScrollBar;
+
+private: // private structures
+
+ struct ModListViewUi {
+ // the group by combo box
+ QComboBox* groupBy;
+
+ // the mod counter
+ QLCDNumber* counter;
+
+ // filters related
+ QLineEdit* filter;
+ QLabel* currentCategory;
+ QPushButton* clearFilters;
+ QComboBox* filterSeparators;
+
+ // the plugin list (for highligths)
+ PluginListView* pluginList;
+ };
+
+ struct MarkerInfos {
+ // conflicts
+ std::set<unsigned int> overwrite;
+ std::set<unsigned int> overwritten;
+ std::set<unsigned int> archiveOverwrite;
+ std::set<unsigned int> archiveOverwritten;
+ std::set<unsigned int> archiveLooseOverwrite;
+ std::set<unsigned int> archiveLooseOverwritten;
+
+ // selected plugins
+ std::set<unsigned int> highlight;
+ };
+
+ struct ModCounters {
+ int active = 0;
+ int backup = 0;
+ int foreign = 0;
+ int separator = 0;
+ int regular = 0;
+
+ struct {
+ int active = 0;
+ int backup = 0;
+ int foreign = 0;
+ int separator = 0;
+ int regular = 0;
+ } visible;
+ };
+
+ // index in the groupby combo
+ //
+ enum GroupBy {
+ NONE = 0,
+ CATEGORY = 1,
+ NEXUS_ID = 2
+ };
+
+private: // private functions
+
+ void onModPrioritiesChanged(const QModelIndexList& indices);
+ void onModInstalled(const QString& modName);
+ void onModFilterActive(bool filterActive);
+
+ // refresh the overwrite markers and the highligthed plugins from
+ // the current selection
+ //
+ void refreshMarkersAndPlugins();
+
+ // clear overwrite markers (without repainting)
+ //
+ void clearOverwriteMarkers();
+
+ // set overwrite markers from the mod in the given list and repaint (if the list
+ // is empty, clear overwrite and repaint)
+ //
+ void setOverwriteMarkers(const QModelIndexList& indexes);
+
+ // retrieve the marker color for the given index
+ //
+ QColor markerColor(const QModelIndex& index) const;
+
+ // retrieve the mod flags for the given index
+ //
+ std::vector<ModInfo::EFlag> modFlags(
+ const QModelIndex& index, bool* forceCompact = nullptr) const;
+
+ // retrieve the conflicts flags for the given index
+ //
+ std::vector<ModInfo::EConflictFlag> conflictFlags(
+ const QModelIndex& index, bool* forceCompact = nullptr) const;
+
+ // retrieve the content icons and tooltip for the given index
+ //
+ std::set<int> contents(const QModelIndex& index, bool* includeChildren) const;
+ QList<QString> contentsIcons(const QModelIndex& index, bool* forceCompact = nullptr) const;
+ QString contentsTooltip(const QModelIndex& index) const;
+
+ // compute the counters for mods according to the current filter
+ //
+ ModCounters counters() const;
+
+ // get/set the selected items on the view, this method return/take indices
+ // from the mod list model, not the view, so it's safe to restore
+ //
+ std::pair<QModelIndex, QModelIndexList> selected() const;
+ void setSelected(const QModelIndex& current, const QModelIndexList& selected);
+
+ // refresh stored expanded items for the current intermediate proxy
+ //
+ void refreshExpandedItems();
+
+ // refresh the group-by proxy, if the index is -1 will refresh the
+ // current one (e.g. when changing the sort column)
+ //
+ void updateGroupByProxy();
+
+public: // member variables
+
+ OrganizerCore* m_core;
+ std::unique_ptr<FilterList> m_filters;
+ CategoryFactory* m_categories;
+ ModListViewUi ui;
+ ModListViewActions* m_actions;
+
+ ModListSortProxy* m_sortProxy;
+ ModListByPriorityProxy* m_byPriorityProxy;
+ QtGroupingProxy* m_byCategoryProxy;
+ QtGroupingProxy* m_byNexusIdProxy;
+
+ // marker used to avoid calling refreshing markers to many
+ // time in a row
+ QTimer m_refreshMarkersTimer;
+
+ // maintain collapsed items for each proxy to avoid
+ // losing them on model reset
+ std::map<QAbstractItemModel*, std::set<QString>> m_collapsed;
+
+ MarkerInfos m_markers;
+ ViewMarkingScrollBar* m_scrollbar;
+
+ bool m_inDragMoveEvent = false;
+
+ // replace the auto-expand timer from QTreeView to avoid
+ // auto-collapsing
+ QBasicTimer m_openTimer;
+
+};
+
+#endif // MODLISTVIEW_H
diff --git a/src/motddialog.cpp b/src/motddialog.cpp
index eee80205..94a34d58 100644
--- a/src/motddialog.cpp
+++ b/src/motddialog.cpp
@@ -1,51 +1,51 @@
-/*
-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 "motddialog.h"
-#include "bbcode.h"
-#include "utility.h"
-#include "ui_motddialog.h"
-#include "organizercore.h"
-#include <utility.h>
-#include <Shlwapi.h>
-
-using namespace MOBase;
-
-MotDDialog::MotDDialog(const QString &message, QWidget *parent)
- : QDialog(parent), ui(new Ui::MotDDialog)
-{
- ui->setupUi(this);
- ui->motdView->setHtml(BBCode::convertToHTML(message));
- connect(ui->motdView, SIGNAL(anchorClicked(QUrl)), this, SLOT(linkClicked(QUrl)));
-}
-
-MotDDialog::~MotDDialog()
-{
- delete ui;
-}
-
-void MotDDialog::on_okButton_clicked()
-{
- this->close();
-}
-
-void MotDDialog::linkClicked(const QUrl &url)
-{
- shell::Open(url);
-}
+/*
+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 "motddialog.h"
+#include "bbcode.h"
+#include "utility.h"
+#include "ui_motddialog.h"
+#include "organizercore.h"
+#include <utility.h>
+#include <Shlwapi.h>
+
+using namespace MOBase;
+
+MotDDialog::MotDDialog(const QString &message, QWidget *parent)
+ : QDialog(parent), ui(new Ui::MotDDialog)
+{
+ ui->setupUi(this);
+ ui->motdView->setHtml(BBCode::convertToHTML(message));
+ connect(ui->motdView, SIGNAL(anchorClicked(QUrl)), this, SLOT(linkClicked(QUrl)));
+}
+
+MotDDialog::~MotDDialog()
+{
+ delete ui;
+}
+
+void MotDDialog::on_okButton_clicked()
+{
+ this->close();
+}
+
+void MotDDialog::linkClicked(const QUrl &url)
+{
+ shell::Open(url);
+}
diff --git a/src/motddialog.h b/src/motddialog.h
index 5791321f..929a51e5 100644
--- a/src/motddialog.h
+++ b/src/motddialog.h
@@ -1,46 +1,46 @@
-/*
-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 MOTDDIALOG_H
-#define MOTDDIALOG_H
-
-#include <QDialog>
-#include <QUrl>
-
-namespace Ui {
-class MotDDialog;
-}
-
-class MotDDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit MotDDialog(const QString &message, QWidget *parent = 0);
- ~MotDDialog();
-
-private slots:
- void on_okButton_clicked();
- void linkClicked(const QUrl &url);
-
-private:
- Ui::MotDDialog *ui;
-};
-
-#endif // MOTDDIALOG_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 MOTDDIALOG_H
+#define MOTDDIALOG_H
+
+#include <QDialog>
+#include <QUrl>
+
+namespace Ui {
+class MotDDialog;
+}
+
+class MotDDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit MotDDialog(const QString &message, QWidget *parent = 0);
+ ~MotDDialog();
+
+private slots:
+ void on_okButton_clicked();
+ void linkClicked(const QUrl &url);
+
+private:
+ Ui::MotDDialog *ui;
+};
+
+#endif // MOTDDIALOG_H
diff --git a/src/multiprocess.cpp b/src/multiprocess.cpp
index 4ce58718..7da2468d 100644
--- a/src/multiprocess.cpp
+++ b/src/multiprocess.cpp
@@ -1,102 +1,102 @@
-#include "multiprocess.h"
-#include "utility.h"
-#include <report.h>
-#include <log.h>
-#include <QLocalSocket>
-
-static const char s_Key[] = "mo-43d1a3ad-eeb0-4818-97c9-eda5216c29b5";
-static const int s_Timeout = 5000;
-
-using MOBase::reportError;
-
-MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject *parent) :
- QObject(parent), m_Ephemeral(false), m_OwnsSM(false)
-{
- m_SharedMem.setKey(s_Key);
-
- if (!m_SharedMem.create(1)) {
- if (m_SharedMem.error() == QSharedMemory::AlreadyExists) {
- if (!allowMultiple) {
- m_SharedMem.attach();
- m_Ephemeral = true;
- }
- }
-
- if ((m_SharedMem.error() != QSharedMemory::NoError) &&
- (m_SharedMem.error() != QSharedMemory::AlreadyExists)) {
- throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString()));
- }
- } else {
- m_OwnsSM = true;
- }
-
- if (m_OwnsSM) {
- connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()), Qt::QueuedConnection);
- // has to be called before listen
- m_Server.setSocketOptions(QLocalServer::WorldAccessOption);
- m_Server.listen(s_Key);
- }
-}
-
-
-void MOMultiProcess::sendMessage(const QString &message)
-{
- if (m_OwnsSM) {
- // nobody there to receive the message
- return;
- }
- QLocalSocket socket(this);
-
- bool connected = false;
- for(int i = 0; i < 2 && !connected; ++i) {
- if (i > 0) {
- Sleep(250);
- }
-
- // other process may be just starting up
- socket.connectToServer(s_Key, QIODevice::WriteOnly);
- connected = socket.waitForConnected(s_Timeout);
- }
-
- if (!connected) {
- reportError(tr("failed to connect to running process: %1").arg(socket.errorString()));
- return;
- }
-
- socket.write(message.toUtf8());
- if (!socket.waitForBytesWritten(s_Timeout)) {
- reportError(tr("failed to communicate with running process: %1").arg(socket.errorString()));
- return;
- }
-
- socket.disconnectFromServer();
- socket.waitForDisconnected();
-}
-
-void MOMultiProcess::receiveMessage()
-{
- QLocalSocket *socket = m_Server.nextPendingConnection();
- if (!socket) {
- return;
- }
-
- if (!socket->waitForReadyRead(s_Timeout)) {
- // check if there are bytes available; if so, it probably means the data was
- // already received by the time waitForReadyRead() was called and the
- // connection has been closed
- const auto av = socket->bytesAvailable();
-
- if (av <= 0) {
- MOBase::log::error(
- "failed to receive data from secondary process: {}",
- socket->errorString());
-
- reportError(tr("failed to receive data from secondary process: %1").arg(socket->errorString()));
- return;
- }
- }
-
- QString message = QString::fromUtf8(socket->readAll().constData());
- emit messageSent(message);
- socket->disconnectFromServer();
-}
+#include "multiprocess.h"
+#include "utility.h"
+#include <report.h>
+#include <log.h>
+#include <QLocalSocket>
+
+static const char s_Key[] = "mo-43d1a3ad-eeb0-4818-97c9-eda5216c29b5";
+static const int s_Timeout = 5000;
+
+using MOBase::reportError;
+
+MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject *parent) :
+ QObject(parent), m_Ephemeral(false), m_OwnsSM(false)
+{
+ m_SharedMem.setKey(s_Key);
+
+ if (!m_SharedMem.create(1)) {
+ if (m_SharedMem.error() == QSharedMemory::AlreadyExists) {
+ if (!allowMultiple) {
+ m_SharedMem.attach();
+ m_Ephemeral = true;
+ }
+ }
+
+ if ((m_SharedMem.error() != QSharedMemory::NoError) &&
+ (m_SharedMem.error() != QSharedMemory::AlreadyExists)) {
+ throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString()));
+ }
+ } else {
+ m_OwnsSM = true;
+ }
+
+ if (m_OwnsSM) {
+ connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()), Qt::QueuedConnection);
+ // has to be called before listen
+ m_Server.setSocketOptions(QLocalServer::WorldAccessOption);
+ m_Server.listen(s_Key);
+ }
+}
+
+
+void MOMultiProcess::sendMessage(const QString &message)
+{
+ if (m_OwnsSM) {
+ // nobody there to receive the message
+ return;
+ }
+ QLocalSocket socket(this);
+
+ bool connected = false;
+ for(int i = 0; i < 2 && !connected; ++i) {
+ if (i > 0) {
+ Sleep(250);
+ }
+
+ // other process may be just starting up
+ socket.connectToServer(s_Key, QIODevice::WriteOnly);
+ connected = socket.waitForConnected(s_Timeout);
+ }
+
+ if (!connected) {
+ reportError(tr("failed to connect to running process: %1").arg(socket.errorString()));
+ return;
+ }
+
+ socket.write(message.toUtf8());
+ if (!socket.waitForBytesWritten(s_Timeout)) {
+ reportError(tr("failed to communicate with running process: %1").arg(socket.errorString()));
+ return;
+ }
+
+ socket.disconnectFromServer();
+ socket.waitForDisconnected();
+}
+
+void MOMultiProcess::receiveMessage()
+{
+ QLocalSocket *socket = m_Server.nextPendingConnection();
+ if (!socket) {
+ return;
+ }
+
+ if (!socket->waitForReadyRead(s_Timeout)) {
+ // check if there are bytes available; if so, it probably means the data was
+ // already received by the time waitForReadyRead() was called and the
+ // connection has been closed
+ const auto av = socket->bytesAvailable();
+
+ if (av <= 0) {
+ MOBase::log::error(
+ "failed to receive data from secondary process: {}",
+ socket->errorString());
+
+ reportError(tr("failed to receive data from secondary process: %1").arg(socket->errorString()));
+ return;
+ }
+ }
+
+ QString message = QString::fromUtf8(socket->readAll().constData());
+ emit messageSent(message);
+ socket->disconnectFromServer();
+}
diff --git a/src/multiprocess.h b/src/multiprocess.h
index 810e63d2..3cb5f52a 100644
--- a/src/multiprocess.h
+++ b/src/multiprocess.h
@@ -1,70 +1,70 @@
-#ifndef MODORGANIZER_MOMULTIPROCESS_INCLUDED
-#define MODORGANIZER_MOMULTIPROCESS_INCLUDED
-
-#include <QObject>
-#include <QSharedMemory>
-#include <QLocalServer>
-
-
-/**
- * used to ensure only a single process of Mod Organizer is started and to
- * allow ephemeral processes to send messages to the primary (visible) one.
- * This way, other processes can start a download in the primary one
- **/
-class MOMultiProcess : public QObject
-{
- Q_OBJECT
-
-public:
- // `allowMultiple`: if another process is running, run this one
- // disconnected from the shared memory
- explicit MOMultiProcess(bool allowMultiple, QObject *parent = 0);
-
- /**
- * @return true if this process's job is to forward data to the primary
- * process through shared memory
- **/
- bool ephemeral() const
- {
- return m_Ephemeral;
- }
-
- // returns true if this is not the primary process, but was allowed because
- // of the AllowMultiple flag
- //
- bool secondary() const
- {
- return !m_Ephemeral && !m_OwnsSM;
- }
-
- /**
- * send a message to the primary process. This can be used to transmit download urls
- *
- * @param message message to send
- **/
- void sendMessage(const QString &message);
-
-signals:
-
- /**
- * @brief emitted when an ephemeral process has sent a message (to us)
- *
- * @param message the message we received
- **/
- void messageSent(const QString &message);
-
-public slots:
-
-private slots:
-
- void receiveMessage();
-
-private:
- bool m_Ephemeral;
- bool m_OwnsSM;
- QSharedMemory m_SharedMem;
- QLocalServer m_Server;
-
-};
-
-#endif // MODORGANIZER_MOMULTIPROCESS_INCLUDED
+#ifndef MODORGANIZER_MOMULTIPROCESS_INCLUDED
+#define MODORGANIZER_MOMULTIPROCESS_INCLUDED
+
+#include <QObject>
+#include <QSharedMemory>
+#include <QLocalServer>
+
+
+/**
+ * used to ensure only a single process of Mod Organizer is started and to
+ * allow ephemeral processes to send messages to the primary (visible) one.
+ * This way, other processes can start a download in the primary one
+ **/
+class MOMultiProcess : public QObject
+{
+ Q_OBJECT
+
+public:
+ // `allowMultiple`: if another process is running, run this one
+ // disconnected from the shared memory
+ explicit MOMultiProcess(bool allowMultiple, QObject *parent = 0);
+
+ /**
+ * @return true if this process's job is to forward data to the primary
+ * process through shared memory
+ **/
+ bool ephemeral() const
+ {
+ return m_Ephemeral;
+ }
+
+ // returns true if this is not the primary process, but was allowed because
+ // of the AllowMultiple flag
+ //
+ bool secondary() const
+ {
+ return !m_Ephemeral && !m_OwnsSM;
+ }
+
+ /**
+ * send a message to the primary process. This can be used to transmit download urls
+ *
+ * @param message message to send
+ **/
+ void sendMessage(const QString &message);
+
+signals:
+
+ /**
+ * @brief emitted when an ephemeral process has sent a message (to us)
+ *
+ * @param message the message we received
+ **/
+ void messageSent(const QString &message);
+
+public slots:
+
+private slots:
+
+ void receiveMessage();
+
+private:
+ bool m_Ephemeral;
+ bool m_OwnsSM;
+ QSharedMemory m_SharedMem;
+ QLocalServer m_Server;
+
+};
+
+#endif // MODORGANIZER_MOMULTIPROCESS_INCLUDED
diff --git a/src/noeditdelegate.cpp b/src/noeditdelegate.cpp
index d5147b9f..671fa427 100644
--- a/src/noeditdelegate.cpp
+++ b/src/noeditdelegate.cpp
@@ -1,10 +1,10 @@
-#include "noeditdelegate.h"
-
-NoEditDelegate::NoEditDelegate(QObject *parent)
- : QStyledItemDelegate(parent)
-{
-}
-
-QWidget *NoEditDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
- return nullptr;
-}
+#include "noeditdelegate.h"
+
+NoEditDelegate::NoEditDelegate(QObject *parent)
+ : QStyledItemDelegate(parent)
+{
+}
+
+QWidget *NoEditDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
+ return nullptr;
+}
diff --git a/src/noeditdelegate.h b/src/noeditdelegate.h
index 6fd5ba76..ff9baa46 100644
--- a/src/noeditdelegate.h
+++ b/src/noeditdelegate.h
@@ -1,12 +1,12 @@
-#ifndef NOEDITDELEGATE_H
-#define NOEDITDELEGATE_H
-
-#include <QStyledItemDelegate>
-
-class NoEditDelegate: public QStyledItemDelegate {
-public:
- NoEditDelegate(QObject *parent = nullptr);
- virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
-};
-
-#endif // NOEDITDELEGATE_H
+#ifndef NOEDITDELEGATE_H
+#define NOEDITDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+class NoEditDelegate: public QStyledItemDelegate {
+public:
+ NoEditDelegate(QObject *parent = nullptr);
+ virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
+};
+
+#endif // NOEDITDELEGATE_H
diff --git a/src/organizercore.h b/src/organizercore.h
index 12058357..dc287b1f 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -1,502 +1,502 @@
-#ifndef ORGANIZERCORE_H
-#define ORGANIZERCORE_H
-
-#include "selfupdater.h"
-#include "settings.h"
-#include "modlist.h"
-#include "modinfo.h"
-#include "pluginlist.h"
-#include "installationmanager.h"
-#include "downloadmanager.h"
-#include "executableslist.h"
-#include "usvfsconnector.h"
-#include "guessedvalue.h"
-#include "moshortcut.h"
-#include "memoizedlock.h"
-#include "processrunner.h"
-#include "uilocker.h"
-#include "envdump.h"
-#include <imoinfo.h>
-#include <iplugindiagnose.h>
-#include <versioninfo.h>
-#include <delayedfilewriter.h>
-#include <boost/signals2.hpp>
-#include "executableinfo.h"
-#include "moddatacontent.h"
-#include <log.h>
-
-#include <QDir>
-#include <QFileInfo>
-#include <QList>
-#include <QObject>
-#include <QSettings>
-#include <QString>
-#include <QStringList>
-#include <QThread>
-#include <QVariant>
-
-class ModListSortProxy;
-class PluginListSortProxy;
-class Profile;
-class IUserInterface;
-class PluginContainer;
-class DirectoryRefresher;
-
-namespace MOBase
-{
- template <typename T> class GuessedValue;
- class IModInterface;
- class IPluginGame;
-}
-
-namespace MOShared
-{
- class DirectoryEntry;
-}
-
-
-class OrganizerCore : public QObject, public MOBase::IPluginDiagnose
-{
-
- Q_OBJECT
- Q_INTERFACES(MOBase::IPluginDiagnose)
-
-private:
-
- friend class OrganizerProxy;
-
- struct SignalCombinerAnd
- {
- using result_type = bool;
-
- template<typename InputIterator>
- bool operator()(InputIterator first, InputIterator last) const
- {
- while (first != last) {
- if (!(*first)) {
- return false;
- }
- ++first;
- }
- return true;
- }
- };
-
-private:
-
- using SignalAboutToRunApplication = boost::signals2::signal<bool(const QString&), SignalCombinerAnd>;
- using SignalFinishedRunApplication = boost::signals2::signal<void(const QString&, unsigned int)>;
- using SignalUserInterfaceInitialized = boost::signals2::signal<void(QMainWindow*)>;
- using SignalProfileCreated = boost::signals2::signal<void(MOBase::IProfile*)>;
- using SignalProfileRenamed = boost::signals2::signal<void(MOBase::IProfile*, QString const&, QString const&)>;
- using SignalProfileRemoved = boost::signals2::signal<void(QString const&)>;
- using SignalProfileChanged = boost::signals2::signal<void(MOBase::IProfile *, MOBase::IProfile *)>;
- using SignalPluginSettingChanged = boost::signals2::signal<void(QString const&, const QString& key, const QVariant&, const QVariant&)>;
- using SignalPluginEnabled = boost::signals2::signal<void(const MOBase::IPlugin*)>;
-
-public:
-
- /**
- * Small holder for the game content returned by the ModDataContent feature (the
- * list of all possible contents, not the per-mod content).
- */
- struct ModDataContentHolder {
-
- using Content = ModDataContent::Content;
-
- /**
- * @return true if the hold list of contents is empty, false otherwise.
- */
- bool empty() const { return m_Contents.empty(); }
-
- /**
- * @param id ID of the content to retrieve.
- *
- * @return the content with the given ID, or a null pointer if it is not found.
- */
- const Content* findById(int id) const {
- auto it = std::find_if(std::begin(m_Contents), std::end(m_Contents), [&id](auto const& content) { return content.id() == id; });
- return it == std::end(m_Contents) ? nullptr : &(*it);
- }
-
- /**
- * Apply the given function to each content whose ID is in the given set.
- *
- * @param ids The set of content IDs.
- * @param fn The function to apply.
- * @param includeFilter true to also apply the function to filter-only contents, false otherwise.
- */
- template <class Fn>
- void forEachContentIn(std::set<int> const& ids, Fn const& fn, bool includeFilter = false) const {
- for (const auto& content : m_Contents) {
- if ((includeFilter || !content.isOnlyForFilter())
- && ids.find(content.id()) != ids.end()) {
- fn(content);
- }
- }
- }
-
- /**
- * @brief Apply fnIn to each content whose ID is in the given set, and fnOut to each content not in the
- * given set, excluding filter-only content (from both cases) unless includeFilter is true.
- *
- * @param ids The set of content IDs.
- * @param fnIn Function to apply to content whose IDs are in ids.
- * @param fnOut Function to apply to content whose IDs are not in ids.
- * @param includeFilter true to also apply the function to filter-only contents, false otherwise.
- */
- template <class FnIn, class FnOut>
- void forEachContentInOrOut(std::set<int> const& ids, FnIn const& fnIn, FnOut const& fnOut, bool includeFilter = false) const {
- for (const auto& content : m_Contents) {
- if ((includeFilter || !content.isOnlyForFilter())) {
- if (ids.find(content.id()) != ids.end()) {
- fnIn(content);
- }
- else {
- fnOut(content);
- }
- }
- }
- }
-
- /**
- * Apply the given function to each content.
- *
- * @param fn The function to apply.
- * @param includeFilter true to also apply the function to filter-only contents, false otherwise.
- */
- template <class Fn>
- void forEachContent(Fn const& fn, bool includeFilter = false) const {
- for (const auto& content : m_Contents) {
- if (includeFilter || !content.isOnlyForFilter()) {
- fn(content);
- }
- }
- }
-
-
- ModDataContentHolder& operator=(ModDataContentHolder const&) = delete;
- ModDataContentHolder& operator=(ModDataContentHolder&&) = default;
-
- private:
-
- std::vector<Content> m_Contents;
-
- /**
- * @brief Construct a ModDataContentHolder without any contents (e.g., if the feature is
- * missing).
- */
- ModDataContentHolder() { }
-
- /**
- * @brief Construct a ModDataContentHold holding the given list of contents.
- */
- ModDataContentHolder(std::vector<ModDataContent::Content> contents) :
- m_Contents(std::move(contents)) { }
-
- friend class OrganizerCore;
- };
-
-public:
- OrganizerCore(Settings &settings);
-
- ~OrganizerCore();
-
- void setUserInterface(IUserInterface* ui);
- void connectPlugins(PluginContainer *container);
-
- void setManagedGame(MOBase::IPluginGame *game);
-
- void updateExecutablesList();
- void updateModInfoFromDisc();
-
- void checkForUpdates();
- void startMOUpdate();
-
- Settings &settings();
- SelfUpdater *updater() { return &m_Updater; }
- InstallationManager *installationManager();
- MOShared::DirectoryEntry *directoryStructure() { return m_DirectoryStructure; }
- DirectoryRefresher *directoryRefresher() { return m_DirectoryRefresher.get(); }
- ExecutablesList *executablesList() { return &m_ExecutablesList; }
- void setExecutablesList(const ExecutablesList &executablesList) {
- m_ExecutablesList = executablesList;
- }
-
- Profile *currentProfile() const { return m_CurrentProfile.get(); }
- void setCurrentProfile(const QString &profileName);
-
- std::vector<QString> enabledArchives();
-
- MOBase::VersionInfo getVersion() const { return m_Updater.getVersion(); }
-
- // return the plugin container
- //
- PluginContainer& pluginContainer() const;
-
- MOBase::IPluginGame const *managedGame() const;
-
- /**
- * @brief Retrieve the organizer proxy of the currently managed game.
- *
- */
- MOBase::IOrganizer const* managedGameOrganizer() const;
-
-
- /**
- * @return the list of contents for the currently managed game, or an empty vector
- * if the game plugin does not implement the ModDataContent feature.
- */
- const ModDataContentHolder& modDataContents() const { return m_Contents; }
-
- bool isArchivesInit() const { return m_ArchivesInit; }
-
- bool saveCurrentLists();
-
- ProcessRunner processRunner();
-
- bool beforeRun(
- const QFileInfo& binary, const QString& profileName,
- const QString& customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries);
-
- void afterRun(const QFileInfo& binary, DWORD exitCode);
-
- ProcessRunner::Results waitForAllUSVFSProcesses(
- UILocker::Reasons reason=UILocker::PreventExit);
-
- void refreshESPList(bool force = false);
- void refreshBSAList();
-
- void refreshDirectoryStructure();
- void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
- void updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> modInfos);
-
- void doAfterLogin(const std::function<void()> &function) { m_PostLoginTasks.append(function); }
- void loggedInAction(QWidget* parent, std::function<void ()> f);
-
- bool previewFileWithAlternatives(QWidget* parent, QString filename, int selectedOrigin=-1);
- bool previewFile(QWidget* parent, const QString& originName, const QString& path);
-
- void loginSuccessfulUpdate(bool necessary);
- void loginFailedUpdate(const QString &message);
-
- static bool createAndMakeWritable(const QString &path);
- bool checkPathSymlinks();
- bool bootstrap();
- void createDefaultProfile();
-
- MOBase::DelayedFileWriter &pluginsWriter() { return m_PluginListsWriter; }
-
- void prepareVFS();
-
- void updateVFSParams(
- MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType,
- const QString& coreDumpsPath, std::chrono::seconds spawnDelay,
- QString executableBlacklist);
-
- void setLogLevel(MOBase::log::Levels level);
-
- bool cycleDiagnostics();
-
- static env::CoreDumpTypes getGlobalCoreDumpType();
- static void setGlobalCoreDumpType(env::CoreDumpTypes type);
- static std::wstring getGlobalCoreDumpPath();
-
-public:
- MOBase::IModRepositoryBridge *createNexusBridge() const;
- QString profileName() const;
- QString profilePath() const;
- QString downloadsPath() const;
- QString overwritePath() const;
- QString basePath() const;
- QString modsPath() const;
- MOBase::VersionInfo appVersion() const;
- MOBase::IPluginGame *getGame(const QString &gameName) const;
- MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name);
- void modDataChanged(MOBase::IModInterface *mod);
- QVariant pluginSetting(const QString &pluginName, const QString &key) const;
- void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value);
- QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def) const;
- void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync);
- static QString pluginDataPath();
- virtual MOBase::IModInterface *installMod(const QString &fileName, int priority, bool reinstallation, ModInfo::Ptr currentMod, const QString &initModName);
- QString resolvePath(const QString &fileName) const;
- QStringList listDirectories(const QString &directoryName) const;
- QStringList findFiles(const QString &path, const std::function<bool (const QString &)> &filter) const;
- QStringList getFileOrigins(const QString &fileName) const;
- QList<MOBase::IOrganizer::FileInfo> findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const;
- DownloadManager *downloadManager();
- PluginList *pluginList();
- ModList *modList();
- void refresh(bool saveChanges = true);
-
- boost::signals2::connection onAboutToRun(const std::function<bool(const QString&)>& func);
- boost::signals2::connection onFinishedRun(const std::function<void(const QString&, unsigned int)>& func);
- boost::signals2::connection onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func);
- boost::signals2::connection onProfileCreated(std::function<void(MOBase::IProfile*)> const& func);
- boost::signals2::connection onProfileRenamed(std::function<void(MOBase::IProfile*, QString const&, QString const&)> const& func);
- boost::signals2::connection onProfileRemoved(std::function<void(QString const&)> const& func);
- boost::signals2::connection onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func);
- boost::signals2::connection onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func);
- boost::signals2::connection onPluginEnabled(std::function<void(const MOBase::IPlugin*)> const& func);
- boost::signals2::connection onPluginDisabled(std::function<void(const MOBase::IPlugin*)> const& func);
-
-public: // IPluginDiagnose interface
-
- virtual std::vector<unsigned int> activeProblems() const;
- virtual QString shortDescription(unsigned int key) const;
- virtual QString fullDescription(unsigned int key) const;
- virtual bool hasGuidedFix(unsigned int key) const;
- virtual void startGuidedFix(unsigned int key) const;
-
-public slots:
-
- void profileRefresh();
-
- void syncOverwrite();
-
- void savePluginList();
-
- void refreshLists();
-
- ModInfo::Ptr installDownload(int downloadIndex, int priority = -1);
- ModInfo::Ptr installArchive(const QString& archivePath, int priority = -1, bool reinstallation = false,
- ModInfo::Ptr currentMod = nullptr, const QString& modName = QString());
-
- void modPrioritiesChanged(QModelIndexList const& indexes);
- void modStatusChanged(unsigned int index);
- void modStatusChanged(QList<unsigned int> index);
- void requestDownload(const QUrl &url, QNetworkReply *reply);
- void downloadRequestedNXM(const QString &url);
-
- void userInterfaceInitialized();
-
- void profileCreated(MOBase::IProfile* profile);
- void profileRenamed(MOBase::IProfile* profile, QString const& oldName, QString const& newName);
- void profileRemoved(QString const& profileName);
-
- bool nexusApi(bool retry = false);
-
-signals:
-
- // emitted after a mod has been installed
- //
- void modInstalled(const QString &modName);
-
- // emitted when the managed game changes
- //
- void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
-
- // emitted when the profile is changed, before notifying plugins
- //
- // the new profile can be stored but the old one is temporary and
- // should not be
- //
- void profileChanged(Profile* oldProfile, Profile* newProfile);
-
- // Notify that the directory structure is ready to be used on the main thread
- // Use queued connections
- void directoryStructureReady();
-
-private:
-
- std::pair<unsigned int, ModInfo::Ptr> doInstall(const QString& archivePath,
- MOBase::GuessedValue<QString> modName, ModInfo::Ptr currentMod, int priority, bool reinstallation);
-
- void saveCurrentProfile();
- void storeSettings();
-
- void updateModActiveState(int index, bool active);
- void updateModsActiveState(const QList<unsigned int> &modIndices, bool active);
-
- // clear the conflict caches of all the given mods, and the mods in conflict
- // with the given mods
- //
- void clearCaches(std::vector<unsigned int> const& indices) const;
-
- bool createDirectory(const QString &path);
-
- QString oldMO1HookDll() const;
-
- /**
- * @brief return a descriptor of the mappings real file->virtual file
- */
- std::vector<Mapping> fileMapping(const QString &profile,
- const QString &customOverwrite);
-
- std::vector<Mapping>
- fileMapping(const QString &dataPath, const QString &relPath,
- const MOShared::DirectoryEntry *base,
- const MOShared::DirectoryEntry *directoryEntry,
- int createDestination);
-
-private slots:
-
- void directory_refreshed();
- void downloadRequested(QNetworkReply *reply, QString gameName, int modID, const QString &fileName);
- void removeOrigin(const QString &name);
- void downloadSpeed(const QString &serverName, int bytesPerSecond);
- void loginSuccessful(bool necessary);
- void loginFailed(const QString &message);
-
-private:
- static const unsigned int PROBLEM_MO1SCRIPTEXTENDERWORKAROUND = 1;
-
-private:
- IUserInterface* m_UserInterface;
- PluginContainer *m_PluginContainer;
- QString m_GameName;
- MOBase::IPluginGame *m_GamePlugin;
- ModDataContentHolder m_Contents;
-
- std::unique_ptr<Profile> m_CurrentProfile;
-
- Settings& m_Settings;
-
- SelfUpdater m_Updater;
-
- SignalAboutToRunApplication m_AboutToRun;
- SignalFinishedRunApplication m_FinishedRun;
- SignalUserInterfaceInitialized m_UserInterfaceInitialized;
- SignalProfileCreated m_ProfileCreated;
- SignalProfileRenamed m_ProfileRenamed;
- SignalProfileRemoved m_ProfileRemoved;
- SignalProfileChanged m_ProfileChanged;
- SignalPluginSettingChanged m_PluginSettingChanged;
- SignalPluginEnabled m_PluginEnabled;
- SignalPluginEnabled m_PluginDisabled;
-
- ModList m_ModList;
- PluginList m_PluginList;
-
-
- QList<std::function<void()>> m_PostLoginTasks;
- QList<std::function<void()>> m_PostRefreshTasks;
-
- ExecutablesList m_ExecutablesList;
- QStringList m_PendingDownloads;
- QStringList m_DefaultArchives;
- QStringList m_ActiveArchives;
-
- std::unique_ptr<DirectoryRefresher> m_DirectoryRefresher;
- MOShared::DirectoryEntry *m_DirectoryStructure;
- MOBase::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_VirtualFileTree;
-
- DownloadManager m_DownloadManager;
- InstallationManager m_InstallationManager;
-
- QThread m_RefresherThread;
-
- std::thread m_StructureDeleter;
-
- bool m_DirectoryUpdate;
- bool m_ArchivesInit;
-
- MOBase::DelayedFileWriter m_PluginListsWriter;
- UsvfsConnector m_USVFS;
-
- UILocker m_UILocker;
-};
-
-#endif // ORGANIZERCORE_H
+#ifndef ORGANIZERCORE_H
+#define ORGANIZERCORE_H
+
+#include "selfupdater.h"
+#include "settings.h"
+#include "modlist.h"
+#include "modinfo.h"
+#include "pluginlist.h"
+#include "installationmanager.h"
+#include "downloadmanager.h"
+#include "executableslist.h"
+#include "usvfsconnector.h"
+#include "guessedvalue.h"
+#include "moshortcut.h"
+#include "memoizedlock.h"
+#include "processrunner.h"
+#include "uilocker.h"
+#include "envdump.h"
+#include <imoinfo.h>
+#include <iplugindiagnose.h>
+#include <versioninfo.h>
+#include <delayedfilewriter.h>
+#include <boost/signals2.hpp>
+#include "executableinfo.h"
+#include "moddatacontent.h"
+#include <log.h>
+
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QSettings>
+#include <QString>
+#include <QStringList>
+#include <QThread>
+#include <QVariant>
+
+class ModListSortProxy;
+class PluginListSortProxy;
+class Profile;
+class IUserInterface;
+class PluginContainer;
+class DirectoryRefresher;
+
+namespace MOBase
+{
+ template <typename T> class GuessedValue;
+ class IModInterface;
+ class IPluginGame;
+}
+
+namespace MOShared
+{
+ class DirectoryEntry;
+}
+
+
+class OrganizerCore : public QObject, public MOBase::IPluginDiagnose
+{
+
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPluginDiagnose)
+
+private:
+
+ friend class OrganizerProxy;
+
+ struct SignalCombinerAnd
+ {
+ using result_type = bool;
+
+ template<typename InputIterator>
+ bool operator()(InputIterator first, InputIterator last) const
+ {
+ while (first != last) {
+ if (!(*first)) {
+ return false;
+ }
+ ++first;
+ }
+ return true;
+ }
+ };
+
+private:
+
+ using SignalAboutToRunApplication = boost::signals2::signal<bool(const QString&), SignalCombinerAnd>;
+ using SignalFinishedRunApplication = boost::signals2::signal<void(const QString&, unsigned int)>;
+ using SignalUserInterfaceInitialized = boost::signals2::signal<void(QMainWindow*)>;
+ using SignalProfileCreated = boost::signals2::signal<void(MOBase::IProfile*)>;
+ using SignalProfileRenamed = boost::signals2::signal<void(MOBase::IProfile*, QString const&, QString const&)>;
+ using SignalProfileRemoved = boost::signals2::signal<void(QString const&)>;
+ using SignalProfileChanged = boost::signals2::signal<void(MOBase::IProfile *, MOBase::IProfile *)>;
+ using SignalPluginSettingChanged = boost::signals2::signal<void(QString const&, const QString& key, const QVariant&, const QVariant&)>;
+ using SignalPluginEnabled = boost::signals2::signal<void(const MOBase::IPlugin*)>;
+
+public:
+
+ /**
+ * Small holder for the game content returned by the ModDataContent feature (the
+ * list of all possible contents, not the per-mod content).
+ */
+ struct ModDataContentHolder {
+
+ using Content = ModDataContent::Content;
+
+ /**
+ * @return true if the hold list of contents is empty, false otherwise.
+ */
+ bool empty() const { return m_Contents.empty(); }
+
+ /**
+ * @param id ID of the content to retrieve.
+ *
+ * @return the content with the given ID, or a null pointer if it is not found.
+ */
+ const Content* findById(int id) const {
+ auto it = std::find_if(std::begin(m_Contents), std::end(m_Contents), [&id](auto const& content) { return content.id() == id; });
+ return it == std::end(m_Contents) ? nullptr : &(*it);
+ }
+
+ /**
+ * Apply the given function to each content whose ID is in the given set.
+ *
+ * @param ids The set of content IDs.
+ * @param fn The function to apply.
+ * @param includeFilter true to also apply the function to filter-only contents, false otherwise.
+ */
+ template <class Fn>
+ void forEachContentIn(std::set<int> const& ids, Fn const& fn, bool includeFilter = false) const {
+ for (const auto& content : m_Contents) {
+ if ((includeFilter || !content.isOnlyForFilter())
+ && ids.find(content.id()) != ids.end()) {
+ fn(content);
+ }
+ }
+ }
+
+ /**
+ * @brief Apply fnIn to each content whose ID is in the given set, and fnOut to each content not in the
+ * given set, excluding filter-only content (from both cases) unless includeFilter is true.
+ *
+ * @param ids The set of content IDs.
+ * @param fnIn Function to apply to content whose IDs are in ids.
+ * @param fnOut Function to apply to content whose IDs are not in ids.
+ * @param includeFilter true to also apply the function to filter-only contents, false otherwise.
+ */
+ template <class FnIn, class FnOut>
+ void forEachContentInOrOut(std::set<int> const& ids, FnIn const& fnIn, FnOut const& fnOut, bool includeFilter = false) const {
+ for (const auto& content : m_Contents) {
+ if ((includeFilter || !content.isOnlyForFilter())) {
+ if (ids.find(content.id()) != ids.end()) {
+ fnIn(content);
+ }
+ else {
+ fnOut(content);
+ }
+ }
+ }
+ }
+
+ /**
+ * Apply the given function to each content.
+ *
+ * @param fn The function to apply.
+ * @param includeFilter true to also apply the function to filter-only contents, false otherwise.
+ */
+ template <class Fn>
+ void forEachContent(Fn const& fn, bool includeFilter = false) const {
+ for (const auto& content : m_Contents) {
+ if (includeFilter || !content.isOnlyForFilter()) {
+ fn(content);
+ }
+ }
+ }
+
+
+ ModDataContentHolder& operator=(ModDataContentHolder const&) = delete;
+ ModDataContentHolder& operator=(ModDataContentHolder&&) = default;
+
+ private:
+
+ std::vector<Content> m_Contents;
+
+ /**
+ * @brief Construct a ModDataContentHolder without any contents (e.g., if the feature is
+ * missing).
+ */
+ ModDataContentHolder() { }
+
+ /**
+ * @brief Construct a ModDataContentHold holding the given list of contents.
+ */
+ ModDataContentHolder(std::vector<ModDataContent::Content> contents) :
+ m_Contents(std::move(contents)) { }
+
+ friend class OrganizerCore;
+ };
+
+public:
+ OrganizerCore(Settings &settings);
+
+ ~OrganizerCore();
+
+ void setUserInterface(IUserInterface* ui);
+ void connectPlugins(PluginContainer *container);
+
+ void setManagedGame(MOBase::IPluginGame *game);
+
+ void updateExecutablesList();
+ void updateModInfoFromDisc();
+
+ void checkForUpdates();
+ void startMOUpdate();
+
+ Settings &settings();
+ SelfUpdater *updater() { return &m_Updater; }
+ InstallationManager *installationManager();
+ MOShared::DirectoryEntry *directoryStructure() { return m_DirectoryStructure; }
+ DirectoryRefresher *directoryRefresher() { return m_DirectoryRefresher.get(); }
+ ExecutablesList *executablesList() { return &m_ExecutablesList; }
+ void setExecutablesList(const ExecutablesList &executablesList) {
+ m_ExecutablesList = executablesList;
+ }
+
+ Profile *currentProfile() const { return m_CurrentProfile.get(); }
+ void setCurrentProfile(const QString &profileName);
+
+ std::vector<QString> enabledArchives();
+
+ MOBase::VersionInfo getVersion() const { return m_Updater.getVersion(); }
+
+ // return the plugin container
+ //
+ PluginContainer& pluginContainer() const;
+
+ MOBase::IPluginGame const *managedGame() const;
+
+ /**
+ * @brief Retrieve the organizer proxy of the currently managed game.
+ *
+ */
+ MOBase::IOrganizer const* managedGameOrganizer() const;
+
+
+ /**
+ * @return the list of contents for the currently managed game, or an empty vector
+ * if the game plugin does not implement the ModDataContent feature.
+ */
+ const ModDataContentHolder& modDataContents() const { return m_Contents; }
+
+ bool isArchivesInit() const { return m_ArchivesInit; }
+
+ bool saveCurrentLists();
+
+ ProcessRunner processRunner();
+
+ bool beforeRun(
+ const QFileInfo& binary, const QString& profileName,
+ const QString& customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries);
+
+ void afterRun(const QFileInfo& binary, DWORD exitCode);
+
+ ProcessRunner::Results waitForAllUSVFSProcesses(
+ UILocker::Reasons reason=UILocker::PreventExit);
+
+ void refreshESPList(bool force = false);
+ void refreshBSAList();
+
+ void refreshDirectoryStructure();
+ void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
+ void updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::Ptr> modInfos);
+
+ void doAfterLogin(const std::function<void()> &function) { m_PostLoginTasks.append(function); }
+ void loggedInAction(QWidget* parent, std::function<void ()> f);
+
+ bool previewFileWithAlternatives(QWidget* parent, QString filename, int selectedOrigin=-1);
+ bool previewFile(QWidget* parent, const QString& originName, const QString& path);
+
+ void loginSuccessfulUpdate(bool necessary);
+ void loginFailedUpdate(const QString &message);
+
+ static bool createAndMakeWritable(const QString &path);
+ bool checkPathSymlinks();
+ bool bootstrap();
+ void createDefaultProfile();
+
+ MOBase::DelayedFileWriter &pluginsWriter() { return m_PluginListsWriter; }
+
+ void prepareVFS();
+
+ void updateVFSParams(
+ MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType,
+ const QString& coreDumpsPath, std::chrono::seconds spawnDelay,
+ QString executableBlacklist);
+
+ void setLogLevel(MOBase::log::Levels level);
+
+ bool cycleDiagnostics();
+
+ static env::CoreDumpTypes getGlobalCoreDumpType();
+ static void setGlobalCoreDumpType(env::CoreDumpTypes type);
+ static std::wstring getGlobalCoreDumpPath();
+
+public:
+ MOBase::IModRepositoryBridge *createNexusBridge() const;
+ QString profileName() const;
+ QString profilePath() const;
+ QString downloadsPath() const;
+ QString overwritePath() const;
+ QString basePath() const;
+ QString modsPath() const;
+ MOBase::VersionInfo appVersion() const;
+ MOBase::IPluginGame *getGame(const QString &gameName) const;
+ MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name);
+ void modDataChanged(MOBase::IModInterface *mod);
+ QVariant pluginSetting(const QString &pluginName, const QString &key) const;
+ void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value);
+ QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def) const;
+ void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync);
+ static QString pluginDataPath();
+ virtual MOBase::IModInterface *installMod(const QString &fileName, int priority, bool reinstallation, ModInfo::Ptr currentMod, const QString &initModName);
+ QString resolvePath(const QString &fileName) const;
+ QStringList listDirectories(const QString &directoryName) const;
+ QStringList findFiles(const QString &path, const std::function<bool (const QString &)> &filter) const;
+ QStringList getFileOrigins(const QString &fileName) const;
+ QList<MOBase::IOrganizer::FileInfo> findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const;
+ DownloadManager *downloadManager();
+ PluginList *pluginList();
+ ModList *modList();
+ void refresh(bool saveChanges = true);
+
+ boost::signals2::connection onAboutToRun(const std::function<bool(const QString&)>& func);
+ boost::signals2::connection onFinishedRun(const std::function<void(const QString&, unsigned int)>& func);
+ boost::signals2::connection onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func);
+ boost::signals2::connection onProfileCreated(std::function<void(MOBase::IProfile*)> const& func);
+ boost::signals2::connection onProfileRenamed(std::function<void(MOBase::IProfile*, QString const&, QString const&)> const& func);
+ boost::signals2::connection onProfileRemoved(std::function<void(QString const&)> const& func);
+ boost::signals2::connection onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func);
+ boost::signals2::connection onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func);
+ boost::signals2::connection onPluginEnabled(std::function<void(const MOBase::IPlugin*)> const& func);
+ boost::signals2::connection onPluginDisabled(std::function<void(const MOBase::IPlugin*)> const& func);
+
+public: // IPluginDiagnose interface
+
+ virtual std::vector<unsigned int> activeProblems() const;
+ virtual QString shortDescription(unsigned int key) const;
+ virtual QString fullDescription(unsigned int key) const;
+ virtual bool hasGuidedFix(unsigned int key) const;
+ virtual void startGuidedFix(unsigned int key) const;
+
+public slots:
+
+ void profileRefresh();
+
+ void syncOverwrite();
+
+ void savePluginList();
+
+ void refreshLists();
+
+ ModInfo::Ptr installDownload(int downloadIndex, int priority = -1);
+ ModInfo::Ptr installArchive(const QString& archivePath, int priority = -1, bool reinstallation = false,
+ ModInfo::Ptr currentMod = nullptr, const QString& modName = QString());
+
+ void modPrioritiesChanged(QModelIndexList const& indexes);
+ void modStatusChanged(unsigned int index);
+ void modStatusChanged(QList<unsigned int> index);
+ void requestDownload(const QUrl &url, QNetworkReply *reply);
+ void downloadRequestedNXM(const QString &url);
+
+ void userInterfaceInitialized();
+
+ void profileCreated(MOBase::IProfile* profile);
+ void profileRenamed(MOBase::IProfile* profile, QString const& oldName, QString const& newName);
+ void profileRemoved(QString const& profileName);
+
+ bool nexusApi(bool retry = false);
+
+signals:
+
+ // emitted after a mod has been installed
+ //
+ void modInstalled(const QString &modName);
+
+ // emitted when the managed game changes
+ //
+ void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
+
+ // emitted when the profile is changed, before notifying plugins
+ //
+ // the new profile can be stored but the old one is temporary and
+ // should not be
+ //
+ void profileChanged(Profile* oldProfile, Profile* newProfile);
+
+ // Notify that the directory structure is ready to be used on the main thread
+ // Use queued connections
+ void directoryStructureReady();
+
+private:
+
+ std::pair<unsigned int, ModInfo::Ptr> doInstall(const QString& archivePath,
+ MOBase::GuessedValue<QString> modName, ModInfo::Ptr currentMod, int priority, bool reinstallation);
+
+ void saveCurrentProfile();
+ void storeSettings();
+
+ void updateModActiveState(int index, bool active);
+ void updateModsActiveState(const QList<unsigned int> &modIndices, bool active);
+
+ // clear the conflict caches of all the given mods, and the mods in conflict
+ // with the given mods
+ //
+ void clearCaches(std::vector<unsigned int> const& indices) const;
+
+ bool createDirectory(const QString &path);
+
+ QString oldMO1HookDll() const;
+
+ /**
+ * @brief return a descriptor of the mappings real file->virtual file
+ */
+ std::vector<Mapping> fileMapping(const QString &profile,
+ const QString &customOverwrite);
+
+ std::vector<Mapping>
+ fileMapping(const QString &dataPath, const QString &relPath,
+ const MOShared::DirectoryEntry *base,
+ const MOShared::DirectoryEntry *directoryEntry,
+ int createDestination);
+
+private slots:
+
+ void directory_refreshed();
+ void downloadRequested(QNetworkReply *reply, QString gameName, int modID, const QString &fileName);
+ void removeOrigin(const QString &name);
+ void downloadSpeed(const QString &serverName, int bytesPerSecond);
+ void loginSuccessful(bool necessary);
+ void loginFailed(const QString &message);
+
+private:
+ static const unsigned int PROBLEM_MO1SCRIPTEXTENDERWORKAROUND = 1;
+
+private:
+ IUserInterface* m_UserInterface;
+ PluginContainer *m_PluginContainer;
+ QString m_GameName;
+ MOBase::IPluginGame *m_GamePlugin;
+ ModDataContentHolder m_Contents;
+
+ std::unique_ptr<Profile> m_CurrentProfile;
+
+ Settings& m_Settings;
+
+ SelfUpdater m_Updater;
+
+ SignalAboutToRunApplication m_AboutToRun;
+ SignalFinishedRunApplication m_FinishedRun;
+ SignalUserInterfaceInitialized m_UserInterfaceInitialized;
+ SignalProfileCreated m_ProfileCreated;
+ SignalProfileRenamed m_ProfileRenamed;
+ SignalProfileRemoved m_ProfileRemoved;
+ SignalProfileChanged m_ProfileChanged;
+ SignalPluginSettingChanged m_PluginSettingChanged;
+ SignalPluginEnabled m_PluginEnabled;
+ SignalPluginEnabled m_PluginDisabled;
+
+ ModList m_ModList;
+ PluginList m_PluginList;
+
+
+ QList<std::function<void()>> m_PostLoginTasks;
+ QList<std::function<void()>> m_PostRefreshTasks;
+
+ ExecutablesList m_ExecutablesList;
+ QStringList m_PendingDownloads;
+ QStringList m_DefaultArchives;
+ QStringList m_ActiveArchives;
+
+ std::unique_ptr<DirectoryRefresher> m_DirectoryRefresher;
+ MOShared::DirectoryEntry *m_DirectoryStructure;
+ MOBase::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_VirtualFileTree;
+
+ DownloadManager m_DownloadManager;
+ InstallationManager m_InstallationManager;
+
+ QThread m_RefresherThread;
+
+ std::thread m_StructureDeleter;
+
+ bool m_DirectoryUpdate;
+ bool m_ArchivesInit;
+
+ MOBase::DelayedFileWriter m_PluginListsWriter;
+ UsvfsConnector m_USVFS;
+
+ UILocker m_UILocker;
+};
+
+#endif // ORGANIZERCORE_H
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp
index e9fe61c2..d5118421 100644
--- a/src/organizerproxy.cpp
+++ b/src/organizerproxy.cpp
@@ -1,375 +1,375 @@
-#include "organizerproxy.h"
-
-#include "shared/appconfig.h"
-#include "organizercore.h"
-#include "plugincontainer.h"
-#include "settings.h"
-#include "glob_matching.h"
-#include "downloadmanagerproxy.h"
-#include "modlistproxy.h"
-#include "pluginlistproxy.h"
-#include "proxyutils.h"
-#include "shared/util.h"
-
-#include <QObject>
-#include <QApplication>
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-OrganizerProxy::OrganizerProxy(OrganizerCore* organizer, PluginContainer* pluginContainer, MOBase::IPlugin* plugin)
- : m_Proxied(organizer)
- , m_PluginContainer(pluginContainer)
- , m_Plugin(plugin)
- , m_DownloadManagerProxy(std::make_unique<DownloadManagerProxy>(this, organizer->downloadManager()))
- , m_ModListProxy(std::make_unique<ModListProxy>(this, organizer->modList()))
- , m_PluginListProxy(std::make_unique<PluginListProxy>(this, organizer->pluginList())) { }
-
-OrganizerProxy::~OrganizerProxy()
-{
- disconnectSignals();
-}
-
-void OrganizerProxy::connectSignals()
-{
- m_Connections.push_back(m_Proxied->onAboutToRun(callSignalIfPluginActive(this, m_AboutToRun, true)));
- m_Connections.push_back(m_Proxied->onFinishedRun(callSignalIfPluginActive(this, m_FinishedRun)));
- m_Connections.push_back(m_Proxied->onProfileCreated(callSignalIfPluginActive(this, m_ProfileCreated)));
- m_Connections.push_back(m_Proxied->onProfileRenamed(callSignalIfPluginActive(this, m_ProfileRenamed)));
- m_Connections.push_back(m_Proxied->onProfileRemoved(callSignalIfPluginActive(this, m_ProfileRemoved)));
- m_Connections.push_back(m_Proxied->onProfileChanged(callSignalIfPluginActive(this, m_ProfileChanged)));
-
- m_Connections.push_back(m_Proxied->onUserInterfaceInitialized(callSignalAlways(m_UserInterfaceInitialized)));
- m_Connections.push_back(m_Proxied->onPluginSettingChanged(callSignalAlways(m_PluginSettingChanged)));
- m_Connections.push_back(m_Proxied->onPluginEnabled(callSignalAlways(m_PluginEnabled)));
- m_Connections.push_back(m_Proxied->onPluginDisabled(callSignalAlways(m_PluginDisabled)));
-
- // Connect the child proxies.
- m_DownloadManagerProxy->connectSignals();
- m_ModListProxy->connectSignals();
- m_PluginListProxy->connectSignals();
-}
-
-void OrganizerProxy::disconnectSignals()
-{
- // Disconnect the child proxies.
- m_DownloadManagerProxy->disconnectSignals();
- m_ModListProxy->disconnectSignals();
- m_PluginListProxy->disconnectSignals();
-
- for (auto& conn : m_Connections) {
- conn.disconnect();
- }
- m_Connections.clear();
-}
-
-IModRepositoryBridge *OrganizerProxy::createNexusBridge() const
-{
- return new NexusBridge(m_PluginContainer, m_Plugin->name());
-}
-
-QString OrganizerProxy::profileName() const
-{
- return m_Proxied->profileName();
-}
-
-QString OrganizerProxy::profilePath() const
-{
- return m_Proxied->profilePath();
-}
-
-QString OrganizerProxy::downloadsPath() const
-{
- return m_Proxied->downloadsPath();
-}
-
-QString OrganizerProxy::overwritePath() const
-{
- return m_Proxied->overwritePath();
-}
-
-QString OrganizerProxy::basePath() const
-{
- return m_Proxied->basePath();
-}
-
-QString OrganizerProxy::modsPath() const
-{
- return m_Proxied->modsPath();
-}
-
-VersionInfo OrganizerProxy::appVersion() const
-{
- return m_Proxied->appVersion();
-}
-
-IPluginGame *OrganizerProxy::getGame(const QString &gameName) const
-{
- return m_Proxied->getGame(gameName);
-}
-
-IModInterface *OrganizerProxy::createMod(MOBase::GuessedValue<QString> &name)
-{
- return m_Proxied->createMod(name);
-}
-
-void OrganizerProxy::modDataChanged(IModInterface *mod)
-{
- m_Proxied->modDataChanged(mod);
-}
-
-bool OrganizerProxy::isPluginEnabled(QString const& pluginName) const
-{
- return m_PluginContainer->isEnabled(pluginName);
-}
-
-bool OrganizerProxy::isPluginEnabled(IPlugin* plugin) const
-{
- return m_PluginContainer->isEnabled(plugin);
-}
-
-QVariant OrganizerProxy::pluginSetting(const QString &pluginName, const QString &key) const
-{
- return m_Proxied->pluginSetting(pluginName, key);
-}
-
-void OrganizerProxy::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value)
-{
- m_Proxied->setPluginSetting(pluginName, key, value);
-}
-
-QVariant OrganizerProxy::persistent(const QString &pluginName, const QString &key, const QVariant &def) const
-{
- return m_Proxied->persistent(pluginName, key, def);
-}
-
-void OrganizerProxy::setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync)
-{
- m_Proxied->setPersistent(pluginName, key, value, sync);
-}
-
-QString OrganizerProxy::pluginDataPath() const
-{
- return OrganizerCore::pluginDataPath();
-}
-
-HANDLE OrganizerProxy::startApplication(
- const QString& exe, const QStringList& args, const QString &cwd,
- const QString& profile, const QString &overwrite, bool ignoreOverwrite)
-{
- log::debug(
- "a plugin has requested to start an application:\n"
- " . executable: '{}'\n"
- " . args: '{}'\n"
- " . cwd: '{}'\n"
- " . profile: '{}'\n"
- " . overwrite: '{}'\n"
- " . ignore overwrite: {}",
- exe, args.join(" "), cwd, profile, overwrite, ignoreOverwrite);
-
- auto runner = m_Proxied->processRunner();
-
- // don't wait for completion
- runner
- .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite)
- .run();
-
- // the plugin is in charge of closing the handle, unless waitForApplication()
- // is called on it
- return runner.stealProcessHandle().release();
-}
-
-bool OrganizerProxy::waitForApplication(HANDLE handle, bool refresh, LPDWORD exitCode) const
-{
- const auto pid = ::GetProcessId(handle);
-
- log::debug(
- "a plugin wants to wait for an application to complete, pid {}{}",
- pid, (pid == 0 ? "unknown (probably already completed)" : ""));
-
- auto runner = m_Proxied->processRunner();
-
- ProcessRunner::WaitFlags waitFlags = ProcessRunner::ForceWait;
-
- if (refresh) {
- waitFlags |= ProcessRunner::TriggerRefresh | ProcessRunner::WaitForRefresh;
- }
-
- const auto r = runner
- .setWaitForCompletion(waitFlags, UILocker::OutputRequired)
- .attachToProcess(handle);
-
- if (exitCode) {
- *exitCode = runner.exitCode();
- }
-
- switch (r)
- {
- case ProcessRunner::Completed:
- return true;
-
- case ProcessRunner::Cancelled: // fall-through
- case ProcessRunner::ForceUnlocked:
- // this is always an error because the application should have run to
- // completion
- return false;
-
- case ProcessRunner::Error: // fall-through
- default:
- return false;
- }
-}
-
-void OrganizerProxy::refresh(bool saveChanges)
-{
- m_Proxied->refresh(saveChanges);
-}
-
-IModInterface *OrganizerProxy::installMod(const QString &fileName, const QString &nameSuggestion)
-{
- return m_Proxied->installMod(fileName, -1, false, nullptr, nameSuggestion);
-}
-
-QString OrganizerProxy::resolvePath(const QString &fileName) const
-{
- return m_Proxied->resolvePath(fileName);
-}
-
-QStringList OrganizerProxy::listDirectories(const QString &directoryName) const
-{
- return m_Proxied->listDirectories(directoryName);
-}
-
-QStringList OrganizerProxy::findFiles(const QString &path, const std::function<bool(const QString&)> &filter) const
-{
- return m_Proxied->findFiles(path, filter);
-}
-
-QStringList OrganizerProxy::findFiles(const QString& path, const QStringList& globFilters) const
-{
- QList<GlobPattern<QChar>> patterns;
- for (auto& gfilter : globFilters) {
- patterns.append(GlobPattern(gfilter));
- }
- return findFiles(path, [&patterns](const QString& filename) {
- for (auto& p : patterns) {
- if (p.match(filename)) {
- return true;
- }
- }
- return false;
- });
-}
-
-QStringList OrganizerProxy::getFileOrigins(const QString &fileName) const
-{
- return m_Proxied->getFileOrigins(fileName);
-}
-
-QList<MOBase::IOrganizer::FileInfo> OrganizerProxy::findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const
-{
- return m_Proxied->findFileInfos(path, filter);
-}
-
-std::shared_ptr<const MOBase::IFileTree> OrganizerProxy::virtualFileTree() const
-{
- return m_Proxied->m_VirtualFileTree.value();
-}
-
-MOBase::IDownloadManager *OrganizerProxy::downloadManager() const
-{
- return m_DownloadManagerProxy.get();
-}
-
-MOBase::IPluginList *OrganizerProxy::pluginList() const
-{
- return m_PluginListProxy.get();
-}
-
-MOBase::IModList *OrganizerProxy::modList() const
-{
- return m_ModListProxy.get();
-}
-
-MOBase::IProfile *OrganizerProxy::profile() const
-{
- return m_Proxied->currentProfile();
-}
-
-MOBase::IPluginGame const *OrganizerProxy::managedGame() const
-{
- return m_Proxied->managedGame();
-}
-
-// CALLBACKS
-
-bool OrganizerProxy::onAboutToRun(const std::function<bool(const QString&)>& func)
-{
- return m_Proxied->onAboutToRun(MOShared::callIfPluginActive(this, func, true)).connected();
-}
-
-bool OrganizerProxy::onFinishedRun(const std::function<void(const QString&, unsigned int)>& func)
-{
- return m_Proxied->onFinishedRun(MOShared::callIfPluginActive(this, func)).connected();
-}
-
-bool OrganizerProxy::onProfileCreated(std::function<void(IProfile*)> const& func)
-{
- return m_ProfileCreated.connect(func).connected();
-}
-
-bool OrganizerProxy::onProfileRenamed(std::function<void(IProfile*, QString const&, QString const&)> const& func)
-{
- return m_ProfileRenamed.connect(func).connected();
-}
-
-bool OrganizerProxy::onProfileRemoved(std::function<void(QString const&)> const& func)
-{
- return m_ProfileRemoved.connect(func).connected();
-}
-
-bool OrganizerProxy::onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func)
-{
- return m_ProfileChanged.connect(func).connected();
-}
-
-bool OrganizerProxy::onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func)
-{
- // Always call this one to allow plugin to initialize themselves even when not active:
- return m_UserInterfaceInitialized.connect(func).connected();
-}
-
-// Always call these one, otherwise plugin cannot detect they are being enabled / disabled:
-bool OrganizerProxy::onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func)
-{
- return m_PluginSettingChanged.connect(func).connected();
-}
-
-bool OrganizerProxy::onPluginEnabled(std::function<void(const IPlugin*)> const& func)
-{
- return m_PluginEnabled.connect(func).connected();
-}
-
-bool OrganizerProxy::onPluginEnabled(const QString& pluginName, std::function<void()> const& func)
-{
- return onPluginEnabled([=](const IPlugin* plugin) {
- if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) {
- func();
- }
- });
-}
-
-bool OrganizerProxy::onPluginDisabled(std::function<void(const IPlugin*)> const& func)
-{
- return m_PluginDisabled.connect(func).connected();
-}
-
-bool OrganizerProxy::onPluginDisabled(const QString& pluginName, std::function<void()> const& func)
-{
- return onPluginDisabled([=](const IPlugin* plugin) {
- if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) {
- func();
- }
- });
-}
+#include "organizerproxy.h"
+
+#include "shared/appconfig.h"
+#include "organizercore.h"
+#include "plugincontainer.h"
+#include "settings.h"
+#include "glob_matching.h"
+#include "downloadmanagerproxy.h"
+#include "modlistproxy.h"
+#include "pluginlistproxy.h"
+#include "proxyutils.h"
+#include "shared/util.h"
+
+#include <QObject>
+#include <QApplication>
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+OrganizerProxy::OrganizerProxy(OrganizerCore* organizer, PluginContainer* pluginContainer, MOBase::IPlugin* plugin)
+ : m_Proxied(organizer)
+ , m_PluginContainer(pluginContainer)
+ , m_Plugin(plugin)
+ , m_DownloadManagerProxy(std::make_unique<DownloadManagerProxy>(this, organizer->downloadManager()))
+ , m_ModListProxy(std::make_unique<ModListProxy>(this, organizer->modList()))
+ , m_PluginListProxy(std::make_unique<PluginListProxy>(this, organizer->pluginList())) { }
+
+OrganizerProxy::~OrganizerProxy()
+{
+ disconnectSignals();
+}
+
+void OrganizerProxy::connectSignals()
+{
+ m_Connections.push_back(m_Proxied->onAboutToRun(callSignalIfPluginActive(this, m_AboutToRun, true)));
+ m_Connections.push_back(m_Proxied->onFinishedRun(callSignalIfPluginActive(this, m_FinishedRun)));
+ m_Connections.push_back(m_Proxied->onProfileCreated(callSignalIfPluginActive(this, m_ProfileCreated)));
+ m_Connections.push_back(m_Proxied->onProfileRenamed(callSignalIfPluginActive(this, m_ProfileRenamed)));
+ m_Connections.push_back(m_Proxied->onProfileRemoved(callSignalIfPluginActive(this, m_ProfileRemoved)));
+ m_Connections.push_back(m_Proxied->onProfileChanged(callSignalIfPluginActive(this, m_ProfileChanged)));
+
+ m_Connections.push_back(m_Proxied->onUserInterfaceInitialized(callSignalAlways(m_UserInterfaceInitialized)));
+ m_Connections.push_back(m_Proxied->onPluginSettingChanged(callSignalAlways(m_PluginSettingChanged)));
+ m_Connections.push_back(m_Proxied->onPluginEnabled(callSignalAlways(m_PluginEnabled)));
+ m_Connections.push_back(m_Proxied->onPluginDisabled(callSignalAlways(m_PluginDisabled)));
+
+ // Connect the child proxies.
+ m_DownloadManagerProxy->connectSignals();
+ m_ModListProxy->connectSignals();
+ m_PluginListProxy->connectSignals();
+}
+
+void OrganizerProxy::disconnectSignals()
+{
+ // Disconnect the child proxies.
+ m_DownloadManagerProxy->disconnectSignals();
+ m_ModListProxy->disconnectSignals();
+ m_PluginListProxy->disconnectSignals();
+
+ for (auto& conn : m_Connections) {
+ conn.disconnect();
+ }
+ m_Connections.clear();
+}
+
+IModRepositoryBridge *OrganizerProxy::createNexusBridge() const
+{
+ return new NexusBridge(m_PluginContainer, m_Plugin->name());
+}
+
+QString OrganizerProxy::profileName() const
+{
+ return m_Proxied->profileName();
+}
+
+QString OrganizerProxy::profilePath() const
+{
+ return m_Proxied->profilePath();
+}
+
+QString OrganizerProxy::downloadsPath() const
+{
+ return m_Proxied->downloadsPath();
+}
+
+QString OrganizerProxy::overwritePath() const
+{
+ return m_Proxied->overwritePath();
+}
+
+QString OrganizerProxy::basePath() const
+{
+ return m_Proxied->basePath();
+}
+
+QString OrganizerProxy::modsPath() const
+{
+ return m_Proxied->modsPath();
+}
+
+VersionInfo OrganizerProxy::appVersion() const
+{
+ return m_Proxied->appVersion();
+}
+
+IPluginGame *OrganizerProxy::getGame(const QString &gameName) const
+{
+ return m_Proxied->getGame(gameName);
+}
+
+IModInterface *OrganizerProxy::createMod(MOBase::GuessedValue<QString> &name)
+{
+ return m_Proxied->createMod(name);
+}
+
+void OrganizerProxy::modDataChanged(IModInterface *mod)
+{
+ m_Proxied->modDataChanged(mod);
+}
+
+bool OrganizerProxy::isPluginEnabled(QString const& pluginName) const
+{
+ return m_PluginContainer->isEnabled(pluginName);
+}
+
+bool OrganizerProxy::isPluginEnabled(IPlugin* plugin) const
+{
+ return m_PluginContainer->isEnabled(plugin);
+}
+
+QVariant OrganizerProxy::pluginSetting(const QString &pluginName, const QString &key) const
+{
+ return m_Proxied->pluginSetting(pluginName, key);
+}
+
+void OrganizerProxy::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value)
+{
+ m_Proxied->setPluginSetting(pluginName, key, value);
+}
+
+QVariant OrganizerProxy::persistent(const QString &pluginName, const QString &key, const QVariant &def) const
+{
+ return m_Proxied->persistent(pluginName, key, def);
+}
+
+void OrganizerProxy::setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync)
+{
+ m_Proxied->setPersistent(pluginName, key, value, sync);
+}
+
+QString OrganizerProxy::pluginDataPath() const
+{
+ return OrganizerCore::pluginDataPath();
+}
+
+HANDLE OrganizerProxy::startApplication(
+ const QString& exe, const QStringList& args, const QString &cwd,
+ const QString& profile, const QString &overwrite, bool ignoreOverwrite)
+{
+ log::debug(
+ "a plugin has requested to start an application:\n"
+ " . executable: '{}'\n"
+ " . args: '{}'\n"
+ " . cwd: '{}'\n"
+ " . profile: '{}'\n"
+ " . overwrite: '{}'\n"
+ " . ignore overwrite: {}",
+ exe, args.join(" "), cwd, profile, overwrite, ignoreOverwrite);
+
+ auto runner = m_Proxied->processRunner();
+
+ // don't wait for completion
+ runner
+ .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite)
+ .run();
+
+ // the plugin is in charge of closing the handle, unless waitForApplication()
+ // is called on it
+ return runner.stealProcessHandle().release();
+}
+
+bool OrganizerProxy::waitForApplication(HANDLE handle, bool refresh, LPDWORD exitCode) const
+{
+ const auto pid = ::GetProcessId(handle);
+
+ log::debug(
+ "a plugin wants to wait for an application to complete, pid {}{}",
+ pid, (pid == 0 ? "unknown (probably already completed)" : ""));
+
+ auto runner = m_Proxied->processRunner();
+
+ ProcessRunner::WaitFlags waitFlags = ProcessRunner::ForceWait;
+
+ if (refresh) {
+ waitFlags |= ProcessRunner::TriggerRefresh | ProcessRunner::WaitForRefresh;
+ }
+
+ const auto r = runner
+ .setWaitForCompletion(waitFlags, UILocker::OutputRequired)
+ .attachToProcess(handle);
+
+ if (exitCode) {
+ *exitCode = runner.exitCode();
+ }
+
+ switch (r)
+ {
+ case ProcessRunner::Completed:
+ return true;
+
+ case ProcessRunner::Cancelled: // fall-through
+ case ProcessRunner::ForceUnlocked:
+ // this is always an error because the application should have run to
+ // completion
+ return false;
+
+ case ProcessRunner::Error: // fall-through
+ default:
+ return false;
+ }
+}
+
+void OrganizerProxy::refresh(bool saveChanges)
+{
+ m_Proxied->refresh(saveChanges);
+}
+
+IModInterface *OrganizerProxy::installMod(const QString &fileName, const QString &nameSuggestion)
+{
+ return m_Proxied->installMod(fileName, -1, false, nullptr, nameSuggestion);
+}
+
+QString OrganizerProxy::resolvePath(const QString &fileName) const
+{
+ return m_Proxied->resolvePath(fileName);
+}
+
+QStringList OrganizerProxy::listDirectories(const QString &directoryName) const
+{
+ return m_Proxied->listDirectories(directoryName);
+}
+
+QStringList OrganizerProxy::findFiles(const QString &path, const std::function<bool(const QString&)> &filter) const
+{
+ return m_Proxied->findFiles(path, filter);
+}
+
+QStringList OrganizerProxy::findFiles(const QString& path, const QStringList& globFilters) const
+{
+ QList<GlobPattern<QChar>> patterns;
+ for (auto& gfilter : globFilters) {
+ patterns.append(GlobPattern(gfilter));
+ }
+ return findFiles(path, [&patterns](const QString& filename) {
+ for (auto& p : patterns) {
+ if (p.match(filename)) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
+
+QStringList OrganizerProxy::getFileOrigins(const QString &fileName) const
+{
+ return m_Proxied->getFileOrigins(fileName);
+}
+
+QList<MOBase::IOrganizer::FileInfo> OrganizerProxy::findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const
+{
+ return m_Proxied->findFileInfos(path, filter);
+}
+
+std::shared_ptr<const MOBase::IFileTree> OrganizerProxy::virtualFileTree() const
+{
+ return m_Proxied->m_VirtualFileTree.value();
+}
+
+MOBase::IDownloadManager *OrganizerProxy::downloadManager() const
+{
+ return m_DownloadManagerProxy.get();
+}
+
+MOBase::IPluginList *OrganizerProxy::pluginList() const
+{
+ return m_PluginListProxy.get();
+}
+
+MOBase::IModList *OrganizerProxy::modList() const
+{
+ return m_ModListProxy.get();
+}
+
+MOBase::IProfile *OrganizerProxy::profile() const
+{
+ return m_Proxied->currentProfile();
+}
+
+MOBase::IPluginGame const *OrganizerProxy::managedGame() const
+{
+ return m_Proxied->managedGame();
+}
+
+// CALLBACKS
+
+bool OrganizerProxy::onAboutToRun(const std::function<bool(const QString&)>& func)
+{
+ return m_Proxied->onAboutToRun(MOShared::callIfPluginActive(this, func, true)).connected();
+}
+
+bool OrganizerProxy::onFinishedRun(const std::function<void(const QString&, unsigned int)>& func)
+{
+ return m_Proxied->onFinishedRun(MOShared::callIfPluginActive(this, func)).connected();
+}
+
+bool OrganizerProxy::onProfileCreated(std::function<void(IProfile*)> const& func)
+{
+ return m_ProfileCreated.connect(func).connected();
+}
+
+bool OrganizerProxy::onProfileRenamed(std::function<void(IProfile*, QString const&, QString const&)> const& func)
+{
+ return m_ProfileRenamed.connect(func).connected();
+}
+
+bool OrganizerProxy::onProfileRemoved(std::function<void(QString const&)> const& func)
+{
+ return m_ProfileRemoved.connect(func).connected();
+}
+
+bool OrganizerProxy::onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func)
+{
+ return m_ProfileChanged.connect(func).connected();
+}
+
+bool OrganizerProxy::onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func)
+{
+ // Always call this one to allow plugin to initialize themselves even when not active:
+ return m_UserInterfaceInitialized.connect(func).connected();
+}
+
+// Always call these one, otherwise plugin cannot detect they are being enabled / disabled:
+bool OrganizerProxy::onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func)
+{
+ return m_PluginSettingChanged.connect(func).connected();
+}
+
+bool OrganizerProxy::onPluginEnabled(std::function<void(const IPlugin*)> const& func)
+{
+ return m_PluginEnabled.connect(func).connected();
+}
+
+bool OrganizerProxy::onPluginEnabled(const QString& pluginName, std::function<void()> const& func)
+{
+ return onPluginEnabled([=](const IPlugin* plugin) {
+ if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) {
+ func();
+ }
+ });
+}
+
+bool OrganizerProxy::onPluginDisabled(std::function<void(const IPlugin*)> const& func)
+{
+ return m_PluginDisabled.connect(func).connected();
+}
+
+bool OrganizerProxy::onPluginDisabled(const QString& pluginName, std::function<void()> const& func)
+{
+ return onPluginDisabled([=](const IPlugin* plugin) {
+ if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) {
+ func();
+ }
+ });
+}
diff --git a/src/organizerproxy.h b/src/organizerproxy.h
index f419ba59..60e891ed 100644
--- a/src/organizerproxy.h
+++ b/src/organizerproxy.h
@@ -1,130 +1,130 @@
-#ifndef ORGANIZERPROXY_H
-#define ORGANIZERPROXY_H
-
-#include <memory>
-
-#include <iplugin.h>
-#include <imoinfo.h>
-
-#include "organizercore.h"
-
-class PluginContainer;
-class DownloadManagerProxy;
-class ModListProxy;
-class PluginListProxy;
-
-class OrganizerProxy : public MOBase::IOrganizer
-{
-
-public:
-
- OrganizerProxy(OrganizerCore *organizer, PluginContainer *pluginContainer, MOBase::IPlugin *plugin);
- ~OrganizerProxy();
-
-public:
-
- /**
- * @return the plugin corresponding to this proxy.
- */
- MOBase::IPlugin* plugin() const { return m_Plugin; }
-
-public: // IOrganizer interface
-
- virtual MOBase::IModRepositoryBridge *createNexusBridge() const;
- virtual QString profileName() const;
- virtual QString profilePath() const;
- virtual QString downloadsPath() const;
- virtual QString overwritePath() const;
- virtual QString basePath() const;
- virtual QString modsPath() const;
- virtual MOBase::VersionInfo appVersion() const;
- virtual MOBase::IPluginGame *getGame(const QString &gameName) const;
- virtual MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name);
- virtual void modDataChanged(MOBase::IModInterface *mod);
- virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const;
- virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true);
- virtual QString pluginDataPath() const;
- virtual MOBase::IModInterface *installMod(const QString &fileName, const QString &nameSuggestion = QString());
- virtual QString resolvePath(const QString &fileName) const;
- virtual QStringList listDirectories(const QString &directoryName) const;
- virtual QStringList findFiles(const QString &path, const std::function<bool(const QString &)> &filter) const override;
- virtual QStringList findFiles(const QString &path, const QStringList &globFilters) const override;
- virtual QStringList getFileOrigins(const QString &fileName) const override;
- virtual QList<FileInfo> findFileInfos(const QString &path, const std::function<bool(const FileInfo&)> &filter) const override;
- virtual std::shared_ptr<const MOBase::IFileTree> virtualFileTree() const override;
-
- virtual MOBase::IDownloadManager *downloadManager() const;
- virtual MOBase::IPluginList *pluginList() const;
- virtual MOBase::IModList *modList() const;
- virtual MOBase::IProfile *profile() const override;
- virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "",
- const QString &profile = "", const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false);
- virtual bool waitForApplication(HANDLE handle, bool refresh = true, LPDWORD exitCode = nullptr) const;
- virtual void refresh(bool saveChanges);
-
- virtual bool onAboutToRun(const std::function<bool(const QString&)> &func) override;
- virtual bool onFinishedRun(const std::function<void (const QString&, unsigned int)> &func) override;
- virtual bool onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func) override;
- virtual bool onProfileCreated(std::function<void(MOBase::IProfile*)> const& func) override;
- virtual bool onProfileRenamed(std::function<void(MOBase::IProfile*, QString const&, QString const&)> const& func) override;
- virtual bool onProfileRemoved(std::function<void(QString const&)> const& func) override;
- virtual bool onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func) override;
-
- // Plugin related:
- virtual bool isPluginEnabled(QString const& pluginName) const override;
- virtual bool isPluginEnabled(MOBase::IPlugin* plugin) const override;
- virtual QVariant pluginSetting(const QString& pluginName, const QString& key) const override;
- virtual void setPluginSetting(const QString& pluginName, const QString& key, const QVariant& value) override;
- virtual bool onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func) override;
- virtual bool onPluginEnabled(std::function<void(const MOBase::IPlugin*)> const& func) override;
- virtual bool onPluginEnabled(const QString& pluginName, std::function<void()> const& func) override;
- virtual bool onPluginDisabled(std::function<void(const MOBase::IPlugin*)> const& func) override;
- virtual bool onPluginDisabled(const QString& pluginName, std::function<void()> const& func) override;
-
- virtual MOBase::IPluginGame const *managedGame() const;
-
-protected:
-
- // The container needs access to some callbacks to simulate startup.
- friend class PluginContainer;
-
- /**
- * @brief Connect the signals from this proxy and all the child proxies (plugin list, mod
- * list, etc.) to the actual implementation. Before this call, plugins can register signals
- * but they won't be triggered.
- */
- void connectSignals();
-
- /**
- * @brief Disconnect the signals from this proxy and all the child proxies (plugin list, mod
- * list, etc.) from the actual implementation.
- */
- void disconnectSignals();
-
-private:
-
- OrganizerCore *m_Proxied;
- PluginContainer *m_PluginContainer;
-
- MOBase::IPlugin *m_Plugin;
-
- OrganizerCore::SignalAboutToRunApplication m_AboutToRun;
- OrganizerCore::SignalFinishedRunApplication m_FinishedRun;
- OrganizerCore::SignalUserInterfaceInitialized m_UserInterfaceInitialized;
- OrganizerCore::SignalProfileCreated m_ProfileCreated;
- OrganizerCore::SignalProfileRenamed m_ProfileRenamed;
- OrganizerCore::SignalProfileRemoved m_ProfileRemoved;
- OrganizerCore::SignalProfileChanged m_ProfileChanged;
- OrganizerCore::SignalPluginSettingChanged m_PluginSettingChanged;
- OrganizerCore::SignalPluginEnabled m_PluginEnabled;
- OrganizerCore::SignalPluginEnabled m_PluginDisabled;
-
- std::vector<boost::signals2::connection> m_Connections;
-
- std::unique_ptr<DownloadManagerProxy> m_DownloadManagerProxy;
- std::unique_ptr<ModListProxy> m_ModListProxy;
- std::unique_ptr<PluginListProxy> m_PluginListProxy;
-
-};
-
-#endif // ORGANIZERPROXY_H
+#ifndef ORGANIZERPROXY_H
+#define ORGANIZERPROXY_H
+
+#include <memory>
+
+#include <iplugin.h>
+#include <imoinfo.h>
+
+#include "organizercore.h"
+
+class PluginContainer;
+class DownloadManagerProxy;
+class ModListProxy;
+class PluginListProxy;
+
+class OrganizerProxy : public MOBase::IOrganizer
+{
+
+public:
+
+ OrganizerProxy(OrganizerCore *organizer, PluginContainer *pluginContainer, MOBase::IPlugin *plugin);
+ ~OrganizerProxy();
+
+public:
+
+ /**
+ * @return the plugin corresponding to this proxy.
+ */
+ MOBase::IPlugin* plugin() const { return m_Plugin; }
+
+public: // IOrganizer interface
+
+ virtual MOBase::IModRepositoryBridge *createNexusBridge() const;
+ virtual QString profileName() const;
+ virtual QString profilePath() const;
+ virtual QString downloadsPath() const;
+ virtual QString overwritePath() const;
+ virtual QString basePath() const;
+ virtual QString modsPath() const;
+ virtual MOBase::VersionInfo appVersion() const;
+ virtual MOBase::IPluginGame *getGame(const QString &gameName) const;
+ virtual MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name);
+ virtual void modDataChanged(MOBase::IModInterface *mod);
+ virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const;
+ virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true);
+ virtual QString pluginDataPath() const;
+ virtual MOBase::IModInterface *installMod(const QString &fileName, const QString &nameSuggestion = QString());
+ virtual QString resolvePath(const QString &fileName) const;
+ virtual QStringList listDirectories(const QString &directoryName) const;
+ virtual QStringList findFiles(const QString &path, const std::function<bool(const QString &)> &filter) const override;
+ virtual QStringList findFiles(const QString &path, const QStringList &globFilters) const override;
+ virtual QStringList getFileOrigins(const QString &fileName) const override;
+ virtual QList<FileInfo> findFileInfos(const QString &path, const std::function<bool(const FileInfo&)> &filter) const override;
+ virtual std::shared_ptr<const MOBase::IFileTree> virtualFileTree() const override;
+
+ virtual MOBase::IDownloadManager *downloadManager() const;
+ virtual MOBase::IPluginList *pluginList() const;
+ virtual MOBase::IModList *modList() const;
+ virtual MOBase::IProfile *profile() const override;
+ virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "",
+ const QString &profile = "", const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false);
+ virtual bool waitForApplication(HANDLE handle, bool refresh = true, LPDWORD exitCode = nullptr) const;
+ virtual void refresh(bool saveChanges);
+
+ virtual bool onAboutToRun(const std::function<bool(const QString&)> &func) override;
+ virtual bool onFinishedRun(const std::function<void (const QString&, unsigned int)> &func) override;
+ virtual bool onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func) override;
+ virtual bool onProfileCreated(std::function<void(MOBase::IProfile*)> const& func) override;
+ virtual bool onProfileRenamed(std::function<void(MOBase::IProfile*, QString const&, QString const&)> const& func) override;
+ virtual bool onProfileRemoved(std::function<void(QString const&)> const& func) override;
+ virtual bool onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func) override;
+
+ // Plugin related:
+ virtual bool isPluginEnabled(QString const& pluginName) const override;
+ virtual bool isPluginEnabled(MOBase::IPlugin* plugin) const override;
+ virtual QVariant pluginSetting(const QString& pluginName, const QString& key) const override;
+ virtual void setPluginSetting(const QString& pluginName, const QString& key, const QVariant& value) override;
+ virtual bool onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func) override;
+ virtual bool onPluginEnabled(std::function<void(const MOBase::IPlugin*)> const& func) override;
+ virtual bool onPluginEnabled(const QString& pluginName, std::function<void()> const& func) override;
+ virtual bool onPluginDisabled(std::function<void(const MOBase::IPlugin*)> const& func) override;
+ virtual bool onPluginDisabled(const QString& pluginName, std::function<void()> const& func) override;
+
+ virtual MOBase::IPluginGame const *managedGame() const;
+
+protected:
+
+ // The container needs access to some callbacks to simulate startup.
+ friend class PluginContainer;
+
+ /**
+ * @brief Connect the signals from this proxy and all the child proxies (plugin list, mod
+ * list, etc.) to the actual implementation. Before this call, plugins can register signals
+ * but they won't be triggered.
+ */
+ void connectSignals();
+
+ /**
+ * @brief Disconnect the signals from this proxy and all the child proxies (plugin list, mod
+ * list, etc.) from the actual implementation.
+ */
+ void disconnectSignals();
+
+private:
+
+ OrganizerCore *m_Proxied;
+ PluginContainer *m_PluginContainer;
+
+ MOBase::IPlugin *m_Plugin;
+
+ OrganizerCore::SignalAboutToRunApplication m_AboutToRun;
+ OrganizerCore::SignalFinishedRunApplication m_FinishedRun;
+ OrganizerCore::SignalUserInterfaceInitialized m_UserInterfaceInitialized;
+ OrganizerCore::SignalProfileCreated m_ProfileCreated;
+ OrganizerCore::SignalProfileRenamed m_ProfileRenamed;
+ OrganizerCore::SignalProfileRemoved m_ProfileRemoved;
+ OrganizerCore::SignalProfileChanged m_ProfileChanged;
+ OrganizerCore::SignalPluginSettingChanged m_PluginSettingChanged;
+ OrganizerCore::SignalPluginEnabled m_PluginEnabled;
+ OrganizerCore::SignalPluginEnabled m_PluginDisabled;
+
+ std::vector<boost::signals2::connection> m_Connections;
+
+ std::unique_ptr<DownloadManagerProxy> m_DownloadManagerProxy;
+ std::unique_ptr<ModListProxy> m_ModListProxy;
+ std::unique_ptr<PluginListProxy> m_PluginListProxy;
+
+};
+
+#endif // ORGANIZERPROXY_H
diff --git a/src/overwriteinfodialog.cpp b/src/overwriteinfodialog.cpp
index 32aae07a..5bd07d3a 100644
--- a/src/overwriteinfodialog.cpp
+++ b/src/overwriteinfodialog.cpp
@@ -1,275 +1,275 @@
-/*
-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 "overwriteinfodialog.h"
-#include "ui_overwriteinfodialog.h"
-#include "report.h"
-#include "utility.h"
-#include "organizercore.h"
-#include <QMessageBox>
-#include <QMenu>
-#include <QShortcut>
-#include <Shlwapi.h>
-
-using namespace MOBase;
-
-OverwriteInfoDialog::OverwriteInfoDialog(ModInfo::Ptr modInfo, QWidget *parent)
- : QDialog(parent), ui(new Ui::OverwriteInfoDialog), m_FileSystemModel(nullptr),
- m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr)
-{
- ui->setupUi(this);
-
- this->setWindowModality(Qt::NonModal);
-
- m_FileSystemModel = new OverwriteFileSystemModel(this);
- m_FileSystemModel->setReadOnly(false);
- setModInfo(modInfo);
- ui->filesView->setModel(m_FileSystemModel);
- ui->filesView->setRootIndex(m_FileSystemModel->index(modInfo->absolutePath()));
- ui->filesView->setColumnWidth(0, 250);
-
- m_DeleteAction = new QAction(tr("&Delete"), ui->filesView);
- m_RenameAction = new QAction(tr("&Rename"), ui->filesView);
- m_OpenAction = new QAction(tr("&Open"), ui->filesView);
- m_NewFolderAction = new QAction(tr("&New Folder"), ui->filesView);
-
- new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated()));
-
- QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered()));
- QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered()));
- QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered()));
- QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered()));
-}
-
-OverwriteInfoDialog::~OverwriteInfoDialog()
-{
- delete ui;
-}
-
-void OverwriteInfoDialog::showEvent(QShowEvent* e)
-{
- const auto& s = Settings::instance();
-
- s.geometry().restoreGeometry(this);
-
- if (!s.geometry().restoreState(ui->filesView->header())) {
- ui->filesView->sortByColumn(0, Qt::AscendingOrder);
- }
-
- QDialog::showEvent(e);
-}
-
-void OverwriteInfoDialog::done(int r)
-{
- auto& s = Settings::instance();
-
- s.geometry().saveGeometry(this);
- s.geometry().saveState(ui->filesView->header());
-
- QDialog::done(r);
-}
-
-void OverwriteInfoDialog::setModInfo(ModInfo::Ptr modInfo)
-{
- m_ModInfo = modInfo;
- if (QDir(modInfo->absolutePath()).exists()) {
- m_FileSystemModel->setRootPath(modInfo->absolutePath());
- } else {
- throw MyException(tr("mod not found: %1").arg(qUtf8Printable(modInfo->absolutePath())));
- }
-}
-
-bool OverwriteInfoDialog::recursiveDelete(const QModelIndex &index)
-{
- for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) {
- QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index);
- if (m_FileSystemModel->isDir(childIndex)) {
- if (!recursiveDelete(childIndex)) {
- log::error("failed to delete {}", m_FileSystemModel->fileName(childIndex));
- return false;
- }
- } else {
- if (!m_FileSystemModel->remove(childIndex)) {
- log::error("failed to delete {}", m_FileSystemModel->fileName(childIndex));
- return false;
- }
- }
- }
- if (!m_FileSystemModel->remove(index)) {
- log::error("failed to delete {}", m_FileSystemModel->fileName(index));
- return false;
- }
- return true;
-}
-
-
-void OverwriteInfoDialog::deleteFile(const QModelIndex &index)
-{
-
- bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index)
- : m_FileSystemModel->remove(index);
- if (!res) {
- QString fileName = m_FileSystemModel->fileName(index);
- reportError(tr("Failed to delete \"%1\"").arg(fileName));
- }
-}
-
-void OverwriteInfoDialog::delete_activated()
-{
- if (ui->filesView->hasFocus()) {
- QItemSelectionModel *selection = ui->filesView->selectionModel();
-
- if (selection->hasSelection() && selection->selectedRows().count() >= 1) {
-
- if (selection->selectedRows().count() == 0) {
- return;
- }
- else if (selection->selectedRows().count() == 1) {
- QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0));
- if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
- return;
- }
- }
- else {
- if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
- return;
- }
- }
-
- foreach(QModelIndex index, selection->selectedRows()) {
- deleteFile(index);
- }
- }
- }
-}
-
-void OverwriteInfoDialog::deleteTriggered()
-{
- if (m_FileSelection.count() == 0) {
- return;
- } else if (m_FileSelection.count() == 1) {
- QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0));
- if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
- return;
- }
- } else {
- if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
- return;
- }
- }
-
- foreach(QModelIndex index, m_FileSelection) {
- deleteFile(index);
- }
-}
-
-
-void OverwriteInfoDialog::renameTriggered()
-{
- QModelIndex selection = m_FileSelection.at(0);
- QModelIndex index = selection.sibling(selection.row(), 0);
- if (!index.isValid() || m_FileSystemModel->isReadOnly()) {
- return;
- }
-
- ui->filesView->edit(index);
-}
-
-
-void OverwriteInfoDialog::openFile(const QModelIndex &index)
-{
- shell::Open(m_FileSystemModel->filePath(index));
-}
-
-
-void OverwriteInfoDialog::openTriggered()
-{
- foreach(QModelIndex idx, m_FileSelection) {
- openFile(idx);
- }
-}
-
-void OverwriteInfoDialog::createDirectoryTriggered()
-{
- QModelIndex selection = m_FileSelection.at(0);
-
- QModelIndex index = m_FileSystemModel->isDir(selection) ? selection
- : selection.parent();
- index = index.sibling(index.row(), 0);
-
- QString name = tr("New Folder");
- QString path = m_FileSystemModel->filePath(index).append("/");
-
- QModelIndex existingIndex = m_FileSystemModel->index(path + name);
- int suffix = 1;
- while (existingIndex.isValid()) {
- name = tr("New Folder") + QString::number(suffix++);
- existingIndex = m_FileSystemModel->index(path + name);
- }
-
- QModelIndex newIndex = m_FileSystemModel->mkdir(index, name);
- if (!newIndex.isValid()) {
- reportError(tr("Failed to create \"%1\"").arg(name));
- return;
- }
-
- ui->filesView->setCurrentIndex(newIndex);
- ui->filesView->edit(newIndex);
-}
-
-void OverwriteInfoDialog::on_explorerButton_clicked()
-{
- shell::Explore(m_ModInfo->absolutePath());
-}
-
-void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint &pos)
-{
- QItemSelectionModel *selectionModel = ui->filesView->selectionModel();
- m_FileSelection = selectionModel->selectedRows(0);
-
- QMenu menu(ui->filesView);
-
- menu.addAction(m_NewFolderAction);
-
- bool hasFiles = false;
-
- foreach(QModelIndex idx, m_FileSelection) {
- if (m_FileSystemModel->fileInfo(idx).isFile()) {
- hasFiles = true;
- break;
- }
- }
-
- if (selectionModel->hasSelection()) {
- if (hasFiles) {
- menu.addAction(m_OpenAction);
- }
- menu.addAction(m_RenameAction);
- menu.addAction(m_DeleteAction);
- } else {
- m_FileSelection.clear();
- m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0));
- }
-
- menu.exec(ui->filesView->viewport()->mapToGlobal(pos));
-}
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "overwriteinfodialog.h"
+#include "ui_overwriteinfodialog.h"
+#include "report.h"
+#include "utility.h"
+#include "organizercore.h"
+#include <QMessageBox>
+#include <QMenu>
+#include <QShortcut>
+#include <Shlwapi.h>
+
+using namespace MOBase;
+
+OverwriteInfoDialog::OverwriteInfoDialog(ModInfo::Ptr modInfo, QWidget *parent)
+ : QDialog(parent), ui(new Ui::OverwriteInfoDialog), m_FileSystemModel(nullptr),
+ m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr)
+{
+ ui->setupUi(this);
+
+ this->setWindowModality(Qt::NonModal);
+
+ m_FileSystemModel = new OverwriteFileSystemModel(this);
+ m_FileSystemModel->setReadOnly(false);
+ setModInfo(modInfo);
+ ui->filesView->setModel(m_FileSystemModel);
+ ui->filesView->setRootIndex(m_FileSystemModel->index(modInfo->absolutePath()));
+ ui->filesView->setColumnWidth(0, 250);
+
+ m_DeleteAction = new QAction(tr("&Delete"), ui->filesView);
+ m_RenameAction = new QAction(tr("&Rename"), ui->filesView);
+ m_OpenAction = new QAction(tr("&Open"), ui->filesView);
+ m_NewFolderAction = new QAction(tr("&New Folder"), ui->filesView);
+
+ new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated()));
+
+ QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered()));
+ QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered()));
+ QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered()));
+ QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered()));
+}
+
+OverwriteInfoDialog::~OverwriteInfoDialog()
+{
+ delete ui;
+}
+
+void OverwriteInfoDialog::showEvent(QShowEvent* e)
+{
+ const auto& s = Settings::instance();
+
+ s.geometry().restoreGeometry(this);
+
+ if (!s.geometry().restoreState(ui->filesView->header())) {
+ ui->filesView->sortByColumn(0, Qt::AscendingOrder);
+ }
+
+ QDialog::showEvent(e);
+}
+
+void OverwriteInfoDialog::done(int r)
+{
+ auto& s = Settings::instance();
+
+ s.geometry().saveGeometry(this);
+ s.geometry().saveState(ui->filesView->header());
+
+ QDialog::done(r);
+}
+
+void OverwriteInfoDialog::setModInfo(ModInfo::Ptr modInfo)
+{
+ m_ModInfo = modInfo;
+ if (QDir(modInfo->absolutePath()).exists()) {
+ m_FileSystemModel->setRootPath(modInfo->absolutePath());
+ } else {
+ throw MyException(tr("mod not found: %1").arg(qUtf8Printable(modInfo->absolutePath())));
+ }
+}
+
+bool OverwriteInfoDialog::recursiveDelete(const QModelIndex &index)
+{
+ for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) {
+ QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index);
+ if (m_FileSystemModel->isDir(childIndex)) {
+ if (!recursiveDelete(childIndex)) {
+ log::error("failed to delete {}", m_FileSystemModel->fileName(childIndex));
+ return false;
+ }
+ } else {
+ if (!m_FileSystemModel->remove(childIndex)) {
+ log::error("failed to delete {}", m_FileSystemModel->fileName(childIndex));
+ return false;
+ }
+ }
+ }
+ if (!m_FileSystemModel->remove(index)) {
+ log::error("failed to delete {}", m_FileSystemModel->fileName(index));
+ return false;
+ }
+ return true;
+}
+
+
+void OverwriteInfoDialog::deleteFile(const QModelIndex &index)
+{
+
+ bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index)
+ : m_FileSystemModel->remove(index);
+ if (!res) {
+ QString fileName = m_FileSystemModel->fileName(index);
+ reportError(tr("Failed to delete \"%1\"").arg(fileName));
+ }
+}
+
+void OverwriteInfoDialog::delete_activated()
+{
+ if (ui->filesView->hasFocus()) {
+ QItemSelectionModel *selection = ui->filesView->selectionModel();
+
+ if (selection->hasSelection() && selection->selectedRows().count() >= 1) {
+
+ if (selection->selectedRows().count() == 0) {
+ return;
+ }
+ else if (selection->selectedRows().count() == 1) {
+ QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0));
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+ else {
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ foreach(QModelIndex index, selection->selectedRows()) {
+ deleteFile(index);
+ }
+ }
+ }
+}
+
+void OverwriteInfoDialog::deleteTriggered()
+{
+ if (m_FileSelection.count() == 0) {
+ return;
+ } else if (m_FileSelection.count() == 1) {
+ QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0));
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ } else {
+ if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ foreach(QModelIndex index, m_FileSelection) {
+ deleteFile(index);
+ }
+}
+
+
+void OverwriteInfoDialog::renameTriggered()
+{
+ QModelIndex selection = m_FileSelection.at(0);
+ QModelIndex index = selection.sibling(selection.row(), 0);
+ if (!index.isValid() || m_FileSystemModel->isReadOnly()) {
+ return;
+ }
+
+ ui->filesView->edit(index);
+}
+
+
+void OverwriteInfoDialog::openFile(const QModelIndex &index)
+{
+ shell::Open(m_FileSystemModel->filePath(index));
+}
+
+
+void OverwriteInfoDialog::openTriggered()
+{
+ foreach(QModelIndex idx, m_FileSelection) {
+ openFile(idx);
+ }
+}
+
+void OverwriteInfoDialog::createDirectoryTriggered()
+{
+ QModelIndex selection = m_FileSelection.at(0);
+
+ QModelIndex index = m_FileSystemModel->isDir(selection) ? selection
+ : selection.parent();
+ index = index.sibling(index.row(), 0);
+
+ QString name = tr("New Folder");
+ QString path = m_FileSystemModel->filePath(index).append("/");
+
+ QModelIndex existingIndex = m_FileSystemModel->index(path + name);
+ int suffix = 1;
+ while (existingIndex.isValid()) {
+ name = tr("New Folder") + QString::number(suffix++);
+ existingIndex = m_FileSystemModel->index(path + name);
+ }
+
+ QModelIndex newIndex = m_FileSystemModel->mkdir(index, name);
+ if (!newIndex.isValid()) {
+ reportError(tr("Failed to create \"%1\"").arg(name));
+ return;
+ }
+
+ ui->filesView->setCurrentIndex(newIndex);
+ ui->filesView->edit(newIndex);
+}
+
+void OverwriteInfoDialog::on_explorerButton_clicked()
+{
+ shell::Explore(m_ModInfo->absolutePath());
+}
+
+void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint &pos)
+{
+ QItemSelectionModel *selectionModel = ui->filesView->selectionModel();
+ m_FileSelection = selectionModel->selectedRows(0);
+
+ QMenu menu(ui->filesView);
+
+ menu.addAction(m_NewFolderAction);
+
+ bool hasFiles = false;
+
+ foreach(QModelIndex idx, m_FileSelection) {
+ if (m_FileSystemModel->fileInfo(idx).isFile()) {
+ hasFiles = true;
+ break;
+ }
+ }
+
+ if (selectionModel->hasSelection()) {
+ if (hasFiles) {
+ menu.addAction(m_OpenAction);
+ }
+ menu.addAction(m_RenameAction);
+ menu.addAction(m_DeleteAction);
+ } else {
+ m_FileSelection.clear();
+ m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0));
+ }
+
+ menu.exec(ui->filesView->viewport()->mapToGlobal(pos));
+}
diff --git a/src/overwriteinfodialog.h b/src/overwriteinfodialog.h
index 6b3ab0ea..c62455fe 100644
--- a/src/overwriteinfodialog.h
+++ b/src/overwriteinfodialog.h
@@ -1,128 +1,128 @@
-/*
-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 OVERWRITEINFODIALOG_H
-#define OVERWRITEINFODIALOG_H
-
-#include "modinfo.h"
-#include <QDialog>
-#include <QFileSystemModel>
-
-namespace Ui {
-class OverwriteInfoDialog;
-}
-
-class OverwriteFileSystemModel : public QFileSystemModel
-{
- Q_OBJECT;
-
-public:
- OverwriteFileSystemModel(QObject *parent)
- : QFileSystemModel(parent), m_RegularColumnCount(0) {}
-
- virtual int columnCount(const QModelIndex &parent) const {
- m_RegularColumnCount = QFileSystemModel::columnCount(parent);
- return m_RegularColumnCount;
- }
-
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const {
- if ((orientation == Qt::Horizontal) &&
- (section >= m_RegularColumnCount)) {
- if (role == Qt::DisplayRole) {
- return tr("Overwrites");
- } else {
- return QVariant();
- }
- } else {
- return QFileSystemModel::headerData(section, orientation, role);
- }
- }
-
- virtual QVariant data(const QModelIndex &index, int role) const {
- if (index.column() == m_RegularColumnCount + 0) {
- if (role == Qt::DisplayRole) {
- return tr("not implemented");
- } else {
- return QVariant();
- }
- } else {
- return QFileSystemModel::data(index, role);
- }
- }
-
-private:
- mutable int m_RegularColumnCount;
-};
-
-
-class OverwriteInfoDialog : public QDialog
-{
- Q_OBJECT
-
-public:
-
- explicit OverwriteInfoDialog(ModInfo::Ptr modInfo, QWidget *parent = 0);
- ~OverwriteInfoDialog();
-
- ModInfo::Ptr modInfo() const { return m_ModInfo; }
-
- // saves geometry
- //
- void done(int r) override;
-
- void setModInfo(ModInfo::Ptr modInfo);
-
-protected:
- // restores geometry
- //
- void showEvent(QShowEvent* e) override;
-
-private:
-
- void openFile(const QModelIndex &index);
- bool recursiveDelete(const QModelIndex &index);
- void deleteFile(const QModelIndex &index);
-
-private slots:
-
- void delete_activated();
-
- void deleteTriggered();
- void renameTriggered();
- void openTriggered();
- void createDirectoryTriggered();
-
- void on_explorerButton_clicked();
- void on_filesView_customContextMenuRequested(const QPoint &pos);
-
-private:
-
- Ui::OverwriteInfoDialog *ui;
- QFileSystemModel *m_FileSystemModel;
- QModelIndexList m_FileSelection;
- QAction *m_DeleteAction;
- QAction *m_RenameAction;
- QAction *m_OpenAction;
- QAction *m_NewFolderAction;
-
- ModInfo::Ptr m_ModInfo;
-
-};
-
-#endif // OVERWRITEINFODIALOG_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 OVERWRITEINFODIALOG_H
+#define OVERWRITEINFODIALOG_H
+
+#include "modinfo.h"
+#include <QDialog>
+#include <QFileSystemModel>
+
+namespace Ui {
+class OverwriteInfoDialog;
+}
+
+class OverwriteFileSystemModel : public QFileSystemModel
+{
+ Q_OBJECT;
+
+public:
+ OverwriteFileSystemModel(QObject *parent)
+ : QFileSystemModel(parent), m_RegularColumnCount(0) {}
+
+ virtual int columnCount(const QModelIndex &parent) const {
+ m_RegularColumnCount = QFileSystemModel::columnCount(parent);
+ return m_RegularColumnCount;
+ }
+
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const {
+ if ((orientation == Qt::Horizontal) &&
+ (section >= m_RegularColumnCount)) {
+ if (role == Qt::DisplayRole) {
+ return tr("Overwrites");
+ } else {
+ return QVariant();
+ }
+ } else {
+ return QFileSystemModel::headerData(section, orientation, role);
+ }
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role) const {
+ if (index.column() == m_RegularColumnCount + 0) {
+ if (role == Qt::DisplayRole) {
+ return tr("not implemented");
+ } else {
+ return QVariant();
+ }
+ } else {
+ return QFileSystemModel::data(index, role);
+ }
+ }
+
+private:
+ mutable int m_RegularColumnCount;
+};
+
+
+class OverwriteInfoDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+
+ explicit OverwriteInfoDialog(ModInfo::Ptr modInfo, QWidget *parent = 0);
+ ~OverwriteInfoDialog();
+
+ ModInfo::Ptr modInfo() const { return m_ModInfo; }
+
+ // saves geometry
+ //
+ void done(int r) override;
+
+ void setModInfo(ModInfo::Ptr modInfo);
+
+protected:
+ // restores geometry
+ //
+ void showEvent(QShowEvent* e) override;
+
+private:
+
+ void openFile(const QModelIndex &index);
+ bool recursiveDelete(const QModelIndex &index);
+ void deleteFile(const QModelIndex &index);
+
+private slots:
+
+ void delete_activated();
+
+ void deleteTriggered();
+ void renameTriggered();
+ void openTriggered();
+ void createDirectoryTriggered();
+
+ void on_explorerButton_clicked();
+ void on_filesView_customContextMenuRequested(const QPoint &pos);
+
+private:
+
+ Ui::OverwriteInfoDialog *ui;
+ QFileSystemModel *m_FileSystemModel;
+ QModelIndexList m_FileSelection;
+ QAction *m_DeleteAction;
+ QAction *m_RenameAction;
+ QAction *m_OpenAction;
+ QAction *m_NewFolderAction;
+
+ ModInfo::Ptr m_ModInfo;
+
+};
+
+#endif // OVERWRITEINFODIALOG_H
diff --git a/src/persistentcookiejar.cpp b/src/persistentcookiejar.cpp
index 8657f356..104a441a 100644
--- a/src/persistentcookiejar.cpp
+++ b/src/persistentcookiejar.cpp
@@ -1,74 +1,74 @@
-#include "persistentcookiejar.h"
-#include <log.h>
-#include <QTemporaryFile>
-#include <QDataStream>
-#include <QNetworkCookie>
-
-using namespace MOBase;
-
-PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *parent)
-: QNetworkCookieJar(parent), m_FileName(fileName)
-{
- restore();
-}
-
-PersistentCookieJar::~PersistentCookieJar() {
- log::debug("save {}", m_FileName);
- save();
-}
-
-void PersistentCookieJar::clear() {
- for (const QNetworkCookie &cookie : allCookies()) {
- deleteCookie(cookie);
- }
-}
-
-void PersistentCookieJar::save() {
- QTemporaryFile file;
- if (!file.open()) {
- log::error("failed to save cookies: couldn't create temporary file");
- return;
- }
- QDataStream data(&file);
-
- QList<QNetworkCookie> cookies = allCookies();
- data << static_cast<quint32>(cookies.size());
-
- for (const QNetworkCookie &cookie : allCookies()) {
- data << cookie.toRawForm();
- }
-
- {
- QFile oldCookies(m_FileName);
- if (oldCookies.exists()) {
- if (!oldCookies.remove()) {
- log::error("failed to save cookies: failed to remove {}", m_FileName);
- return;
- }
- } // if it doesn't exists that's fine
- }
-
- if (!file.copy(m_FileName)) {
- log::error("failed to save cookies: failed to write {}", m_FileName);
- }
-}
-
-void PersistentCookieJar::restore() {
- QFile file(m_FileName);
- if (!file.open(QIODevice::ReadOnly)) {
- // not necessarily a problem, the file may just not exist (yet)
- return;
- }
-
- QList<QNetworkCookie> allCookies;
-
- QDataStream data(&file);
- quint32 count;
- data >> count;
- for (quint32 i = 0; i < count; ++i) {
- QByteArray cookieRaw;
- data >> cookieRaw;
- allCookies.append(QNetworkCookie::parseCookies(cookieRaw));
- }
- setAllCookies(allCookies);
-}
+#include "persistentcookiejar.h"
+#include <log.h>
+#include <QTemporaryFile>
+#include <QDataStream>
+#include <QNetworkCookie>
+
+using namespace MOBase;
+
+PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *parent)
+: QNetworkCookieJar(parent), m_FileName(fileName)
+{
+ restore();
+}
+
+PersistentCookieJar::~PersistentCookieJar() {
+ log::debug("save {}", m_FileName);
+ save();
+}
+
+void PersistentCookieJar::clear() {
+ for (const QNetworkCookie &cookie : allCookies()) {
+ deleteCookie(cookie);
+ }
+}
+
+void PersistentCookieJar::save() {
+ QTemporaryFile file;
+ if (!file.open()) {
+ log::error("failed to save cookies: couldn't create temporary file");
+ return;
+ }
+ QDataStream data(&file);
+
+ QList<QNetworkCookie> cookies = allCookies();
+ data << static_cast<quint32>(cookies.size());
+
+ for (const QNetworkCookie &cookie : allCookies()) {
+ data << cookie.toRawForm();
+ }
+
+ {
+ QFile oldCookies(m_FileName);
+ if (oldCookies.exists()) {
+ if (!oldCookies.remove()) {
+ log::error("failed to save cookies: failed to remove {}", m_FileName);
+ return;
+ }
+ } // if it doesn't exists that's fine
+ }
+
+ if (!file.copy(m_FileName)) {
+ log::error("failed to save cookies: failed to write {}", m_FileName);
+ }
+}
+
+void PersistentCookieJar::restore() {
+ QFile file(m_FileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ // not necessarily a problem, the file may just not exist (yet)
+ return;
+ }
+
+ QList<QNetworkCookie> allCookies;
+
+ QDataStream data(&file);
+ quint32 count;
+ data >> count;
+ for (quint32 i = 0; i < count; ++i) {
+ QByteArray cookieRaw;
+ data >> cookieRaw;
+ allCookies.append(QNetworkCookie::parseCookies(cookieRaw));
+ }
+ setAllCookies(allCookies);
+}
diff --git a/src/persistentcookiejar.h b/src/persistentcookiejar.h
index 0ff747ee..fcf645f4 100644
--- a/src/persistentcookiejar.h
+++ b/src/persistentcookiejar.h
@@ -1,30 +1,30 @@
-#ifndef PERSISTENTCOOKIEJAR_H
-#define PERSISTENTCOOKIEJAR_H
-
-#include <QNetworkCookieJar>
-
-
-class PersistentCookieJar : public QNetworkCookieJar {
-
- Q_OBJECT
-
-public:
- PersistentCookieJar(const QString &fileName, QObject *parent = 0);
- virtual ~PersistentCookieJar();
-
- void clear();
-
-private:
-
- void save();
-
- void restore();
-
-private:
-
- QString m_FileName;
-
-};
-
-
-#endif // PERSISTENTCOOKIEJAR_H
+#ifndef PERSISTENTCOOKIEJAR_H
+#define PERSISTENTCOOKIEJAR_H
+
+#include <QNetworkCookieJar>
+
+
+class PersistentCookieJar : public QNetworkCookieJar {
+
+ Q_OBJECT
+
+public:
+ PersistentCookieJar(const QString &fileName, QObject *parent = 0);
+ virtual ~PersistentCookieJar();
+
+ void clear();
+
+private:
+
+ void save();
+
+ void restore();
+
+private:
+
+ QString m_FileName;
+
+};
+
+
+#endif // PERSISTENTCOOKIEJAR_H
diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp
index f71d89e4..a6455e96 100644
--- a/src/plugincontainer.cpp
+++ b/src/plugincontainer.cpp
@@ -1,1190 +1,1190 @@
-#include "plugincontainer.h"
-#include "organizercore.h"
-#include "organizerproxy.h"
-#include "report.h"
-#include <ipluginproxy.h>
-#include "iuserinterface.h"
-#include <idownloadmanager.h>
-#include "shared/appconfig.h"
-#include <QAction>
-#include <QToolButton>
-#include <QCoreApplication>
-#include <QMessageBox>
-#include <QDirIterator>
-#include <boost/fusion/sequence/intrinsic/at_key.hpp>
-#include <boost/fusion/include/at_key.hpp>
-#include <boost/fusion/algorithm/iteration/for_each.hpp>
-#include <boost/fusion/include/for_each.hpp>
-
-using namespace MOBase;
-using namespace MOShared;
-
-namespace bf = boost::fusion;
-
-// Welcome to the wonderful world of MO2 plugin management!
-//
-// We'll start by the C++ side.
-//
-// There are 9 types of MO2 plugins, two of which cannot be standalone: IPluginDiagnose
-// and IPluginFileMapper. This means that you can have a class implementing IPluginGame,
-// IPluginDiagnose and IPluginFileMapper. It is not possible for a class to implement
-// two full plugin types (e.g. IPluginPreview and IPluginTool).
-//
-// Plugins are fetch as QObject initially and must be "qobject-casted" to the right type.
-//
-// Plugins are stored in the PluginContainer class in various C++ containers: there is a vector
-// that stores all the plugin as QObject, multiple vectors that stores the plugin of each types,
-// a map to find IPlugin object from their names or from IPluginDiagnose or IFileMapper (since
-// these do not inherit IPlugin, they cannot be downcasted).
-//
-// Requirements for plugins are stored in m_Requirements:
-// - IPluginGame cannot be enabled by user. A game plugin is considered enable only if it is
-// the one corresponding to the currently managed games.
-// - If a plugin has a master plugin (IPlugin::master()), it cannot be enabled/disabled by users,
-// and will follow the enabled/disabled state of its parent.
-// - Each plugin has an "enabled" setting stored in persistence. If the setting does not exist,
-// the plugin's enabledByDefault is used instead.
-// - A plugin is considered disabled if the setting is false.
-// - If the setting is true, a plugin is considered disabled if one of its
-// requirements is not met.
-// - Users cannot enable a plugin if one of its requirements is not met.
-//
-// Now let's move to the Proxy side... Or the as of now, the Python side.
-//
-// Proxied plugins are much more annoying because they can implement all interfaces, and are
-// given to MO2 as separate plugins... A Python class implementing IPluginGame and IPluginDiagnose
-// will be seen by MO2 as two separate QObject, and they will all have the same name.
-//
-// When a proxied plugin is registered, a few things must be taken care of:
-// - There can only be one plugin mapped to a name in the PluginContainer class, so we keep the
-// plugin corresponding to the most relevant class (see PluginTypeOrder), e.g. if the class
-// inherits both IPluginGame and IPluginFileMapper, we map the name to the C++ QObject corresponding
-// to the IPluginGame.
-// - When a proxied plugin implements multiple interfaces, the IPlugin corresponding to the most
-// important interface is set as the parent (hidden) of the other IPlugin through PluginRequirements.
-// This way, the plugin are managed together (enabled/disabled state). The "fake" children plugins
-// will not be returned by PluginRequirements::children().
-// - Since each interface corresponds to a different QObject, we need to take care not to call
-// IPlugin::init() on each QObject, but only on the first one.
-//
-// All the proxied plugins are linked to the proxy plugin by PluginRequirements. If the proxy plugin
-// is disabled, the proxied plugins are not even loaded so not visible in the plugin management tab.
-
-template <class T>
-struct PluginTypeName;
-
-template <> struct PluginTypeName<MOBase::IPlugin> { static QString value() { return QT_TR_NOOP("Plugin"); } };
-template <> struct PluginTypeName<MOBase::IPluginDiagnose> { static QString value() { return QT_TR_NOOP("Diagnose"); } };
-template <> struct PluginTypeName<MOBase::IPluginGame> { static QString value() { return QT_TR_NOOP("Game"); } };
-template <> struct PluginTypeName<MOBase::IPluginInstaller> { static QString value() { return QT_TR_NOOP("Installer"); } };
-template <> struct PluginTypeName<MOBase::IPluginModPage> { static QString value() { return QT_TR_NOOP("Mod Page"); } };
-template <> struct PluginTypeName<MOBase::IPluginPreview> { static QString value() { return QT_TR_NOOP("Preview"); } };
-template <> struct PluginTypeName<MOBase::IPluginTool> { static QString value() { return QT_TR_NOOP("Tool"); } };
-template <> struct PluginTypeName<MOBase::IPluginProxy> { static QString value() { return QT_TR_NOOP("Proxy"); } };
-template <> struct PluginTypeName<MOBase::IPluginFileMapper> { static QString value() { return QT_TR_NOOP("File Mapper"); } };
-
-
-QStringList PluginContainer::pluginInterfaces()
-{
- // Find all the names:
- QStringList names;
- boost::mp11::mp_for_each<PluginTypeOrder>([&names](const auto* p) {
- using plugin_type = std::decay_t<decltype(*p)>;
- auto name = PluginTypeName<plugin_type>::value();
- if (!name.isEmpty()) {
- names.append(name);
- }
- });
-
- return names;
-}
-
-
-// PluginRequirementProxy
-
-const std::set<QString> PluginRequirements::s_CorePlugins{
- "INI Bakery"
-};
-
-PluginRequirements::PluginRequirements(
- PluginContainer* pluginContainer, MOBase::IPlugin* plugin, OrganizerProxy* proxy,
- MOBase::IPluginProxy* pluginProxy)
- : m_PluginContainer(pluginContainer)
- , m_Plugin(plugin)
- , m_PluginProxy(pluginProxy)
- , m_Master(nullptr)
- , m_Organizer(proxy)
-{
- // There are a lots of things we cannot set here (e.g. m_Master) because we do not
- // know the order plugins are loaded.
-}
-
-void PluginRequirements::fetchRequirements() {
- m_Requirements = m_Plugin->requirements();
-}
-
-IPluginProxy* PluginRequirements::proxy() const
-{
- return m_PluginProxy;
-}
-
-std::vector<IPlugin*> PluginRequirements::proxied() const
-{
- std::vector<IPlugin*> children;
- if (dynamic_cast<IPluginProxy*>(m_Plugin)) {
- for (auto* obj : m_PluginContainer->plugins<QObject>()) {
- auto* plugin = qobject_cast<IPlugin*>(obj);
- if (plugin && m_PluginContainer->requirements(plugin).proxy() == m_Plugin) {
- children.push_back(plugin);
- }
- }
- }
- return children;
-}
-
-IPlugin* PluginRequirements::master() const
-{
- // If we have a m_Master, it was forced and thus override the default master().
- if (m_Master) {
- return m_Master;
- }
-
- if (m_Plugin->master().isEmpty()) {
- return nullptr;
- }
-
- return m_PluginContainer->plugin(m_Plugin->master());
-}
-
-void PluginRequirements::setMaster(IPlugin* master)
-{
- m_Master = master;
-}
-
-std::vector<IPlugin*> PluginRequirements::children() const
-{
- std::vector<IPlugin*> children;
- for (auto* obj : m_PluginContainer->plugins<QObject>()) {
- auto* plugin = qobject_cast<IPlugin*>(obj);
-
- // Not checking master() but requirements().master() due to "hidden"
- // masters.
- // If the master has the same name as the plugin, this is a "hidden"
- // master, we do not add it here.
- if (plugin
- && m_PluginContainer->requirements(plugin).master() == m_Plugin
- && plugin->name() != m_Plugin->name()) {
- children.push_back(plugin);
- }
- }
- return children;
-}
-
-std::vector<IPluginRequirement::Problem> PluginRequirements::problems() const
-{
- std::vector<IPluginRequirement::Problem> result;
- for (auto& requirement : m_Requirements) {
- if (auto p = requirement->check(m_Organizer)) {
- result.push_back(*p);
- }
- }
- return result;
-}
-
-bool PluginRequirements::canEnable() const
-{
- return problems().empty();
-}
-
-bool PluginRequirements::isCorePlugin() const
-{
- // Let's consider game plugins as "core":
- if (m_PluginContainer->implementInterface<IPluginGame>(m_Plugin)) {
- return true;
- }
-
- return s_CorePlugins.contains(m_Plugin->name());
-}
-
-bool PluginRequirements::hasRequirements() const
-{
- return !m_Requirements.empty();
-}
-
-QStringList PluginRequirements::requiredGames() const
-{
- // We look for a "GameDependencyRequirement" - There can be only one since otherwise
- // it'd mean that the plugin requires two games at once.
- for (auto& requirement : m_Requirements) {
- if (auto* gdep = dynamic_cast<const GameDependencyRequirement*>(requirement.get())) {
- return gdep->gameNames();
- }
- }
-
- return {};
-}
-
-std::vector<MOBase::IPlugin*> PluginRequirements::requiredFor() const
-{
- std::vector<MOBase::IPlugin*> required;
- std::set<MOBase::IPlugin*> visited;
- requiredFor(required, visited);
- return required;
-}
-
-
-void PluginRequirements::requiredFor(std::vector<MOBase::IPlugin*> &required, std::set<MOBase::IPlugin*>& visited) const
-{
- // Handle cyclic dependencies.
- if (visited.contains(m_Plugin)) {
- return;
- }
- visited.insert(m_Plugin);
-
-
- for (auto& [plugin, requirements] : m_PluginContainer->m_Requirements) {
-
- // If the plugin is not enabled, discard:
- if (!m_PluginContainer->isEnabled(plugin)) {
- continue;
- }
-
- // Check the requirements:
- for (auto& requirement : requirements.m_Requirements) {
-
- // We check for plugin dependency. Game dependency are not checked this way.
- if (auto* pdep = dynamic_cast<const PluginDependencyRequirement*>(requirement.get())) {
-
- // Check if at least one of the plugin in the requirements is enabled (except this
- // one):
- bool oneEnabled = false;
- for (auto& pluginName : pdep->pluginNames()) {
- if (pluginName != m_Plugin->name() && m_PluginContainer->isEnabled(pluginName)) {
- oneEnabled = true;
- break;
- }
- }
-
- // No plugin enabled found, so the plugin requires this plugin:
- if (!oneEnabled) {
- required.push_back(plugin);
- requirements.requiredFor(required, visited);
- break;
- }
- }
- }
- }
-}
-
-// PluginContainer
-
-PluginContainer::PluginContainer(OrganizerCore *organizer)
- : m_Organizer(organizer)
- , m_UserInterface(nullptr)
- , m_PreviewGenerator(*this)
-{
-}
-
-PluginContainer::~PluginContainer() {
- m_Organizer = nullptr;
- unloadPlugins();
-}
-
-void PluginContainer::startPlugins(IUserInterface *userInterface)
-{
- m_UserInterface = userInterface;
- startPluginsImpl(plugins<QObject>());
-}
-
-QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const
-{
- // We need a QObject to be able to qobject_cast<> to the plugin types:
- QObject* oPlugin = as_qobject(plugin);
-
- if (!oPlugin) {
- return {};
- }
-
- return implementedInterfaces(oPlugin);
-}
-
-QStringList PluginContainer::implementedInterfaces(QObject * oPlugin) const
-{
- // Find all the names:
- QStringList names;
- boost::mp11::mp_for_each<PluginTypeOrder>([oPlugin, &names](const auto* p) {
- using plugin_type = std::decay_t<decltype(*p)>;
- if (qobject_cast<plugin_type*>(oPlugin)) {
- auto name = PluginTypeName<plugin_type>::value();
- if (!name.isEmpty()) {
- names.append(name);
- }
- }
- });
-
- // If the plugin implements at least one interface other than IPlugin, remove IPlugin:
- if (names.size() > 1) {
- names.removeAll(PluginTypeName<IPlugin>::value());
- }
-
- return names;
-}
-
-QString PluginContainer::topImplementedInterface(IPlugin* plugin) const
-{
- auto interfaces = implementedInterfaces(plugin);
- return interfaces.isEmpty() ? "" : interfaces[0];
-}
-
-bool PluginContainer::isBetterInterface(QObject* lhs, QObject* rhs) const
-{
- int count = 0, lhsIdx = -1, rhsIdx = -1;
- boost::mp11::mp_for_each<PluginTypeOrder>([&](const auto* p) {
- using plugin_type = std::decay_t<decltype(*p)>;
- if (lhsIdx < 0 && qobject_cast<plugin_type*>(lhs)) {
- lhsIdx = count;
- }
- if (rhsIdx < 0 && qobject_cast<plugin_type*>(rhs)) {
- rhsIdx = count;
- }
- ++count;
- });
- return lhsIdx < rhsIdx;
-}
-
-QStringList PluginContainer::pluginFileNames() const
-{
- QStringList result;
- for (QPluginLoader *loader : m_PluginLoaders) {
- result.append(loader->fileName());
- }
- std::vector<IPluginProxy *> proxyList = bf::at_key<IPluginProxy>(m_Plugins);
- for (IPluginProxy *proxy : proxyList) {
- QStringList proxiedPlugins = proxy->pluginList(
- QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
- result.append(proxiedPlugins);
- }
- return result;
-}
-
-QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const
-{
- // Find the correspond QObject - Can this be done safely with a cast?
- auto& objects = bf::at_key<QObject>(m_Plugins);
- auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) {
- return qobject_cast<IPlugin*>(obj) == plugin;
- });
-
- if (it == std::end(objects)) {
- return nullptr;
- }
-
- return *it;
-}
-
-bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy, bool skipInit)
-{
- // when MO has no instance loaded, init() is not called on plugins, except
- // for proxy plugins, where init() is called with a null IOrganizer
- //
- // after proxies are initialized, instantiate() is called for all the plugins
- // they've discovered, but as for regular plugins, init() won't be
- // called on them if m_OrganizerCore is null
-
- if (plugin == nullptr) {
- return false;
- }
-
- OrganizerProxy* proxy = nullptr;
- if (m_Organizer) {
- proxy = new OrganizerProxy(m_Organizer, this, plugin);
- proxy->setParent(as_qobject(plugin));
- }
-
- // Check if it is a proxy plugin:
- bool isProxy = dynamic_cast<IPluginProxy*>(plugin);
-
- auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy));
-
- if (!m_Organizer && !isProxy) {
- return true;
- }
-
- if (skipInit) {
- return true;
- }
-
- if (!plugin->init(proxy)) {
- log::warn("plugin failed to initialize");
- return false;
- }
-
- // Update requirements:
- it->second.fetchRequirements();
-
- return true;
-}
-
-void PluginContainer::registerGame(IPluginGame *game)
-{
- m_SupportedGames.insert({ game->gameName(), game });
-}
-
-void PluginContainer::unregisterGame(MOBase::IPluginGame* game)
-{
- m_SupportedGames.erase(game->gameName());
-}
-
-IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString& filepath, MOBase::IPluginProxy* pluginProxy)
-{
-
- // generic treatment for all plugins
- IPlugin *pluginObj = qobject_cast<IPlugin*>(plugin);
- if (pluginObj == nullptr) {
- log::debug("PluginContainer::registerPlugin() called with a non IPlugin QObject.");
- return nullptr;
- }
-
- // If we already a plugin with this name:
- bool skipInit = false;
- auto& mapNames = bf::at_key<QString>(m_AccessPlugins);
- if (mapNames.contains(pluginObj->name())) {
-
- IPlugin* other = mapNames[pluginObj->name()];
-
- // If both plugins are from the same proxy and the same file, this is usually
- // ok (in theory some one could write two different classes from the same Python file/module):
- if (pluginProxy && m_Requirements.at(other).proxy() == pluginProxy
- && this->filepath(other) == QDir::cleanPath(filepath)) {
-
- // Plugin has already been initialized:
- skipInit = true;
-
- if (isBetterInterface(plugin, as_qobject(other))) {
- log::debug("replacing plugin '{}' with interfaces [{}] by one with interfaces [{}]",
- pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(plugin).join(", "));
- bf::at_key<QString>(m_AccessPlugins)[pluginObj->name()] = pluginObj;
- }
- }
- else {
- log::warn("Trying to register two plugins with the name '{}' (from {} and {}), the second one will not be registered.",
- pluginObj->name(), this->filepath(other), QDir::cleanPath(filepath));
- return nullptr;
- }
- }
- else {
- bf::at_key<QString>(m_AccessPlugins)[pluginObj->name()] = pluginObj;
- }
-
- // Storing the original QObject* is a bit of a hack as I couldn't figure out any
- // way to cast directly between IPlugin* and IPluginDiagnose*
- bf::at_key<QObject>(m_Plugins).push_back(plugin);
-
- plugin->setProperty("filepath", QDir::cleanPath(filepath));
- plugin->setParent(this);
-
- if (m_Organizer) {
- m_Organizer->settings().plugins().registerPlugin(pluginObj);
- }
-
- { // diagnosis plugin
- IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(plugin);
- if (diagnose != nullptr) {
- bf::at_key<IPluginDiagnose>(m_Plugins).push_back(diagnose);
- bf::at_key<IPluginDiagnose>(m_AccessPlugins)[diagnose] = pluginObj;
- diagnose->onInvalidated([&]() { emit diagnosisUpdate(); });
- }
- }
- { // file mapper plugin
- IPluginFileMapper *mapper = qobject_cast<IPluginFileMapper*>(plugin);
- if (mapper != nullptr) {
- bf::at_key<IPluginFileMapper>(m_Plugins).push_back(mapper);
- bf::at_key<IPluginFileMapper>(m_AccessPlugins)[mapper] = pluginObj;
- }
- }
- { // mod page plugin
- IPluginModPage *modPage = qobject_cast<IPluginModPage*>(plugin);
- if (initPlugin(modPage, pluginProxy, skipInit)) {
- bf::at_key<IPluginModPage>(m_Plugins).push_back(modPage);
- emit pluginRegistered(modPage);
- return modPage;
- }
- }
- { // game plugin
- IPluginGame *game = qobject_cast<IPluginGame*>(plugin);
- if (game) {
- game->detectGame();
- if (initPlugin(game, pluginProxy, skipInit)) {
- bf::at_key<IPluginGame>(m_Plugins).push_back(game);
- registerGame(game);
- emit pluginRegistered(game);
- return game;
- }
- }
- }
- { // tool plugins
- IPluginTool *tool = qobject_cast<IPluginTool*>(plugin);
- if (initPlugin(tool, pluginProxy, skipInit)) {
- bf::at_key<IPluginTool>(m_Plugins).push_back(tool);
- emit pluginRegistered(tool);
- return tool;
- }
- }
- { // installer plugins
- IPluginInstaller *installer = qobject_cast<IPluginInstaller*>(plugin);
- if (initPlugin(installer, pluginProxy, skipInit)) {
- bf::at_key<IPluginInstaller>(m_Plugins).push_back(installer);
- if (m_Organizer) {
- installer->setInstallationManager(m_Organizer->installationManager());
- }
- emit pluginRegistered(installer);
- return installer;
- }
- }
- { // preview plugins
- IPluginPreview *preview = qobject_cast<IPluginPreview*>(plugin);
- if (initPlugin(preview, pluginProxy, skipInit)) {
- bf::at_key<IPluginPreview>(m_Plugins).push_back(preview);
- return preview;
- }
- }
- { // proxy plugins
- IPluginProxy *proxy = qobject_cast<IPluginProxy*>(plugin);
- if (initPlugin(proxy, pluginProxy, skipInit)) {
- bf::at_key<IPluginProxy>(m_Plugins).push_back(proxy);
- emit pluginRegistered(proxy);
-
- QStringList filepaths = proxy->pluginList(
- QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
- for (const QString& filepath : filepaths) {
- loadProxied(filepath, proxy);
- }
- return proxy;
- }
- }
-
- { // dummy plugins
- // only initialize these, no processing otherwise
- IPlugin *dummy = qobject_cast<IPlugin*>(plugin);
- if (initPlugin(dummy, pluginProxy, skipInit)) {
- bf::at_key<IPlugin>(m_Plugins).push_back(dummy);
- emit pluginRegistered(dummy);
- return dummy;
- }
- }
-
- return nullptr;
-}
-
-IPlugin* PluginContainer::managedGame() const
-{
- // TODO: This const_cast is safe but ugly. Most methods require a IPlugin*, so
- // returning a const-version if painful. This should be fixed by making methods accept
- // a const IPlugin* instead, but there are a few tricks with qobject_cast and const.
- return const_cast<IPluginGame*>(m_Organizer->managedGame());
-}
-
-bool PluginContainer::isEnabled(IPlugin* plugin) const
-{
- // Check if it's a game plugin:
- if (implementInterface<IPluginGame>(plugin)) {
- return plugin == m_Organizer->managedGame();
- }
-
- // Check the master, if any:
- auto& requirements = m_Requirements.at(plugin);
-
- if (requirements.master()) {
- return isEnabled(requirements.master());
- }
-
- // Check if the plugin is enabled:
- if (!m_Organizer->persistent(plugin->name(), "enabled", plugin->enabledByDefault()).toBool()) {
- return false;
- }
-
- // Check the requirements:
- return m_Requirements.at(plugin).canEnable();
-}
-
-void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies)
-{
- // If required, disable dependencies:
- if (!enable && dependencies) {
- for (auto* p : requirements(plugin).requiredFor()) {
- setEnabled(p, false, false); // No need to "recurse" here since requiredFor already does it.
- }
- }
-
- // Always disable/enable child plugins:
- for (auto* p : requirements(plugin).children()) {
- // "Child" plugin should have no dependencies.
- setEnabled(p, enable, false);
- }
-
- m_Organizer->setPersistent(plugin->name(), "enabled", enable, true);
-
- if (enable) {
- emit pluginEnabled(plugin);
- }
- else {
- emit pluginDisabled(plugin);
- }
-}
-
-MOBase::IPlugin* PluginContainer::plugin(QString const& pluginName) const
-{
- auto& map = bf::at_key<QString>(m_AccessPlugins);
- auto it = map.find(pluginName);
- if (it == std::end(map)) {
- return nullptr;
- }
- return it->second;
-}
-
-MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginDiagnose* diagnose) const
-{
- auto& map = bf::at_key<IPluginDiagnose>(m_AccessPlugins);
- auto it = map.find(diagnose);
- if (it == std::end(map)) {
- return nullptr;
- }
- return it->second;
-}
-
-MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginFileMapper* mapper) const
-{
- auto& map = bf::at_key<IPluginFileMapper>(m_AccessPlugins);
- auto it = map.find(mapper);
- if (it == std::end(map)) {
- return nullptr;
- }
- return it->second;
-}
-
-bool PluginContainer::isEnabled(QString const& pluginName) const {
- IPlugin* p = plugin(pluginName);
- return p ? isEnabled(p) : false;
-}
-bool PluginContainer::isEnabled(MOBase::IPluginDiagnose* diagnose) const {
- IPlugin* p = plugin(diagnose);
- return p ? isEnabled(p) : false;
-}
-bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const {
- IPlugin* p = plugin(mapper);
- return p ? isEnabled(p) : false;
-}
-
-const PluginRequirements& PluginContainer::requirements(IPlugin* plugin) const
-{
- return m_Requirements.at(plugin);
-}
-
-OrganizerProxy* PluginContainer::organizerProxy(MOBase::IPlugin* plugin) const
-{
- return requirements(plugin).m_Organizer;
-}
-
-MOBase::IPluginProxy* PluginContainer::pluginProxy(MOBase::IPlugin* plugin) const
-{
- return requirements(plugin).proxy();
-}
-
-QString PluginContainer::filepath(MOBase::IPlugin* plugin) const
-{
- return as_qobject(plugin)->property("filepath").toString();
-}
-
-IPluginGame *PluginContainer::game(const QString &name) const
-{
- auto iter = m_SupportedGames.find(name);
- if (iter != m_SupportedGames.end()) {
- return iter->second;
- } else {
- return nullptr;
- }
-}
-
-const PreviewGenerator &PluginContainer::previewGenerator() const
-{
- return m_PreviewGenerator;
-}
-
-void PluginContainer::startPluginsImpl(const std::vector<QObject*>& plugins) const
-{
- // setUserInterface()
- if (m_UserInterface) {
- for (auto* plugin : plugins) {
- if (auto* proxy = qobject_cast<IPluginProxy*>(plugin)) {
- proxy->setParentWidget(m_UserInterface->mainWindow());
- }
- if (auto* modPage = qobject_cast<IPluginModPage*>(plugin)) {
- modPage->setParentWidget(m_UserInterface->mainWindow());
- }
- if (auto* tool = qobject_cast<IPluginTool*>(plugin)) {
- tool->setParentWidget(m_UserInterface->mainWindow());
- }
- if (auto* installer = qobject_cast<IPluginInstaller*>(plugin)) {
- installer->setParentWidget(m_UserInterface->mainWindow());
- }
- }
- }
-
- // Trigger initial callbacks, e.g. onUserInterfaceInitialized and onProfileChanged.
- if (m_Organizer) {
- for (auto* object : plugins) {
- auto* plugin = qobject_cast<IPlugin*>(object);
- auto* oproxy = organizerProxy(plugin);
- oproxy->connectSignals();
- oproxy->m_ProfileChanged(nullptr, m_Organizer->currentProfile());
-
- if (m_UserInterface) {
- oproxy->m_UserInterfaceInitialized(m_UserInterface->mainWindow());
- }
- }
- }
-}
-
-std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath, IPluginProxy* proxy)
-{
- std::vector<QObject*> proxiedPlugins;
-
- try {
- // We get a list of matching plugins as proxies can return multiple plugins
- // per file and do not have a good way of supporting multiple inheritance.
- QList<QObject*> matchingPlugins = proxy->load(filepath);
-
- // We are going to group plugin by names and "fix" them later:
- std::map<QString, std::vector<IPlugin*>> proxiedByNames;
-
- for (QObject* proxiedPlugin : matchingPlugins) {
- if (proxiedPlugin != nullptr) {
-
- if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) {
- log::debug("loaded plugin '{}' from '{}' - [{}]",
- proxied->name(), QFileInfo(filepath).fileName(), implementedInterfaces(proxied).join(", "));
-
- // Store the plugin for later:
- proxiedPlugins.push_back(proxiedPlugin);
- proxiedByNames[proxied->name()].push_back(proxied);
- }
- else {
- log::warn(
- "plugin \"{}\" failed to load. If this plugin is for an older version of MO "
- "you have to update it or delete it if no update exists.",
- filepath);
- }
- }
- }
-
- // Fake masters:
- for (auto& [name, proxiedPlugins] : proxiedByNames) {
- if (proxiedPlugins.size() > 1) {
- auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins),
- [&](auto const& lhs, auto const& rhs) {
- return isBetterInterface(as_qobject(lhs), as_qobject(rhs));
- });
-
- for (auto& proxiedPlugin : proxiedPlugins) {
- if (proxiedPlugin != *it) {
- m_Requirements.at(proxiedPlugin).setMaster(*it);
- }
- }
- }
- }
- }
- catch (const std::exception& e) {
- reportError(QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what()));
- }
-
- return proxiedPlugins;
-}
-
-QObject* PluginContainer::loadQtPlugin(const QString& filepath)
-{
- std::unique_ptr<QPluginLoader> pluginLoader(new QPluginLoader(filepath, this));
- if (pluginLoader->instance() == nullptr) {
- m_FailedPlugins.push_back(filepath);
- log::error("failed to load plugin {}: {}", filepath, pluginLoader->errorString());
- }
- else {
- QObject* object = pluginLoader->instance();
- if (IPlugin* plugin = registerPlugin(object, filepath, nullptr); plugin) {
- log::debug("loaded plugin '{}' from '{}' - [{}]",
- plugin->name(), QFileInfo(filepath).fileName(), implementedInterfaces(plugin).join(", "));
- m_PluginLoaders.push_back(pluginLoader.release());
- return object;
- }
- else {
- m_FailedPlugins.push_back(filepath);
- log::warn("plugin '{}' failed to load (may be outdated)", filepath);
- }
- }
- return nullptr;
-}
-
-std::optional<QString> PluginContainer::isQtPluginFolder(const QString& filepath) const {
-
- if (!QFileInfo(filepath).isDir()) {
- return {};
- }
-
- QDirIterator iter(filepath, QDir::Files | QDir::NoDotAndDotDot);
- while (iter.hasNext()) {
- iter.next();
- const auto filePath = iter.filePath();
-
- // not a library, skip
- if (!QLibrary::isLibrary(filePath)) {
- continue;
- }
-
- // check if we have proper metadata - this does not load the plugin (metaData() should
- // be very lightweight)
- const QPluginLoader loader(filePath);
- if (!loader.metaData().isEmpty()) {
- return filePath;
- }
- }
-
- return {};
-}
-
-void PluginContainer::loadPlugin(QString const& filepath)
-{
- std::vector<QObject*> plugins;
- if (QFileInfo(filepath).isFile() && QLibrary::isLibrary(filepath)) {
- QObject* plugin = loadQtPlugin(filepath);
- if (plugin) {
- plugins.push_back(plugin);
- }
- }
- else if (auto p = isQtPluginFolder(filepath)) {
- QObject* plugin = loadQtPlugin(*p);
- if (plugin) {
- plugins.push_back(plugin);
- }
- }
- else {
- // We need to check if this can be handled by a proxy.
- for (auto* proxy : this->plugins<IPluginProxy>()) {
- auto filepaths = proxy->pluginList(
- QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
- if (filepaths.contains(filepath)) {
- plugins = loadProxied(filepath, proxy);
- break;
- }
- }
- }
-
- for (auto* plugin : plugins) {
- emit pluginRegistered(qobject_cast<IPlugin*>(plugin));
- }
-
- startPluginsImpl(plugins);
-}
-
-void PluginContainer::unloadPlugin(MOBase::IPlugin* plugin, QObject* object)
-{
- if (auto* game = qobject_cast<IPluginGame*>(object)) {
-
- if (game == managedGame()) {
- throw Exception("cannot unload the plugin for the currently managed game");
- }
-
- unregisterGame(game);
- }
-
- // We need to remove from the m_Plugins maps BEFORE unloading from the proxy
- // otherwise the qobject_cast to check the plugin type will not work.
- bf::for_each(m_Plugins, [object](auto& t) {
- using type = typename std::decay_t<decltype(t.second)>::value_type;
-
- // We do not want to remove from QObject since we are iterating over them.
- if constexpr (!std::is_same<type, QObject*>{}) {
- auto itp = std::find(t.second.begin(), t.second.end(), qobject_cast<type>(object));
- if (itp != t.second.end()) {
- t.second.erase(itp);
- }
- }
- });
-
- emit pluginUnregistered(plugin);
-
- // Remove from the members.
- if (auto* diagnose = qobject_cast<IPluginDiagnose*>(object)) {
- bf::at_key<IPluginDiagnose>(m_AccessPlugins).erase(diagnose);
- }
- if (auto* mapper = qobject_cast<IPluginFileMapper*>(object)) {
- bf::at_key<IPluginFileMapper>(m_AccessPlugins).erase(mapper);
- }
-
- auto& mapNames = bf::at_key<QString>(m_AccessPlugins);
- if (mapNames.contains(plugin->name())) {
- mapNames.erase(plugin->name());
- }
-
- m_Organizer->settings().plugins().unregisterPlugin(plugin);
-
- // Force disconnection of the signals from the proxies. This is a safety
- // operations since those signals should be disconnected when the proxies
- // are destroyed anyway.
- organizerProxy(plugin)->disconnectSignals();
-
- // Is this a proxied plugin?
- auto* proxy = pluginProxy(plugin);
-
- if (proxy) {
- proxy->unload(filepath(plugin));
- }
- else {
- // We need to find the loader.
- auto it = std::find_if(m_PluginLoaders.begin(), m_PluginLoaders.end(),
- [object](auto* loader) { return loader->instance() == object; });
-
- if (it != m_PluginLoaders.end()) {
- if (!(*it)->unload()) {
- log::error("failed to unload {}: {}", (*it)->fileName(), (*it)->errorString());
- }
- delete* it;
- m_PluginLoaders.erase(it);
- }
- else {
- log::error("loader for plugin {} does not exist, cannot unload", plugin->name());
- }
-
- }
-
- object->deleteLater();
-
- // Do this at the end.
- m_Requirements.erase(plugin);
-}
-
-void PluginContainer::unloadPlugin(QString const& filepath)
-{
- // We need to find all the plugins from the given path and
- // unload them:
- QString cleanPath = QDir::cleanPath(filepath);
- auto &objects = bf::at_key<QObject>(m_Plugins);
- for (auto it = objects.begin(); it != objects.end(); ) {
- auto* plugin = qobject_cast<IPlugin*>(*it);
- if (this->filepath(plugin) == filepath) {
- unloadPlugin(plugin, *it);
- it = objects.erase(it);
- }
- else {
- ++it;
- }
- }
-}
-
-void PluginContainer::reloadPlugin(QString const& filepath)
-{
- unloadPlugin(filepath);
- loadPlugin(filepath);
-}
-
-void PluginContainer::unloadPlugins()
-{
- if (m_Organizer) {
- // this will clear several structures that can hold on to pointers to
- // plugins, as well as read the plugin blacklist from the ini file, which
- // is used in loadPlugins() below to skip plugins
- //
- // note that the first thing loadPlugins() does is call unloadPlugins(),
- // so this makes sure the blacklist is always available
- m_Organizer->settings().plugins().clearPlugins();
- }
-
- bf::for_each(m_Plugins, [](auto& t) { t.second.clear(); });
- bf::for_each(m_AccessPlugins, [](auto& t) { t.second.clear(); });
- m_Requirements.clear();
-
- while (!m_PluginLoaders.empty()) {
- QPluginLoader* loader = m_PluginLoaders.back();
- m_PluginLoaders.pop_back();
- if ((loader != nullptr) && !loader->unload()) {
- log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString());
- }
- delete loader;
- }
-}
-
-void PluginContainer::loadPlugins()
-{
- TimeThis tt("PluginContainer::loadPlugins()");
-
- unloadPlugins();
-
- for (QObject *plugin : QPluginLoader::staticInstances()) {
- registerPlugin(plugin, "", nullptr);
- }
-
- QFile loadCheck;
- QString skipPlugin;
-
- if (m_Organizer) {
- loadCheck.setFileName(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp");
-
- if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) {
- // oh, there was a failed plugin load last time. Find out which plugin was loaded last
- QString fileName;
- while (!loadCheck.atEnd()) {
- fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed();
- }
-
- log::warn("loadcheck file found for plugin '{}'", fileName);
-
- MOBase::TaskDialog dlg;
-
- const auto Skip = QMessageBox::Ignore;
- const auto Blacklist = QMessageBox::Cancel;
- const auto Load = QMessageBox::Ok;
-
- const auto r = dlg
- .title(tr("Plugin error"))
- .main(tr(
- "Mod Organizer failed to load the plugin '%1' last time it was started.")
- .arg(fileName))
- .content(tr(
- "The plugin can be skipped for this session, blacklisted, "
- "or loaded normally, in which case it might fail again. Blacklisted "
- "plugins can be re-enabled later in the settings."))
- .icon(QMessageBox::Warning)
- .button({tr("Skip this plugin"), Skip})
- .button({tr("Blacklist this plugin"), Blacklist})
- .button({tr("Load this plugin"), Load})
- .exec();
-
- switch (r)
- {
- case Skip:
- log::warn("user wants to skip plugin '{}'", fileName);
- skipPlugin = fileName;
- break;
-
- case Blacklist:
- log::warn("user wants to blacklist plugin '{}'", fileName);
- m_Organizer->settings().plugins().addBlacklist(fileName);
- break;
-
- case Load:
- log::warn("user wants to load plugin '{}' anyway", fileName);
- break;
- }
-
- loadCheck.close();
- }
-
- loadCheck.open(QIODevice::WriteOnly);
- }
-
- QString pluginPath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
- log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath));
- QDirIterator iter(pluginPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
-
- while (iter.hasNext()) {
- iter.next();
-
- if (skipPlugin == iter.fileName()) {
- log::debug("plugin \"{}\" skipped for this session", iter.fileName());
- continue;
- }
-
- if (m_Organizer) {
- if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) {
- log::debug("plugin \"{}\" blacklisted", iter.fileName());
- continue;
- }
- }
-
- if (loadCheck.isOpen()) {
- loadCheck.write(iter.fileName().toUtf8());
- loadCheck.write("\n");
- loadCheck.flush();
- }
-
- QString filepath = iter.filePath();
- if (QLibrary::isLibrary(filepath)) {
- loadQtPlugin(filepath);
- }
- else if (auto p = isQtPluginFolder(filepath)) {
- loadQtPlugin(*p);
- }
- }
-
- if (skipPlugin.isEmpty()) {
- // remove the load check file on success
- if (loadCheck.isOpen()) {
- loadCheck.remove();
- }
- } else {
- // remember the plugin for next time
- if (loadCheck.isOpen()) {
- loadCheck.close();
- }
-
- log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin);
- loadCheck.open(QIODevice::WriteOnly);
- loadCheck.write(skipPlugin.toUtf8());
- loadCheck.write("\n");
- loadCheck.flush();
- }
-
-
- bf::at_key<IPluginDiagnose>(m_Plugins).push_back(this);
-
- if (m_Organizer) {
- bf::at_key<IPluginDiagnose>(m_Plugins).push_back(m_Organizer);
- m_Organizer->connectPlugins(this);
- }
-}
-
-std::vector<unsigned int> PluginContainer::activeProblems() const
-{
- std::vector<unsigned int> problems;
- if (m_FailedPlugins.size()) {
- problems.push_back(PROBLEM_PLUGINSNOTLOADED);
- }
- return problems;
-}
-
-QString PluginContainer::shortDescription(unsigned int key) const
-{
- switch (key) {
- case PROBLEM_PLUGINSNOTLOADED: {
- return tr("Some plugins could not be loaded");
- } break;
- default: {
- return tr("Description missing");
- } break;
- }
-}
-
-QString PluginContainer::fullDescription(unsigned int key) const
-{
- switch (key) {
- case PROBLEM_PLUGINSNOTLOADED: {
- QString result = tr("The following plugins could not be loaded. The reason may be missing "
- "dependencies (i.e. python) or an outdated version:") + "<ul>";
- for (const QString &plugin : m_FailedPlugins) {
- result += "<li>" + plugin + "</li>";
- }
- result += "<ul>";
- return result;
- } break;
- default: {
- return tr("Description missing");
- } break;
- }
-}
-
-bool PluginContainer::hasGuidedFix(unsigned int) const
-{
- return false;
-}
-
-void PluginContainer::startGuidedFix(unsigned int) const
-{
-}
+#include "plugincontainer.h"
+#include "organizercore.h"
+#include "organizerproxy.h"
+#include "report.h"
+#include <ipluginproxy.h>
+#include "iuserinterface.h"
+#include <idownloadmanager.h>
+#include "shared/appconfig.h"
+#include <QAction>
+#include <QToolButton>
+#include <QCoreApplication>
+#include <QMessageBox>
+#include <QDirIterator>
+#include <boost/fusion/sequence/intrinsic/at_key.hpp>
+#include <boost/fusion/include/at_key.hpp>
+#include <boost/fusion/algorithm/iteration/for_each.hpp>
+#include <boost/fusion/include/for_each.hpp>
+
+using namespace MOBase;
+using namespace MOShared;
+
+namespace bf = boost::fusion;
+
+// Welcome to the wonderful world of MO2 plugin management!
+//
+// We'll start by the C++ side.
+//
+// There are 9 types of MO2 plugins, two of which cannot be standalone: IPluginDiagnose
+// and IPluginFileMapper. This means that you can have a class implementing IPluginGame,
+// IPluginDiagnose and IPluginFileMapper. It is not possible for a class to implement
+// two full plugin types (e.g. IPluginPreview and IPluginTool).
+//
+// Plugins are fetch as QObject initially and must be "qobject-casted" to the right type.
+//
+// Plugins are stored in the PluginContainer class in various C++ containers: there is a vector
+// that stores all the plugin as QObject, multiple vectors that stores the plugin of each types,
+// a map to find IPlugin object from their names or from IPluginDiagnose or IFileMapper (since
+// these do not inherit IPlugin, they cannot be downcasted).
+//
+// Requirements for plugins are stored in m_Requirements:
+// - IPluginGame cannot be enabled by user. A game plugin is considered enable only if it is
+// the one corresponding to the currently managed games.
+// - If a plugin has a master plugin (IPlugin::master()), it cannot be enabled/disabled by users,
+// and will follow the enabled/disabled state of its parent.
+// - Each plugin has an "enabled" setting stored in persistence. If the setting does not exist,
+// the plugin's enabledByDefault is used instead.
+// - A plugin is considered disabled if the setting is false.
+// - If the setting is true, a plugin is considered disabled if one of its
+// requirements is not met.
+// - Users cannot enable a plugin if one of its requirements is not met.
+//
+// Now let's move to the Proxy side... Or the as of now, the Python side.
+//
+// Proxied plugins are much more annoying because they can implement all interfaces, and are
+// given to MO2 as separate plugins... A Python class implementing IPluginGame and IPluginDiagnose
+// will be seen by MO2 as two separate QObject, and they will all have the same name.
+//
+// When a proxied plugin is registered, a few things must be taken care of:
+// - There can only be one plugin mapped to a name in the PluginContainer class, so we keep the
+// plugin corresponding to the most relevant class (see PluginTypeOrder), e.g. if the class
+// inherits both IPluginGame and IPluginFileMapper, we map the name to the C++ QObject corresponding
+// to the IPluginGame.
+// - When a proxied plugin implements multiple interfaces, the IPlugin corresponding to the most
+// important interface is set as the parent (hidden) of the other IPlugin through PluginRequirements.
+// This way, the plugin are managed together (enabled/disabled state). The "fake" children plugins
+// will not be returned by PluginRequirements::children().
+// - Since each interface corresponds to a different QObject, we need to take care not to call
+// IPlugin::init() on each QObject, but only on the first one.
+//
+// All the proxied plugins are linked to the proxy plugin by PluginRequirements. If the proxy plugin
+// is disabled, the proxied plugins are not even loaded so not visible in the plugin management tab.
+
+template <class T>
+struct PluginTypeName;
+
+template <> struct PluginTypeName<MOBase::IPlugin> { static QString value() { return QT_TR_NOOP("Plugin"); } };
+template <> struct PluginTypeName<MOBase::IPluginDiagnose> { static QString value() { return QT_TR_NOOP("Diagnose"); } };
+template <> struct PluginTypeName<MOBase::IPluginGame> { static QString value() { return QT_TR_NOOP("Game"); } };
+template <> struct PluginTypeName<MOBase::IPluginInstaller> { static QString value() { return QT_TR_NOOP("Installer"); } };
+template <> struct PluginTypeName<MOBase::IPluginModPage> { static QString value() { return QT_TR_NOOP("Mod Page"); } };
+template <> struct PluginTypeName<MOBase::IPluginPreview> { static QString value() { return QT_TR_NOOP("Preview"); } };
+template <> struct PluginTypeName<MOBase::IPluginTool> { static QString value() { return QT_TR_NOOP("Tool"); } };
+template <> struct PluginTypeName<MOBase::IPluginProxy> { static QString value() { return QT_TR_NOOP("Proxy"); } };
+template <> struct PluginTypeName<MOBase::IPluginFileMapper> { static QString value() { return QT_TR_NOOP("File Mapper"); } };
+
+
+QStringList PluginContainer::pluginInterfaces()
+{
+ // Find all the names:
+ QStringList names;
+ boost::mp11::mp_for_each<PluginTypeOrder>([&names](const auto* p) {
+ using plugin_type = std::decay_t<decltype(*p)>;
+ auto name = PluginTypeName<plugin_type>::value();
+ if (!name.isEmpty()) {
+ names.append(name);
+ }
+ });
+
+ return names;
+}
+
+
+// PluginRequirementProxy
+
+const std::set<QString> PluginRequirements::s_CorePlugins{
+ "INI Bakery"
+};
+
+PluginRequirements::PluginRequirements(
+ PluginContainer* pluginContainer, MOBase::IPlugin* plugin, OrganizerProxy* proxy,
+ MOBase::IPluginProxy* pluginProxy)
+ : m_PluginContainer(pluginContainer)
+ , m_Plugin(plugin)
+ , m_PluginProxy(pluginProxy)
+ , m_Master(nullptr)
+ , m_Organizer(proxy)
+{
+ // There are a lots of things we cannot set here (e.g. m_Master) because we do not
+ // know the order plugins are loaded.
+}
+
+void PluginRequirements::fetchRequirements() {
+ m_Requirements = m_Plugin->requirements();
+}
+
+IPluginProxy* PluginRequirements::proxy() const
+{
+ return m_PluginProxy;
+}
+
+std::vector<IPlugin*> PluginRequirements::proxied() const
+{
+ std::vector<IPlugin*> children;
+ if (dynamic_cast<IPluginProxy*>(m_Plugin)) {
+ for (auto* obj : m_PluginContainer->plugins<QObject>()) {
+ auto* plugin = qobject_cast<IPlugin*>(obj);
+ if (plugin && m_PluginContainer->requirements(plugin).proxy() == m_Plugin) {
+ children.push_back(plugin);
+ }
+ }
+ }
+ return children;
+}
+
+IPlugin* PluginRequirements::master() const
+{
+ // If we have a m_Master, it was forced and thus override the default master().
+ if (m_Master) {
+ return m_Master;
+ }
+
+ if (m_Plugin->master().isEmpty()) {
+ return nullptr;
+ }
+
+ return m_PluginContainer->plugin(m_Plugin->master());
+}
+
+void PluginRequirements::setMaster(IPlugin* master)
+{
+ m_Master = master;
+}
+
+std::vector<IPlugin*> PluginRequirements::children() const
+{
+ std::vector<IPlugin*> children;
+ for (auto* obj : m_PluginContainer->plugins<QObject>()) {
+ auto* plugin = qobject_cast<IPlugin*>(obj);
+
+ // Not checking master() but requirements().master() due to "hidden"
+ // masters.
+ // If the master has the same name as the plugin, this is a "hidden"
+ // master, we do not add it here.
+ if (plugin
+ && m_PluginContainer->requirements(plugin).master() == m_Plugin
+ && plugin->name() != m_Plugin->name()) {
+ children.push_back(plugin);
+ }
+ }
+ return children;
+}
+
+std::vector<IPluginRequirement::Problem> PluginRequirements::problems() const
+{
+ std::vector<IPluginRequirement::Problem> result;
+ for (auto& requirement : m_Requirements) {
+ if (auto p = requirement->check(m_Organizer)) {
+ result.push_back(*p);
+ }
+ }
+ return result;
+}
+
+bool PluginRequirements::canEnable() const
+{
+ return problems().empty();
+}
+
+bool PluginRequirements::isCorePlugin() const
+{
+ // Let's consider game plugins as "core":
+ if (m_PluginContainer->implementInterface<IPluginGame>(m_Plugin)) {
+ return true;
+ }
+
+ return s_CorePlugins.contains(m_Plugin->name());
+}
+
+bool PluginRequirements::hasRequirements() const
+{
+ return !m_Requirements.empty();
+}
+
+QStringList PluginRequirements::requiredGames() const
+{
+ // We look for a "GameDependencyRequirement" - There can be only one since otherwise
+ // it'd mean that the plugin requires two games at once.
+ for (auto& requirement : m_Requirements) {
+ if (auto* gdep = dynamic_cast<const GameDependencyRequirement*>(requirement.get())) {
+ return gdep->gameNames();
+ }
+ }
+
+ return {};
+}
+
+std::vector<MOBase::IPlugin*> PluginRequirements::requiredFor() const
+{
+ std::vector<MOBase::IPlugin*> required;
+ std::set<MOBase::IPlugin*> visited;
+ requiredFor(required, visited);
+ return required;
+}
+
+
+void PluginRequirements::requiredFor(std::vector<MOBase::IPlugin*> &required, std::set<MOBase::IPlugin*>& visited) const
+{
+ // Handle cyclic dependencies.
+ if (visited.contains(m_Plugin)) {
+ return;
+ }
+ visited.insert(m_Plugin);
+
+
+ for (auto& [plugin, requirements] : m_PluginContainer->m_Requirements) {
+
+ // If the plugin is not enabled, discard:
+ if (!m_PluginContainer->isEnabled(plugin)) {
+ continue;
+ }
+
+ // Check the requirements:
+ for (auto& requirement : requirements.m_Requirements) {
+
+ // We check for plugin dependency. Game dependency are not checked this way.
+ if (auto* pdep = dynamic_cast<const PluginDependencyRequirement*>(requirement.get())) {
+
+ // Check if at least one of the plugin in the requirements is enabled (except this
+ // one):
+ bool oneEnabled = false;
+ for (auto& pluginName : pdep->pluginNames()) {
+ if (pluginName != m_Plugin->name() && m_PluginContainer->isEnabled(pluginName)) {
+ oneEnabled = true;
+ break;
+ }
+ }
+
+ // No plugin enabled found, so the plugin requires this plugin:
+ if (!oneEnabled) {
+ required.push_back(plugin);
+ requirements.requiredFor(required, visited);
+ break;
+ }
+ }
+ }
+ }
+}
+
+// PluginContainer
+
+PluginContainer::PluginContainer(OrganizerCore *organizer)
+ : m_Organizer(organizer)
+ , m_UserInterface(nullptr)
+ , m_PreviewGenerator(*this)
+{
+}
+
+PluginContainer::~PluginContainer() {
+ m_Organizer = nullptr;
+ unloadPlugins();
+}
+
+void PluginContainer::startPlugins(IUserInterface *userInterface)
+{
+ m_UserInterface = userInterface;
+ startPluginsImpl(plugins<QObject>());
+}
+
+QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const
+{
+ // We need a QObject to be able to qobject_cast<> to the plugin types:
+ QObject* oPlugin = as_qobject(plugin);
+
+ if (!oPlugin) {
+ return {};
+ }
+
+ return implementedInterfaces(oPlugin);
+}
+
+QStringList PluginContainer::implementedInterfaces(QObject * oPlugin) const
+{
+ // Find all the names:
+ QStringList names;
+ boost::mp11::mp_for_each<PluginTypeOrder>([oPlugin, &names](const auto* p) {
+ using plugin_type = std::decay_t<decltype(*p)>;
+ if (qobject_cast<plugin_type*>(oPlugin)) {
+ auto name = PluginTypeName<plugin_type>::value();
+ if (!name.isEmpty()) {
+ names.append(name);
+ }
+ }
+ });
+
+ // If the plugin implements at least one interface other than IPlugin, remove IPlugin:
+ if (names.size() > 1) {
+ names.removeAll(PluginTypeName<IPlugin>::value());
+ }
+
+ return names;
+}
+
+QString PluginContainer::topImplementedInterface(IPlugin* plugin) const
+{
+ auto interfaces = implementedInterfaces(plugin);
+ return interfaces.isEmpty() ? "" : interfaces[0];
+}
+
+bool PluginContainer::isBetterInterface(QObject* lhs, QObject* rhs) const
+{
+ int count = 0, lhsIdx = -1, rhsIdx = -1;
+ boost::mp11::mp_for_each<PluginTypeOrder>([&](const auto* p) {
+ using plugin_type = std::decay_t<decltype(*p)>;
+ if (lhsIdx < 0 && qobject_cast<plugin_type*>(lhs)) {
+ lhsIdx = count;
+ }
+ if (rhsIdx < 0 && qobject_cast<plugin_type*>(rhs)) {
+ rhsIdx = count;
+ }
+ ++count;
+ });
+ return lhsIdx < rhsIdx;
+}
+
+QStringList PluginContainer::pluginFileNames() const
+{
+ QStringList result;
+ for (QPluginLoader *loader : m_PluginLoaders) {
+ result.append(loader->fileName());
+ }
+ std::vector<IPluginProxy *> proxyList = bf::at_key<IPluginProxy>(m_Plugins);
+ for (IPluginProxy *proxy : proxyList) {
+ QStringList proxiedPlugins = proxy->pluginList(
+ QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
+ result.append(proxiedPlugins);
+ }
+ return result;
+}
+
+QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const
+{
+ // Find the correspond QObject - Can this be done safely with a cast?
+ auto& objects = bf::at_key<QObject>(m_Plugins);
+ auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) {
+ return qobject_cast<IPlugin*>(obj) == plugin;
+ });
+
+ if (it == std::end(objects)) {
+ return nullptr;
+ }
+
+ return *it;
+}
+
+bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy, bool skipInit)
+{
+ // when MO has no instance loaded, init() is not called on plugins, except
+ // for proxy plugins, where init() is called with a null IOrganizer
+ //
+ // after proxies are initialized, instantiate() is called for all the plugins
+ // they've discovered, but as for regular plugins, init() won't be
+ // called on them if m_OrganizerCore is null
+
+ if (plugin == nullptr) {
+ return false;
+ }
+
+ OrganizerProxy* proxy = nullptr;
+ if (m_Organizer) {
+ proxy = new OrganizerProxy(m_Organizer, this, plugin);
+ proxy->setParent(as_qobject(plugin));
+ }
+
+ // Check if it is a proxy plugin:
+ bool isProxy = dynamic_cast<IPluginProxy*>(plugin);
+
+ auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy));
+
+ if (!m_Organizer && !isProxy) {
+ return true;
+ }
+
+ if (skipInit) {
+ return true;
+ }
+
+ if (!plugin->init(proxy)) {
+ log::warn("plugin failed to initialize");
+ return false;
+ }
+
+ // Update requirements:
+ it->second.fetchRequirements();
+
+ return true;
+}
+
+void PluginContainer::registerGame(IPluginGame *game)
+{
+ m_SupportedGames.insert({ game->gameName(), game });
+}
+
+void PluginContainer::unregisterGame(MOBase::IPluginGame* game)
+{
+ m_SupportedGames.erase(game->gameName());
+}
+
+IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString& filepath, MOBase::IPluginProxy* pluginProxy)
+{
+
+ // generic treatment for all plugins
+ IPlugin *pluginObj = qobject_cast<IPlugin*>(plugin);
+ if (pluginObj == nullptr) {
+ log::debug("PluginContainer::registerPlugin() called with a non IPlugin QObject.");
+ return nullptr;
+ }
+
+ // If we already a plugin with this name:
+ bool skipInit = false;
+ auto& mapNames = bf::at_key<QString>(m_AccessPlugins);
+ if (mapNames.contains(pluginObj->name())) {
+
+ IPlugin* other = mapNames[pluginObj->name()];
+
+ // If both plugins are from the same proxy and the same file, this is usually
+ // ok (in theory some one could write two different classes from the same Python file/module):
+ if (pluginProxy && m_Requirements.at(other).proxy() == pluginProxy
+ && this->filepath(other) == QDir::cleanPath(filepath)) {
+
+ // Plugin has already been initialized:
+ skipInit = true;
+
+ if (isBetterInterface(plugin, as_qobject(other))) {
+ log::debug("replacing plugin '{}' with interfaces [{}] by one with interfaces [{}]",
+ pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(plugin).join(", "));
+ bf::at_key<QString>(m_AccessPlugins)[pluginObj->name()] = pluginObj;
+ }
+ }
+ else {
+ log::warn("Trying to register two plugins with the name '{}' (from {} and {}), the second one will not be registered.",
+ pluginObj->name(), this->filepath(other), QDir::cleanPath(filepath));
+ return nullptr;
+ }
+ }
+ else {
+ bf::at_key<QString>(m_AccessPlugins)[pluginObj->name()] = pluginObj;
+ }
+
+ // Storing the original QObject* is a bit of a hack as I couldn't figure out any
+ // way to cast directly between IPlugin* and IPluginDiagnose*
+ bf::at_key<QObject>(m_Plugins).push_back(plugin);
+
+ plugin->setProperty("filepath", QDir::cleanPath(filepath));
+ plugin->setParent(this);
+
+ if (m_Organizer) {
+ m_Organizer->settings().plugins().registerPlugin(pluginObj);
+ }
+
+ { // diagnosis plugin
+ IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(plugin);
+ if (diagnose != nullptr) {
+ bf::at_key<IPluginDiagnose>(m_Plugins).push_back(diagnose);
+ bf::at_key<IPluginDiagnose>(m_AccessPlugins)[diagnose] = pluginObj;
+ diagnose->onInvalidated([&]() { emit diagnosisUpdate(); });
+ }
+ }
+ { // file mapper plugin
+ IPluginFileMapper *mapper = qobject_cast<IPluginFileMapper*>(plugin);
+ if (mapper != nullptr) {
+ bf::at_key<IPluginFileMapper>(m_Plugins).push_back(mapper);
+ bf::at_key<IPluginFileMapper>(m_AccessPlugins)[mapper] = pluginObj;
+ }
+ }
+ { // mod page plugin
+ IPluginModPage *modPage = qobject_cast<IPluginModPage*>(plugin);
+ if (initPlugin(modPage, pluginProxy, skipInit)) {
+ bf::at_key<IPluginModPage>(m_Plugins).push_back(modPage);
+ emit pluginRegistered(modPage);
+ return modPage;
+ }
+ }
+ { // game plugin
+ IPluginGame *game = qobject_cast<IPluginGame*>(plugin);
+ if (game) {
+ game->detectGame();
+ if (initPlugin(game, pluginProxy, skipInit)) {
+ bf::at_key<IPluginGame>(m_Plugins).push_back(game);
+ registerGame(game);
+ emit pluginRegistered(game);
+ return game;
+ }
+ }
+ }
+ { // tool plugins
+ IPluginTool *tool = qobject_cast<IPluginTool*>(plugin);
+ if (initPlugin(tool, pluginProxy, skipInit)) {
+ bf::at_key<IPluginTool>(m_Plugins).push_back(tool);
+ emit pluginRegistered(tool);
+ return tool;
+ }
+ }
+ { // installer plugins
+ IPluginInstaller *installer = qobject_cast<IPluginInstaller*>(plugin);
+ if (initPlugin(installer, pluginProxy, skipInit)) {
+ bf::at_key<IPluginInstaller>(m_Plugins).push_back(installer);
+ if (m_Organizer) {
+ installer->setInstallationManager(m_Organizer->installationManager());
+ }
+ emit pluginRegistered(installer);
+ return installer;
+ }
+ }
+ { // preview plugins
+ IPluginPreview *preview = qobject_cast<IPluginPreview*>(plugin);
+ if (initPlugin(preview, pluginProxy, skipInit)) {
+ bf::at_key<IPluginPreview>(m_Plugins).push_back(preview);
+ return preview;
+ }
+ }
+ { // proxy plugins
+ IPluginProxy *proxy = qobject_cast<IPluginProxy*>(plugin);
+ if (initPlugin(proxy, pluginProxy, skipInit)) {
+ bf::at_key<IPluginProxy>(m_Plugins).push_back(proxy);
+ emit pluginRegistered(proxy);
+
+ QStringList filepaths = proxy->pluginList(
+ QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
+ for (const QString& filepath : filepaths) {
+ loadProxied(filepath, proxy);
+ }
+ return proxy;
+ }
+ }
+
+ { // dummy plugins
+ // only initialize these, no processing otherwise
+ IPlugin *dummy = qobject_cast<IPlugin*>(plugin);
+ if (initPlugin(dummy, pluginProxy, skipInit)) {
+ bf::at_key<IPlugin>(m_Plugins).push_back(dummy);
+ emit pluginRegistered(dummy);
+ return dummy;
+ }
+ }
+
+ return nullptr;
+}
+
+IPlugin* PluginContainer::managedGame() const
+{
+ // TODO: This const_cast is safe but ugly. Most methods require a IPlugin*, so
+ // returning a const-version if painful. This should be fixed by making methods accept
+ // a const IPlugin* instead, but there are a few tricks with qobject_cast and const.
+ return const_cast<IPluginGame*>(m_Organizer->managedGame());
+}
+
+bool PluginContainer::isEnabled(IPlugin* plugin) const
+{
+ // Check if it's a game plugin:
+ if (implementInterface<IPluginGame>(plugin)) {
+ return plugin == m_Organizer->managedGame();
+ }
+
+ // Check the master, if any:
+ auto& requirements = m_Requirements.at(plugin);
+
+ if (requirements.master()) {
+ return isEnabled(requirements.master());
+ }
+
+ // Check if the plugin is enabled:
+ if (!m_Organizer->persistent(plugin->name(), "enabled", plugin->enabledByDefault()).toBool()) {
+ return false;
+ }
+
+ // Check the requirements:
+ return m_Requirements.at(plugin).canEnable();
+}
+
+void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies)
+{
+ // If required, disable dependencies:
+ if (!enable && dependencies) {
+ for (auto* p : requirements(plugin).requiredFor()) {
+ setEnabled(p, false, false); // No need to "recurse" here since requiredFor already does it.
+ }
+ }
+
+ // Always disable/enable child plugins:
+ for (auto* p : requirements(plugin).children()) {
+ // "Child" plugin should have no dependencies.
+ setEnabled(p, enable, false);
+ }
+
+ m_Organizer->setPersistent(plugin->name(), "enabled", enable, true);
+
+ if (enable) {
+ emit pluginEnabled(plugin);
+ }
+ else {
+ emit pluginDisabled(plugin);
+ }
+}
+
+MOBase::IPlugin* PluginContainer::plugin(QString const& pluginName) const
+{
+ auto& map = bf::at_key<QString>(m_AccessPlugins);
+ auto it = map.find(pluginName);
+ if (it == std::end(map)) {
+ return nullptr;
+ }
+ return it->second;
+}
+
+MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginDiagnose* diagnose) const
+{
+ auto& map = bf::at_key<IPluginDiagnose>(m_AccessPlugins);
+ auto it = map.find(diagnose);
+ if (it == std::end(map)) {
+ return nullptr;
+ }
+ return it->second;
+}
+
+MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginFileMapper* mapper) const
+{
+ auto& map = bf::at_key<IPluginFileMapper>(m_AccessPlugins);
+ auto it = map.find(mapper);
+ if (it == std::end(map)) {
+ return nullptr;
+ }
+ return it->second;
+}
+
+bool PluginContainer::isEnabled(QString const& pluginName) const {
+ IPlugin* p = plugin(pluginName);
+ return p ? isEnabled(p) : false;
+}
+bool PluginContainer::isEnabled(MOBase::IPluginDiagnose* diagnose) const {
+ IPlugin* p = plugin(diagnose);
+ return p ? isEnabled(p) : false;
+}
+bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const {
+ IPlugin* p = plugin(mapper);
+ return p ? isEnabled(p) : false;
+}
+
+const PluginRequirements& PluginContainer::requirements(IPlugin* plugin) const
+{
+ return m_Requirements.at(plugin);
+}
+
+OrganizerProxy* PluginContainer::organizerProxy(MOBase::IPlugin* plugin) const
+{
+ return requirements(plugin).m_Organizer;
+}
+
+MOBase::IPluginProxy* PluginContainer::pluginProxy(MOBase::IPlugin* plugin) const
+{
+ return requirements(plugin).proxy();
+}
+
+QString PluginContainer::filepath(MOBase::IPlugin* plugin) const
+{
+ return as_qobject(plugin)->property("filepath").toString();
+}
+
+IPluginGame *PluginContainer::game(const QString &name) const
+{
+ auto iter = m_SupportedGames.find(name);
+ if (iter != m_SupportedGames.end()) {
+ return iter->second;
+ } else {
+ return nullptr;
+ }
+}
+
+const PreviewGenerator &PluginContainer::previewGenerator() const
+{
+ return m_PreviewGenerator;
+}
+
+void PluginContainer::startPluginsImpl(const std::vector<QObject*>& plugins) const
+{
+ // setUserInterface()
+ if (m_UserInterface) {
+ for (auto* plugin : plugins) {
+ if (auto* proxy = qobject_cast<IPluginProxy*>(plugin)) {
+ proxy->setParentWidget(m_UserInterface->mainWindow());
+ }
+ if (auto* modPage = qobject_cast<IPluginModPage*>(plugin)) {
+ modPage->setParentWidget(m_UserInterface->mainWindow());
+ }
+ if (auto* tool = qobject_cast<IPluginTool*>(plugin)) {
+ tool->setParentWidget(m_UserInterface->mainWindow());
+ }
+ if (auto* installer = qobject_cast<IPluginInstaller*>(plugin)) {
+ installer->setParentWidget(m_UserInterface->mainWindow());
+ }
+ }
+ }
+
+ // Trigger initial callbacks, e.g. onUserInterfaceInitialized and onProfileChanged.
+ if (m_Organizer) {
+ for (auto* object : plugins) {
+ auto* plugin = qobject_cast<IPlugin*>(object);
+ auto* oproxy = organizerProxy(plugin);
+ oproxy->connectSignals();
+ oproxy->m_ProfileChanged(nullptr, m_Organizer->currentProfile());
+
+ if (m_UserInterface) {
+ oproxy->m_UserInterfaceInitialized(m_UserInterface->mainWindow());
+ }
+ }
+ }
+}
+
+std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath, IPluginProxy* proxy)
+{
+ std::vector<QObject*> proxiedPlugins;
+
+ try {
+ // We get a list of matching plugins as proxies can return multiple plugins
+ // per file and do not have a good way of supporting multiple inheritance.
+ QList<QObject*> matchingPlugins = proxy->load(filepath);
+
+ // We are going to group plugin by names and "fix" them later:
+ std::map<QString, std::vector<IPlugin*>> proxiedByNames;
+
+ for (QObject* proxiedPlugin : matchingPlugins) {
+ if (proxiedPlugin != nullptr) {
+
+ if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) {
+ log::debug("loaded plugin '{}' from '{}' - [{}]",
+ proxied->name(), QFileInfo(filepath).fileName(), implementedInterfaces(proxied).join(", "));
+
+ // Store the plugin for later:
+ proxiedPlugins.push_back(proxiedPlugin);
+ proxiedByNames[proxied->name()].push_back(proxied);
+ }
+ else {
+ log::warn(
+ "plugin \"{}\" failed to load. If this plugin is for an older version of MO "
+ "you have to update it or delete it if no update exists.",
+ filepath);
+ }
+ }
+ }
+
+ // Fake masters:
+ for (auto& [name, proxiedPlugins] : proxiedByNames) {
+ if (proxiedPlugins.size() > 1) {
+ auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins),
+ [&](auto const& lhs, auto const& rhs) {
+ return isBetterInterface(as_qobject(lhs), as_qobject(rhs));
+ });
+
+ for (auto& proxiedPlugin : proxiedPlugins) {
+ if (proxiedPlugin != *it) {
+ m_Requirements.at(proxiedPlugin).setMaster(*it);
+ }
+ }
+ }
+ }
+ }
+ catch (const std::exception& e) {
+ reportError(QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what()));
+ }
+
+ return proxiedPlugins;
+}
+
+QObject* PluginContainer::loadQtPlugin(const QString& filepath)
+{
+ std::unique_ptr<QPluginLoader> pluginLoader(new QPluginLoader(filepath, this));
+ if (pluginLoader->instance() == nullptr) {
+ m_FailedPlugins.push_back(filepath);
+ log::error("failed to load plugin {}: {}", filepath, pluginLoader->errorString());
+ }
+ else {
+ QObject* object = pluginLoader->instance();
+ if (IPlugin* plugin = registerPlugin(object, filepath, nullptr); plugin) {
+ log::debug("loaded plugin '{}' from '{}' - [{}]",
+ plugin->name(), QFileInfo(filepath).fileName(), implementedInterfaces(plugin).join(", "));
+ m_PluginLoaders.push_back(pluginLoader.release());
+ return object;
+ }
+ else {
+ m_FailedPlugins.push_back(filepath);
+ log::warn("plugin '{}' failed to load (may be outdated)", filepath);
+ }
+ }
+ return nullptr;
+}
+
+std::optional<QString> PluginContainer::isQtPluginFolder(const QString& filepath) const {
+
+ if (!QFileInfo(filepath).isDir()) {
+ return {};
+ }
+
+ QDirIterator iter(filepath, QDir::Files | QDir::NoDotAndDotDot);
+ while (iter.hasNext()) {
+ iter.next();
+ const auto filePath = iter.filePath();
+
+ // not a library, skip
+ if (!QLibrary::isLibrary(filePath)) {
+ continue;
+ }
+
+ // check if we have proper metadata - this does not load the plugin (metaData() should
+ // be very lightweight)
+ const QPluginLoader loader(filePath);
+ if (!loader.metaData().isEmpty()) {
+ return filePath;
+ }
+ }
+
+ return {};
+}
+
+void PluginContainer::loadPlugin(QString const& filepath)
+{
+ std::vector<QObject*> plugins;
+ if (QFileInfo(filepath).isFile() && QLibrary::isLibrary(filepath)) {
+ QObject* plugin = loadQtPlugin(filepath);
+ if (plugin) {
+ plugins.push_back(plugin);
+ }
+ }
+ else if (auto p = isQtPluginFolder(filepath)) {
+ QObject* plugin = loadQtPlugin(*p);
+ if (plugin) {
+ plugins.push_back(plugin);
+ }
+ }
+ else {
+ // We need to check if this can be handled by a proxy.
+ for (auto* proxy : this->plugins<IPluginProxy>()) {
+ auto filepaths = proxy->pluginList(
+ QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
+ if (filepaths.contains(filepath)) {
+ plugins = loadProxied(filepath, proxy);
+ break;
+ }
+ }
+ }
+
+ for (auto* plugin : plugins) {
+ emit pluginRegistered(qobject_cast<IPlugin*>(plugin));
+ }
+
+ startPluginsImpl(plugins);
+}
+
+void PluginContainer::unloadPlugin(MOBase::IPlugin* plugin, QObject* object)
+{
+ if (auto* game = qobject_cast<IPluginGame*>(object)) {
+
+ if (game == managedGame()) {
+ throw Exception("cannot unload the plugin for the currently managed game");
+ }
+
+ unregisterGame(game);
+ }
+
+ // We need to remove from the m_Plugins maps BEFORE unloading from the proxy
+ // otherwise the qobject_cast to check the plugin type will not work.
+ bf::for_each(m_Plugins, [object](auto& t) {
+ using type = typename std::decay_t<decltype(t.second)>::value_type;
+
+ // We do not want to remove from QObject since we are iterating over them.
+ if constexpr (!std::is_same<type, QObject*>{}) {
+ auto itp = std::find(t.second.begin(), t.second.end(), qobject_cast<type>(object));
+ if (itp != t.second.end()) {
+ t.second.erase(itp);
+ }
+ }
+ });
+
+ emit pluginUnregistered(plugin);
+
+ // Remove from the members.
+ if (auto* diagnose = qobject_cast<IPluginDiagnose*>(object)) {
+ bf::at_key<IPluginDiagnose>(m_AccessPlugins).erase(diagnose);
+ }
+ if (auto* mapper = qobject_cast<IPluginFileMapper*>(object)) {
+ bf::at_key<IPluginFileMapper>(m_AccessPlugins).erase(mapper);
+ }
+
+ auto& mapNames = bf::at_key<QString>(m_AccessPlugins);
+ if (mapNames.contains(plugin->name())) {
+ mapNames.erase(plugin->name());
+ }
+
+ m_Organizer->settings().plugins().unregisterPlugin(plugin);
+
+ // Force disconnection of the signals from the proxies. This is a safety
+ // operations since those signals should be disconnected when the proxies
+ // are destroyed anyway.
+ organizerProxy(plugin)->disconnectSignals();
+
+ // Is this a proxied plugin?
+ auto* proxy = pluginProxy(plugin);
+
+ if (proxy) {
+ proxy->unload(filepath(plugin));
+ }
+ else {
+ // We need to find the loader.
+ auto it = std::find_if(m_PluginLoaders.begin(), m_PluginLoaders.end(),
+ [object](auto* loader) { return loader->instance() == object; });
+
+ if (it != m_PluginLoaders.end()) {
+ if (!(*it)->unload()) {
+ log::error("failed to unload {}: {}", (*it)->fileName(), (*it)->errorString());
+ }
+ delete* it;
+ m_PluginLoaders.erase(it);
+ }
+ else {
+ log::error("loader for plugin {} does not exist, cannot unload", plugin->name());
+ }
+
+ }
+
+ object->deleteLater();
+
+ // Do this at the end.
+ m_Requirements.erase(plugin);
+}
+
+void PluginContainer::unloadPlugin(QString const& filepath)
+{
+ // We need to find all the plugins from the given path and
+ // unload them:
+ QString cleanPath = QDir::cleanPath(filepath);
+ auto &objects = bf::at_key<QObject>(m_Plugins);
+ for (auto it = objects.begin(); it != objects.end(); ) {
+ auto* plugin = qobject_cast<IPlugin*>(*it);
+ if (this->filepath(plugin) == filepath) {
+ unloadPlugin(plugin, *it);
+ it = objects.erase(it);
+ }
+ else {
+ ++it;
+ }
+ }
+}
+
+void PluginContainer::reloadPlugin(QString const& filepath)
+{
+ unloadPlugin(filepath);
+ loadPlugin(filepath);
+}
+
+void PluginContainer::unloadPlugins()
+{
+ if (m_Organizer) {
+ // this will clear several structures that can hold on to pointers to
+ // plugins, as well as read the plugin blacklist from the ini file, which
+ // is used in loadPlugins() below to skip plugins
+ //
+ // note that the first thing loadPlugins() does is call unloadPlugins(),
+ // so this makes sure the blacklist is always available
+ m_Organizer->settings().plugins().clearPlugins();
+ }
+
+ bf::for_each(m_Plugins, [](auto& t) { t.second.clear(); });
+ bf::for_each(m_AccessPlugins, [](auto& t) { t.second.clear(); });
+ m_Requirements.clear();
+
+ while (!m_PluginLoaders.empty()) {
+ QPluginLoader* loader = m_PluginLoaders.back();
+ m_PluginLoaders.pop_back();
+ if ((loader != nullptr) && !loader->unload()) {
+ log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString());
+ }
+ delete loader;
+ }
+}
+
+void PluginContainer::loadPlugins()
+{
+ TimeThis tt("PluginContainer::loadPlugins()");
+
+ unloadPlugins();
+
+ for (QObject *plugin : QPluginLoader::staticInstances()) {
+ registerPlugin(plugin, "", nullptr);
+ }
+
+ QFile loadCheck;
+ QString skipPlugin;
+
+ if (m_Organizer) {
+ loadCheck.setFileName(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp");
+
+ if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) {
+ // oh, there was a failed plugin load last time. Find out which plugin was loaded last
+ QString fileName;
+ while (!loadCheck.atEnd()) {
+ fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed();
+ }
+
+ log::warn("loadcheck file found for plugin '{}'", fileName);
+
+ MOBase::TaskDialog dlg;
+
+ const auto Skip = QMessageBox::Ignore;
+ const auto Blacklist = QMessageBox::Cancel;
+ const auto Load = QMessageBox::Ok;
+
+ const auto r = dlg
+ .title(tr("Plugin error"))
+ .main(tr(
+ "Mod Organizer failed to load the plugin '%1' last time it was started.")
+ .arg(fileName))
+ .content(tr(
+ "The plugin can be skipped for this session, blacklisted, "
+ "or loaded normally, in which case it might fail again. Blacklisted "
+ "plugins can be re-enabled later in the settings."))
+ .icon(QMessageBox::Warning)
+ .button({tr("Skip this plugin"), Skip})
+ .button({tr("Blacklist this plugin"), Blacklist})
+ .button({tr("Load this plugin"), Load})
+ .exec();
+
+ switch (r)
+ {
+ case Skip:
+ log::warn("user wants to skip plugin '{}'", fileName);
+ skipPlugin = fileName;
+ break;
+
+ case Blacklist:
+ log::warn("user wants to blacklist plugin '{}'", fileName);
+ m_Organizer->settings().plugins().addBlacklist(fileName);
+ break;
+
+ case Load:
+ log::warn("user wants to load plugin '{}' anyway", fileName);
+ break;
+ }
+
+ loadCheck.close();
+ }
+
+ loadCheck.open(QIODevice::WriteOnly);
+ }
+
+ QString pluginPath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
+ log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath));
+ QDirIterator iter(pluginPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+
+ while (iter.hasNext()) {
+ iter.next();
+
+ if (skipPlugin == iter.fileName()) {
+ log::debug("plugin \"{}\" skipped for this session", iter.fileName());
+ continue;
+ }
+
+ if (m_Organizer) {
+ if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) {
+ log::debug("plugin \"{}\" blacklisted", iter.fileName());
+ continue;
+ }
+ }
+
+ if (loadCheck.isOpen()) {
+ loadCheck.write(iter.fileName().toUtf8());
+ loadCheck.write("\n");
+ loadCheck.flush();
+ }
+
+ QString filepath = iter.filePath();
+ if (QLibrary::isLibrary(filepath)) {
+ loadQtPlugin(filepath);
+ }
+ else if (auto p = isQtPluginFolder(filepath)) {
+ loadQtPlugin(*p);
+ }
+ }
+
+ if (skipPlugin.isEmpty()) {
+ // remove the load check file on success
+ if (loadCheck.isOpen()) {
+ loadCheck.remove();
+ }
+ } else {
+ // remember the plugin for next time
+ if (loadCheck.isOpen()) {
+ loadCheck.close();
+ }
+
+ log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin);
+ loadCheck.open(QIODevice::WriteOnly);
+ loadCheck.write(skipPlugin.toUtf8());
+ loadCheck.write("\n");
+ loadCheck.flush();
+ }
+
+
+ bf::at_key<IPluginDiagnose>(m_Plugins).push_back(this);
+
+ if (m_Organizer) {
+ bf::at_key<IPluginDiagnose>(m_Plugins).push_back(m_Organizer);
+ m_Organizer->connectPlugins(this);
+ }
+}
+
+std::vector<unsigned int> PluginContainer::activeProblems() const
+{
+ std::vector<unsigned int> problems;
+ if (m_FailedPlugins.size()) {
+ problems.push_back(PROBLEM_PLUGINSNOTLOADED);
+ }
+ return problems;
+}
+
+QString PluginContainer::shortDescription(unsigned int key) const
+{
+ switch (key) {
+ case PROBLEM_PLUGINSNOTLOADED: {
+ return tr("Some plugins could not be loaded");
+ } break;
+ default: {
+ return tr("Description missing");
+ } break;
+ }
+}
+
+QString PluginContainer::fullDescription(unsigned int key) const
+{
+ switch (key) {
+ case PROBLEM_PLUGINSNOTLOADED: {
+ QString result = tr("The following plugins could not be loaded. The reason may be missing "
+ "dependencies (i.e. python) or an outdated version:") + "<ul>";
+ for (const QString &plugin : m_FailedPlugins) {
+ result += "<li>" + plugin + "</li>";
+ }
+ result += "<ul>";
+ return result;
+ } break;
+ default: {
+ return tr("Description missing");
+ } break;
+ }
+}
+
+bool PluginContainer::hasGuidedFix(unsigned int) const
+{
+ return false;
+}
+
+void PluginContainer::startGuidedFix(unsigned int) const
+{
+}
diff --git a/src/plugincontainer.h b/src/plugincontainer.h
index 2ab9a4b7..688c9f10 100644
--- a/src/plugincontainer.h
+++ b/src/plugincontainer.h
@@ -1,498 +1,498 @@
-#ifndef PLUGINCONTAINER_H
-#define PLUGINCONTAINER_H
-
-#include "previewgenerator.h"
-
-class OrganizerCore;
-class IUserInterface;
-
-#include <iplugindiagnose.h>
-#include <ipluginmodpage.h>
-#include <iplugingame.h>
-#include <iplugintool.h>
-#include <ipluginproxy.h>
-#include <iplugininstaller.h>
-#include <ipluginfilemapper.h>
-#include <QtPlugin>
-#include <QPluginLoader>
-#include <QFile>
-#ifndef Q_MOC_RUN
-#include <boost/fusion/container.hpp>
-#include <boost/fusion/include/at_key.hpp>
-#include <boost/mp11.hpp>
-#endif // Q_MOC_RUN
-#include <vector>
-#include <memory>
-
-
-class OrganizerProxy;
-
-/**
- * @brief Class that wrap multiple requirements for a plugin together. THis
- * class owns the requirements.
- */
-class PluginRequirements {
-public:
-
- /**
- * @return true if the plugin can be enabled (all requirements are met).
- */
- bool canEnable() const;
-
- /**
- * @return true if this is a core plugin, i.e. a plugin that should not be
- * manually enabled or disabled by the user.
- */
- bool isCorePlugin() const;
-
- /**
- * @return true if this plugin has requirements (satisfied or not).
- */
- bool hasRequirements() const;
-
- /**
- * @return the proxy that created this plugin, if any.
- */
- MOBase::IPluginProxy* proxy() const;
-
- /**
- * @return the list of plugins this plugin proxies (if it's a proxy plugin).
- */
- std::vector<MOBase::IPlugin*> proxied() const;
-
- /**
- * @return the master of this plugin, if any.
- */
- MOBase::IPlugin* master() const;
-
- /**
- * @return the plugins this plugin is master of.
- */
- std::vector<MOBase::IPlugin*> children() const;
-
- /**
- * @return the list of problems to be resolved before enabling the plugin.
- */
- std::vector<MOBase::IPluginRequirement::Problem> problems() const;
-
- /**
- * @return the name of the games (gameName()) this plugin can be used with, or an empty
- * list if this plugin does not require particular games.
- */
- QStringList requiredGames() const;
-
- /**
- * @return the list of plugins currently enabled that would have to be disabled
- * if this plugin was disabled.
- */
- std::vector<MOBase::IPlugin*> requiredFor() const;
-
-private:
-
- // The list of "Core" plugins.
- static const std::set<QString> s_CorePlugins;
-
- // Accumulator version for requiredFor() to avoid infinite recursion.
- void requiredFor(std::vector<MOBase::IPlugin*>& required, std::set<MOBase::IPlugin*>& visited) const;
-
- // Retrieve the requirements from the underlying plugin, take ownership on them
- // and store them. We cannot do this in the constructor because we want to have a
- // constructed object before calling init().
- void fetchRequirements();
-
- // Set the master for this plugin. This is required to "fake" masters for proxied plugins.
- void setMaster(MOBase::IPlugin* master);
-
- friend class OrganizerCore;
- friend class PluginContainer;
-
- PluginContainer* m_PluginContainer;
- MOBase::IPlugin* m_Plugin;
- MOBase::IPluginProxy* m_PluginProxy;
- MOBase::IPlugin* m_Master;
- std::vector<std::shared_ptr<const MOBase::IPluginRequirement>> m_Requirements;
- OrganizerProxy* m_Organizer;
- std::vector<MOBase::IPlugin*> m_RequiredFor;
-
- PluginRequirements(
- PluginContainer* pluginContainer, MOBase::IPlugin* plugin,
- OrganizerProxy* proxy, MOBase::IPluginProxy* pluginProxy);
-
-};
-
-
-/**
- *
- */
-class PluginContainer : public QObject, public MOBase::IPluginDiagnose
-{
-
- Q_OBJECT
- Q_INTERFACES(MOBase::IPluginDiagnose)
-
-private:
-
- using PluginMap = boost::fusion::map<
- boost::fusion::pair<QObject, std::vector<QObject*>>,
- boost::fusion::pair<MOBase::IPlugin, std::vector<MOBase::IPlugin*>>,
- boost::fusion::pair<MOBase::IPluginDiagnose, std::vector<MOBase::IPluginDiagnose*>>,
- boost::fusion::pair<MOBase::IPluginGame, std::vector<MOBase::IPluginGame*>>,
- boost::fusion::pair<MOBase::IPluginInstaller, std::vector<MOBase::IPluginInstaller*>>,
- boost::fusion::pair<MOBase::IPluginModPage, std::vector<MOBase::IPluginModPage*>>,
- boost::fusion::pair<MOBase::IPluginPreview, std::vector<MOBase::IPluginPreview*>>,
- boost::fusion::pair<MOBase::IPluginTool, std::vector<MOBase::IPluginTool*>>,
- boost::fusion::pair<MOBase::IPluginProxy, std::vector<MOBase::IPluginProxy*>>,
- boost::fusion::pair<MOBase::IPluginFileMapper, std::vector<MOBase::IPluginFileMapper*>>
- >;
-
- using AccessPluginMap = boost::fusion::map<
- boost::fusion::pair<MOBase::IPluginDiagnose, std::map<MOBase::IPluginDiagnose*, MOBase::IPlugin*>>,
- boost::fusion::pair<MOBase::IPluginFileMapper, std::map<MOBase::IPluginFileMapper*, MOBase::IPlugin*>>,
- boost::fusion::pair<QString, std::map<QString, MOBase::IPlugin*>>
- >;
-
- static const unsigned int PROBLEM_PLUGINSNOTLOADED = 1;
-
- /**
- * This typedefs defines the order of plugin interface. This is increasing order of
- * importance".
- *
- * @note IPlugin is the less important interface, followed by IPluginDiagnose and
- * IPluginFileMapper as those are usually implemented together with another interface.
- * Other interfaces are in a alphabetical order since it is unlikely a plugin will
- * implement multiple ones.
- */
- using PluginTypeOrder =
- boost::mp11::mp_transform<
- std::add_pointer_t,
- boost::mp11::mp_list<
- MOBase::IPluginGame, MOBase::IPluginInstaller, MOBase::IPluginModPage, MOBase::IPluginPreview,
- MOBase::IPluginProxy, MOBase::IPluginTool, MOBase::IPluginDiagnose, MOBase::IPluginFileMapper,
- MOBase::IPlugin
- >
- >;
-
- static_assert(
- boost::mp11::mp_size<PluginTypeOrder>::value == boost::mp11::mp_size<PluginContainer::PluginMap>::value - 1);
-
-public:
-
- /**
- * @brief Retrieved the (localized) names of the various plugin interfaces.
- *
- * @return the (localized) names of the various plugin interfaces.
- */
- static QStringList pluginInterfaces();
-
-public:
-
- PluginContainer(OrganizerCore* organizer);
- virtual ~PluginContainer();
-
- /**
- * @brief Start the plugins.
- *
- * This function should not be called before MO2 is ready and plugins can be
- * started, and will do the following:
- * - connect the callbacks of the plugins,
- * - set the parent widget for plugins that can have one,
- * - notify plugins that MO2 has been started, including:
- * - triggering a call to the "profile changed" callback for the initial profile,
- * - triggering a call to the "user interface initialized" callback.
- *
- * @param userInterface The main user interface to use for the plugins.
- */
- void startPlugins(IUserInterface* userInterface);
-
- /**
- * @brief Load, unload or reload the plugin at the given path.
- *
- */
- void loadPlugin(QString const& filepath);
- void unloadPlugin(QString const& filepath);
- void reloadPlugin(QString const& filepath);
-
- /**
- * @brief Load all plugins.
- *
- */
- void loadPlugins();
-
- /**
- * @brief Retrieve the list of plugins of the given type.
- *
- * @return the list of plugins of the specified type.
- *
- * @tparam T The type of plugin to retrieve.
- */
- template <typename T>
- const std::vector<T*> &plugins() const {
- typename boost::fusion::result_of::at_key<const PluginMap, T>::type temp = boost::fusion::at_key<T>(m_Plugins);
- return temp;
- }
-
- /**
- * @brief Check if a plugin implement a given interface.
- *
- * @param plugin The plugin to check.
- *
- * @return true if the plugin implements the interface, false otherwise.
- *
- * @tparam The interface type.
- */
- template <typename T>
- bool implementInterface(MOBase::IPlugin* plugin) const {
- // We need a QObject to be able to qobject_cast<> to the plugin types:
- QObject* oPlugin = as_qobject(plugin);
-
- if (!oPlugin) {
- return false;
- }
-
- return qobject_cast<T*>(oPlugin);
- }
-
- /**
- * @brief Retrieve a plugin from its name or a corresponding non-IPlugin
- * interface.
- *
- * @param t Name of the plugin to retrieve, or non-IPlugin interface.
- *
- * @return the corresponding plugin, or a null pointer.
- *
- * @note It is possible to have multiple plugins for the same name when
- * dealing with proxied plugins (e.g. Python), in which case the
- * most important one will be returned, as specified in PluginTypeOrder.
- */
- MOBase::IPlugin* plugin(QString const& pluginName) const;
- MOBase::IPlugin* plugin(MOBase::IPluginDiagnose* diagnose) const;
- MOBase::IPlugin* plugin(MOBase::IPluginFileMapper* mapper) const;
-
- /**
- * @brief Find the game plugin corresponding to the given name.
- *
- * @param name The name of the game to find a plugin for (as returned by
- * IPluginGame::gameName()).
- *
- * @return the game plugin for the given name, or a null pointer if no
- * plugin exists for this game.
- */
- MOBase::IPluginGame* game(const QString& name) const;
-
- /**
- * @return the IPlugin interface to the currently managed game.
- */
- MOBase::IPlugin* managedGame() const;
-
- /**
- * @brief Check if the given plugin is enabled.
- *
- * @param plugin The plugin to check.
- *
- * @return true if the plugin is enabled, false otherwise.
- */
- bool isEnabled(MOBase::IPlugin* plugin) const;
-
- // These are friendly methods that called isEnabled(plugin(arg)).
- bool isEnabled(QString const& pluginName) const;
- bool isEnabled(MOBase::IPluginDiagnose* diagnose) const;
- bool isEnabled(MOBase::IPluginFileMapper* mapper) const;
-
- /**
- * @brief Enable or disable a plugin.
- *
- * @param plugin The plugin to enable or disable.
- * @param enable true to enable, false to disable.
- * @param dependencies If true and enable is false, dependencies will also
- * be disabled (see PluginRequirements::requiredFor).
- */
- void setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies = true);
-
- /**
- * @brief Retrieve the requirements for the given plugin.
- *
- * @param plugin The plugin to retrieve the requirements for.
- *
- * @return the requirements (as proxy) for the given plugin.
- */
- const PluginRequirements& requirements(MOBase::IPlugin* plugin) const;
-
- /**
- * @brief Retrieved the (localized) names of interfaces implemented by the given
- * plugin.
- *
- * @param plugin The plugin to retrieve interface for.
- *
- * @return the (localized) names of interfaces implemented by this plugin.
- */
- QStringList implementedInterfaces(MOBase::IPlugin* plugin) const;
-
- /**
- * @brief Return the (localized) name of the most important interface implemented by
- * the given plugin.
- *
- * The order of interfaces is defined in X.
- *
- * @param plugin The plugin to retrieve the interface for.
- *
- * @return the (localized) name of the most important interface implemented by this plugin.
- */
- QString topImplementedInterface(MOBase::IPlugin* plugin) const;
-
- /**
- * @return the preview generator.
- */
- const PreviewGenerator &previewGenerator() const;
-
- /**
- * @return the list of plugin file names, including proxied plugins.
- */
- QStringList pluginFileNames() const;
-
-public: // IPluginDiagnose interface
-
- virtual std::vector<unsigned int> activeProblems() const;
- virtual QString shortDescription(unsigned int key) const;
- virtual QString fullDescription(unsigned int key) const;
- virtual bool hasGuidedFix(unsigned int key) const;
- virtual void startGuidedFix(unsigned int key) const;
-
-signals:
-
- /**
- * @brief Emitted when plugins are enabled or disabled.
- */
- void pluginEnabled(MOBase::IPlugin*);
- void pluginDisabled(MOBase::IPlugin*);
-
- /**
- * @brief Emitted when plugins are registered or unregistered.
- */
- void pluginRegistered(MOBase::IPlugin*);
- void pluginUnregistered(MOBase::IPlugin*);
-
- void diagnosisUpdate();
-
-private:
-
- friend class PluginRequirements;
-
- // Unload all the plugins.
- void unloadPlugins();
-
- // Retrieve the organizer proxy for the given plugin.
- OrganizerProxy* organizerProxy(MOBase::IPlugin* plugin) const;
-
- // Retrieve the proxy plugin that instantiated the given plugin, or a null pointer
- // if the plugin was not instantiated by a proxy.
- MOBase::IPluginProxy* pluginProxy(MOBase::IPlugin* plugin) const;
-
- // Retrieve the path to the file or folder corresponding to the plugin.
- QString filepath(MOBase::IPlugin* plugin) const;
-
- // Load plugins from the given filepath using the given proxy.
- std::vector<QObject*> loadProxied(const QString& filepath, MOBase::IPluginProxy* proxy);
-
- // Load the Qt plugin from the given file.
- QObject* loadQtPlugin(const QString& filepath);
-
- // check if a plugin is folder containing a Qt plugin, it is, return the path to the
- // DLL containing the plugin in the folder, otherwise return an empty optional
- //
- // a Qt plugin folder is a folder with a DLL containing a library (not in a subdirectory),
- // if multiple plugins are present, only the first one is returned
- //
- // extra DLLs are ignored by Qt so can be present in the folder
- //
- std::optional<QString> isQtPluginFolder(const QString& filepath) const;
-
- // See startPlugins for more details. This is simply an intermediate function
- // that can be used when loading plugins after initialization. This uses the
- // user interface in m_UserInterface.
- void startPluginsImpl(const std::vector<QObject*>& plugins) const;
-
- /**
- * @brief Unload the given plugin.
- *
- * This function is not public because it's kind of dangerous trying to unload
- * plugin directly since some plugins are linked together.
- *
- * @param plugin The plugin to unload/unregister.
- * @param object The QObject corresponding to the plugin.
- */
- void unloadPlugin(MOBase::IPlugin* plugin, QObject* object);
-
- /**
- * @brief Retrieved the (localized) names of interfaces implemented by the given
- * plugin.
- *
- * @param plugin The plugin to retrieve interface for.
- *
- * @return the (localized) names of interfaces implemented by this plugin.
- *
- * @note This function can be used to get implemented interfaces before registering
- * a plugin.
- */
- QStringList implementedInterfaces(QObject* plugin) const;
-
- /**
- * @brief Check if a plugin implements a "better" interface than another
- * one, as specified by PluginTypeOrder.
- *
- * @param lhs, rhs The plugin to compare.
- *
- * @return true if the left plugin implements a better interface than the right
- * one, false otherwise (or if both implements the same interface).
- */
- bool isBetterInterface(QObject* lhs, QObject* rhs) const;
-
- /**
- * @brief Find the QObject* corresponding to the given plugin.
- *
- * @param plugin The plugin to find the QObject* for.
- *
- * @return a QObject* for the given plugin.
- */
- QObject* as_qobject(MOBase::IPlugin* plugin) const;
-
- /**
- * @brief Initialize a plugin.
- *
- * @param plugin The plugin to initialize.
- * @param proxy The proxy that created this plugin (can be null).
- * @param skipInit If true, IPlugin::init() will not be called, regardless
- * of the state of the container.
- *
- * @return true if the plugin was initialized correctly, false otherwise.
- */
- bool initPlugin(MOBase::IPlugin *plugin, MOBase::IPluginProxy* proxy, bool skipInit);
-
- void registerGame(MOBase::IPluginGame *game);
- void unregisterGame(MOBase::IPluginGame* game);
-
- MOBase::IPlugin* registerPlugin(QObject *pluginObj, const QString &fileName, MOBase::IPluginProxy *proxy);
-
- // Core organizer, can be null (e.g. on first MO2 startup).
- OrganizerCore *m_Organizer;
-
- // Main user interface, can be null until MW has been initialized.
- IUserInterface *m_UserInterface;
-
- PluginMap m_Plugins;
-
- // This maps allow access to IPlugin* from name or diagnose/mapper object.
- AccessPluginMap m_AccessPlugins;
-
- std::map<MOBase::IPlugin*, PluginRequirements> m_Requirements;
-
- std::map<QString, MOBase::IPluginGame*> m_SupportedGames;
- QStringList m_FailedPlugins;
- std::vector<QPluginLoader*> m_PluginLoaders;
-
- PreviewGenerator m_PreviewGenerator;
-
- QFile m_PluginsCheck;
-};
-
-
-#endif // PLUGINCONTAINER_H
+#ifndef PLUGINCONTAINER_H
+#define PLUGINCONTAINER_H
+
+#include "previewgenerator.h"
+
+class OrganizerCore;
+class IUserInterface;
+
+#include <iplugindiagnose.h>
+#include <ipluginmodpage.h>
+#include <iplugingame.h>
+#include <iplugintool.h>
+#include <ipluginproxy.h>
+#include <iplugininstaller.h>
+#include <ipluginfilemapper.h>
+#include <QtPlugin>
+#include <QPluginLoader>
+#include <QFile>
+#ifndef Q_MOC_RUN
+#include <boost/fusion/container.hpp>
+#include <boost/fusion/include/at_key.hpp>
+#include <boost/mp11.hpp>
+#endif // Q_MOC_RUN
+#include <vector>
+#include <memory>
+
+
+class OrganizerProxy;
+
+/**
+ * @brief Class that wrap multiple requirements for a plugin together. THis
+ * class owns the requirements.
+ */
+class PluginRequirements {
+public:
+
+ /**
+ * @return true if the plugin can be enabled (all requirements are met).
+ */
+ bool canEnable() const;
+
+ /**
+ * @return true if this is a core plugin, i.e. a plugin that should not be
+ * manually enabled or disabled by the user.
+ */
+ bool isCorePlugin() const;
+
+ /**
+ * @return true if this plugin has requirements (satisfied or not).
+ */
+ bool hasRequirements() const;
+
+ /**
+ * @return the proxy that created this plugin, if any.
+ */
+ MOBase::IPluginProxy* proxy() const;
+
+ /**
+ * @return the list of plugins this plugin proxies (if it's a proxy plugin).
+ */
+ std::vector<MOBase::IPlugin*> proxied() const;
+
+ /**
+ * @return the master of this plugin, if any.
+ */
+ MOBase::IPlugin* master() const;
+
+ /**
+ * @return the plugins this plugin is master of.
+ */
+ std::vector<MOBase::IPlugin*> children() const;
+
+ /**
+ * @return the list of problems to be resolved before enabling the plugin.
+ */
+ std::vector<MOBase::IPluginRequirement::Problem> problems() const;
+
+ /**
+ * @return the name of the games (gameName()) this plugin can be used with, or an empty
+ * list if this plugin does not require particular games.
+ */
+ QStringList requiredGames() const;
+
+ /**
+ * @return the list of plugins currently enabled that would have to be disabled
+ * if this plugin was disabled.
+ */
+ std::vector<MOBase::IPlugin*> requiredFor() const;
+
+private:
+
+ // The list of "Core" plugins.
+ static const std::set<QString> s_CorePlugins;
+
+ // Accumulator version for requiredFor() to avoid infinite recursion.
+ void requiredFor(std::vector<MOBase::IPlugin*>& required, std::set<MOBase::IPlugin*>& visited) const;
+
+ // Retrieve the requirements from the underlying plugin, take ownership on them
+ // and store them. We cannot do this in the constructor because we want to have a
+ // constructed object before calling init().
+ void fetchRequirements();
+
+ // Set the master for this plugin. This is required to "fake" masters for proxied plugins.
+ void setMaster(MOBase::IPlugin* master);
+
+ friend class OrganizerCore;
+ friend class PluginContainer;
+
+ PluginContainer* m_PluginContainer;
+ MOBase::IPlugin* m_Plugin;
+ MOBase::IPluginProxy* m_PluginProxy;
+ MOBase::IPlugin* m_Master;
+ std::vector<std::shared_ptr<const MOBase::IPluginRequirement>> m_Requirements;
+ OrganizerProxy* m_Organizer;
+ std::vector<MOBase::IPlugin*> m_RequiredFor;
+
+ PluginRequirements(
+ PluginContainer* pluginContainer, MOBase::IPlugin* plugin,
+ OrganizerProxy* proxy, MOBase::IPluginProxy* pluginProxy);
+
+};
+
+
+/**
+ *
+ */
+class PluginContainer : public QObject, public MOBase::IPluginDiagnose
+{
+
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPluginDiagnose)
+
+private:
+
+ using PluginMap = boost::fusion::map<
+ boost::fusion::pair<QObject, std::vector<QObject*>>,
+ boost::fusion::pair<MOBase::IPlugin, std::vector<MOBase::IPlugin*>>,
+ boost::fusion::pair<MOBase::IPluginDiagnose, std::vector<MOBase::IPluginDiagnose*>>,
+ boost::fusion::pair<MOBase::IPluginGame, std::vector<MOBase::IPluginGame*>>,
+ boost::fusion::pair<MOBase::IPluginInstaller, std::vector<MOBase::IPluginInstaller*>>,
+ boost::fusion::pair<MOBase::IPluginModPage, std::vector<MOBase::IPluginModPage*>>,
+ boost::fusion::pair<MOBase::IPluginPreview, std::vector<MOBase::IPluginPreview*>>,
+ boost::fusion::pair<MOBase::IPluginTool, std::vector<MOBase::IPluginTool*>>,
+ boost::fusion::pair<MOBase::IPluginProxy, std::vector<MOBase::IPluginProxy*>>,
+ boost::fusion::pair<MOBase::IPluginFileMapper, std::vector<MOBase::IPluginFileMapper*>>
+ >;
+
+ using AccessPluginMap = boost::fusion::map<
+ boost::fusion::pair<MOBase::IPluginDiagnose, std::map<MOBase::IPluginDiagnose*, MOBase::IPlugin*>>,
+ boost::fusion::pair<MOBase::IPluginFileMapper, std::map<MOBase::IPluginFileMapper*, MOBase::IPlugin*>>,
+ boost::fusion::pair<QString, std::map<QString, MOBase::IPlugin*>>
+ >;
+
+ static const unsigned int PROBLEM_PLUGINSNOTLOADED = 1;
+
+ /**
+ * This typedefs defines the order of plugin interface. This is increasing order of
+ * importance".
+ *
+ * @note IPlugin is the less important interface, followed by IPluginDiagnose and
+ * IPluginFileMapper as those are usually implemented together with another interface.
+ * Other interfaces are in a alphabetical order since it is unlikely a plugin will
+ * implement multiple ones.
+ */
+ using PluginTypeOrder =
+ boost::mp11::mp_transform<
+ std::add_pointer_t,
+ boost::mp11::mp_list<
+ MOBase::IPluginGame, MOBase::IPluginInstaller, MOBase::IPluginModPage, MOBase::IPluginPreview,
+ MOBase::IPluginProxy, MOBase::IPluginTool, MOBase::IPluginDiagnose, MOBase::IPluginFileMapper,
+ MOBase::IPlugin
+ >
+ >;
+
+ static_assert(
+ boost::mp11::mp_size<PluginTypeOrder>::value == boost::mp11::mp_size<PluginContainer::PluginMap>::value - 1);
+
+public:
+
+ /**
+ * @brief Retrieved the (localized) names of the various plugin interfaces.
+ *
+ * @return the (localized) names of the various plugin interfaces.
+ */
+ static QStringList pluginInterfaces();
+
+public:
+
+ PluginContainer(OrganizerCore* organizer);
+ virtual ~PluginContainer();
+
+ /**
+ * @brief Start the plugins.
+ *
+ * This function should not be called before MO2 is ready and plugins can be
+ * started, and will do the following:
+ * - connect the callbacks of the plugins,
+ * - set the parent widget for plugins that can have one,
+ * - notify plugins that MO2 has been started, including:
+ * - triggering a call to the "profile changed" callback for the initial profile,
+ * - triggering a call to the "user interface initialized" callback.
+ *
+ * @param userInterface The main user interface to use for the plugins.
+ */
+ void startPlugins(IUserInterface* userInterface);
+
+ /**
+ * @brief Load, unload or reload the plugin at the given path.
+ *
+ */
+ void loadPlugin(QString const& filepath);
+ void unloadPlugin(QString const& filepath);
+ void reloadPlugin(QString const& filepath);
+
+ /**
+ * @brief Load all plugins.
+ *
+ */
+ void loadPlugins();
+
+ /**
+ * @brief Retrieve the list of plugins of the given type.
+ *
+ * @return the list of plugins of the specified type.
+ *
+ * @tparam T The type of plugin to retrieve.
+ */
+ template <typename T>
+ const std::vector<T*> &plugins() const {
+ typename boost::fusion::result_of::at_key<const PluginMap, T>::type temp = boost::fusion::at_key<T>(m_Plugins);
+ return temp;
+ }
+
+ /**
+ * @brief Check if a plugin implement a given interface.
+ *
+ * @param plugin The plugin to check.
+ *
+ * @return true if the plugin implements the interface, false otherwise.
+ *
+ * @tparam The interface type.
+ */
+ template <typename T>
+ bool implementInterface(MOBase::IPlugin* plugin) const {
+ // We need a QObject to be able to qobject_cast<> to the plugin types:
+ QObject* oPlugin = as_qobject(plugin);
+
+ if (!oPlugin) {
+ return false;
+ }
+
+ return qobject_cast<T*>(oPlugin);
+ }
+
+ /**
+ * @brief Retrieve a plugin from its name or a corresponding non-IPlugin
+ * interface.
+ *
+ * @param t Name of the plugin to retrieve, or non-IPlugin interface.
+ *
+ * @return the corresponding plugin, or a null pointer.
+ *
+ * @note It is possible to have multiple plugins for the same name when
+ * dealing with proxied plugins (e.g. Python), in which case the
+ * most important one will be returned, as specified in PluginTypeOrder.
+ */
+ MOBase::IPlugin* plugin(QString const& pluginName) const;
+ MOBase::IPlugin* plugin(MOBase::IPluginDiagnose* diagnose) const;
+ MOBase::IPlugin* plugin(MOBase::IPluginFileMapper* mapper) const;
+
+ /**
+ * @brief Find the game plugin corresponding to the given name.
+ *
+ * @param name The name of the game to find a plugin for (as returned by
+ * IPluginGame::gameName()).
+ *
+ * @return the game plugin for the given name, or a null pointer if no
+ * plugin exists for this game.
+ */
+ MOBase::IPluginGame* game(const QString& name) const;
+
+ /**
+ * @return the IPlugin interface to the currently managed game.
+ */
+ MOBase::IPlugin* managedGame() const;
+
+ /**
+ * @brief Check if the given plugin is enabled.
+ *
+ * @param plugin The plugin to check.
+ *
+ * @return true if the plugin is enabled, false otherwise.
+ */
+ bool isEnabled(MOBase::IPlugin* plugin) const;
+
+ // These are friendly methods that called isEnabled(plugin(arg)).
+ bool isEnabled(QString const& pluginName) const;
+ bool isEnabled(MOBase::IPluginDiagnose* diagnose) const;
+ bool isEnabled(MOBase::IPluginFileMapper* mapper) const;
+
+ /**
+ * @brief Enable or disable a plugin.
+ *
+ * @param plugin The plugin to enable or disable.
+ * @param enable true to enable, false to disable.
+ * @param dependencies If true and enable is false, dependencies will also
+ * be disabled (see PluginRequirements::requiredFor).
+ */
+ void setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies = true);
+
+ /**
+ * @brief Retrieve the requirements for the given plugin.
+ *
+ * @param plugin The plugin to retrieve the requirements for.
+ *
+ * @return the requirements (as proxy) for the given plugin.
+ */
+ const PluginRequirements& requirements(MOBase::IPlugin* plugin) const;
+
+ /**
+ * @brief Retrieved the (localized) names of interfaces implemented by the given
+ * plugin.
+ *
+ * @param plugin The plugin to retrieve interface for.
+ *
+ * @return the (localized) names of interfaces implemented by this plugin.
+ */
+ QStringList implementedInterfaces(MOBase::IPlugin* plugin) const;
+
+ /**
+ * @brief Return the (localized) name of the most important interface implemented by
+ * the given plugin.
+ *
+ * The order of interfaces is defined in X.
+ *
+ * @param plugin The plugin to retrieve the interface for.
+ *
+ * @return the (localized) name of the most important interface implemented by this plugin.
+ */
+ QString topImplementedInterface(MOBase::IPlugin* plugin) const;
+
+ /**
+ * @return the preview generator.
+ */
+ const PreviewGenerator &previewGenerator() const;
+
+ /**
+ * @return the list of plugin file names, including proxied plugins.
+ */
+ QStringList pluginFileNames() const;
+
+public: // IPluginDiagnose interface
+
+ virtual std::vector<unsigned int> activeProblems() const;
+ virtual QString shortDescription(unsigned int key) const;
+ virtual QString fullDescription(unsigned int key) const;
+ virtual bool hasGuidedFix(unsigned int key) const;
+ virtual void startGuidedFix(unsigned int key) const;
+
+signals:
+
+ /**
+ * @brief Emitted when plugins are enabled or disabled.
+ */
+ void pluginEnabled(MOBase::IPlugin*);
+ void pluginDisabled(MOBase::IPlugin*);
+
+ /**
+ * @brief Emitted when plugins are registered or unregistered.
+ */
+ void pluginRegistered(MOBase::IPlugin*);
+ void pluginUnregistered(MOBase::IPlugin*);
+
+ void diagnosisUpdate();
+
+private:
+
+ friend class PluginRequirements;
+
+ // Unload all the plugins.
+ void unloadPlugins();
+
+ // Retrieve the organizer proxy for the given plugin.
+ OrganizerProxy* organizerProxy(MOBase::IPlugin* plugin) const;
+
+ // Retrieve the proxy plugin that instantiated the given plugin, or a null pointer
+ // if the plugin was not instantiated by a proxy.
+ MOBase::IPluginProxy* pluginProxy(MOBase::IPlugin* plugin) const;
+
+ // Retrieve the path to the file or folder corresponding to the plugin.
+ QString filepath(MOBase::IPlugin* plugin) const;
+
+ // Load plugins from the given filepath using the given proxy.
+ std::vector<QObject*> loadProxied(const QString& filepath, MOBase::IPluginProxy* proxy);
+
+ // Load the Qt plugin from the given file.
+ QObject* loadQtPlugin(const QString& filepath);
+
+ // check if a plugin is folder containing a Qt plugin, it is, return the path to the
+ // DLL containing the plugin in the folder, otherwise return an empty optional
+ //
+ // a Qt plugin folder is a folder with a DLL containing a library (not in a subdirectory),
+ // if multiple plugins are present, only the first one is returned
+ //
+ // extra DLLs are ignored by Qt so can be present in the folder
+ //
+ std::optional<QString> isQtPluginFolder(const QString& filepath) const;
+
+ // See startPlugins for more details. This is simply an intermediate function
+ // that can be used when loading plugins after initialization. This uses the
+ // user interface in m_UserInterface.
+ void startPluginsImpl(const std::vector<QObject*>& plugins) const;
+
+ /**
+ * @brief Unload the given plugin.
+ *
+ * This function is not public because it's kind of dangerous trying to unload
+ * plugin directly since some plugins are linked together.
+ *
+ * @param plugin The plugin to unload/unregister.
+ * @param object The QObject corresponding to the plugin.
+ */
+ void unloadPlugin(MOBase::IPlugin* plugin, QObject* object);
+
+ /**
+ * @brief Retrieved the (localized) names of interfaces implemented by the given
+ * plugin.
+ *
+ * @param plugin The plugin to retrieve interface for.
+ *
+ * @return the (localized) names of interfaces implemented by this plugin.
+ *
+ * @note This function can be used to get implemented interfaces before registering
+ * a plugin.
+ */
+ QStringList implementedInterfaces(QObject* plugin) const;
+
+ /**
+ * @brief Check if a plugin implements a "better" interface than another
+ * one, as specified by PluginTypeOrder.
+ *
+ * @param lhs, rhs The plugin to compare.
+ *
+ * @return true if the left plugin implements a better interface than the right
+ * one, false otherwise (or if both implements the same interface).
+ */
+ bool isBetterInterface(QObject* lhs, QObject* rhs) const;
+
+ /**
+ * @brief Find the QObject* corresponding to the given plugin.
+ *
+ * @param plugin The plugin to find the QObject* for.
+ *
+ * @return a QObject* for the given plugin.
+ */
+ QObject* as_qobject(MOBase::IPlugin* plugin) const;
+
+ /**
+ * @brief Initialize a plugin.
+ *
+ * @param plugin The plugin to initialize.
+ * @param proxy The proxy that created this plugin (can be null).
+ * @param skipInit If true, IPlugin::init() will not be called, regardless
+ * of the state of the container.
+ *
+ * @return true if the plugin was initialized correctly, false otherwise.
+ */
+ bool initPlugin(MOBase::IPlugin *plugin, MOBase::IPluginProxy* proxy, bool skipInit);
+
+ void registerGame(MOBase::IPluginGame *game);
+ void unregisterGame(MOBase::IPluginGame* game);
+
+ MOBase::IPlugin* registerPlugin(QObject *pluginObj, const QString &fileName, MOBase::IPluginProxy *proxy);
+
+ // Core organizer, can be null (e.g. on first MO2 startup).
+ OrganizerCore *m_Organizer;
+
+ // Main user interface, can be null until MW has been initialized.
+ IUserInterface *m_UserInterface;
+
+ PluginMap m_Plugins;
+
+ // This maps allow access to IPlugin* from name or diagnose/mapper object.
+ AccessPluginMap m_AccessPlugins;
+
+ std::map<MOBase::IPlugin*, PluginRequirements> m_Requirements;
+
+ std::map<QString, MOBase::IPluginGame*> m_SupportedGames;
+ QStringList m_FailedPlugins;
+ std::vector<QPluginLoader*> m_PluginLoaders;
+
+ PreviewGenerator m_PreviewGenerator;
+
+ QFile m_PluginsCheck;
+};
+
+
+#endif // PLUGINCONTAINER_H
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp
index 28e0af51..a4742529 100644
--- a/src/pluginlist.cpp
+++ b/src/pluginlist.cpp
@@ -1,1721 +1,1721 @@
-/*
-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 "pluginlist.h"
-#include "settings.h"
-#include "scopeguard.h"
-#include "modinfo.h"
-#include "modlist.h"
-#include "viewmarkingscrollbar.h"
-#include "shared/directoryentry.h"
-#include "shared/filesorigin.h"
-#include "shared/fileentry.h"
-
-#include <utility.h>
-#include <iplugingame.h>
-#include <espfile.h>
-#include <report.h>
-#include "shared/windows_error.h"
-#include <safewritefile.h>
-#include <gameplugins.h>
-
-#include <QtDebug>
-#include <QMessageBox>
-#include <QMimeData>
-#include <QCoreApplication>
-#include <QDateTime>
-#include <QDir>
-#include <QFile>
-#include <QFileInfo>
-#include <QFileInfo>
-#include <QListWidgetItem>
-#include <QRegularExpression>
-#include <QString>
-#include <QApplication>
-#include <QKeyEvent>
-#include <QSortFilterProxyModel>
-
-#include <ctime>
-#include <algorithm>
-#include <stdexcept>
-
-#include "organizercore.h"
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-static QString TruncateString(const QString& text)
-{
- QString new_text = text;
-
- if (new_text.length() > 1024) {
- new_text.truncate(1024);
- new_text += "...";
- }
-
- return new_text;
-}
-
-
-PluginList::PluginList(OrganizerCore& organizer)
- : QAbstractItemModel(&organizer)
- , m_Organizer(organizer)
- , m_FontMetrics(QFont())
-{
- connect(this, SIGNAL(writePluginsList()), this, SLOT(generatePluginIndexes()));
- m_LastCheck.start();
-}
-
-PluginList::~PluginList()
-{
- m_Refreshed.disconnect_all_slots();
- m_PluginMoved.disconnect_all_slots();
- m_PluginStateChanged.disconnect_all_slots();
-}
-
-QString PluginList::getColumnName(int column)
-{
- switch (column) {
- case COL_NAME: return tr("Name");
- case COL_PRIORITY: return tr("Priority");
- case COL_MODINDEX: return tr("Mod Index");
- case COL_FLAGS: return tr("Flags");
- default: return tr("unknown");
- }
-}
-
-
-QString PluginList::getColumnToolTip(int column)
-{
- switch (column) {
- case COL_NAME: return tr("Name of the plugin");
- case COL_FLAGS: return tr("Emblems to highlight things that might require attention.");
- case COL_PRIORITY: return tr("Load priority of plugins. The higher, the more \"important\" it is and thus "
- "overwrites data from plugins with lower priority.");
- case COL_MODINDEX: return tr("Determines the formids of objects originating from this mods.");
- default: return tr("unknown");
- }
-}
-
-void PluginList::highlightPlugins(
- const std::vector<unsigned int>& modIndices,
- const MOShared::DirectoryEntry &directoryEntry)
-{
- auto* profile = m_Organizer.currentProfile();
-
- for (auto &esp : m_ESPs) {
- esp.modSelected = false;
- }
-
- for (auto& modIndex : modIndices) {
- ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex);
- if (!selectedMod.isNull() && profile->modEnabled(modIndex)) {
- QDir dir(selectedMod->absolutePath());
- QStringList plugins = dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl");
- const MOShared::FilesOrigin& origin = directoryEntry.getOriginByName(selectedMod->internalName().toStdWString());
- if (plugins.size() > 0) {
- for (auto plugin : plugins) {
- MOShared::FileEntryPtr file = directoryEntry.findFile(plugin.toStdWString());
- if (file && file->getOrigin() != origin.getID()) {
- const auto alternatives = file->getAlternatives();
- if (std::find_if(alternatives.begin(), alternatives.end(), [&](const FileAlternative& element) { return element.originID() == origin.getID(); }) == alternatives.end())
- continue;
- }
- std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
- if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].modSelected = true;
- }
- }
- }
- }
- }
-
- emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
-}
-
-void PluginList::refresh(const QString &profileName
- , const DirectoryEntry &baseDirectory
- , const QString &lockedOrderFile
- , bool force)
-{
- TimeThis tt("PluginList::refresh()");
-
- if (force) {
- m_ESPs.clear();
- m_ESPsByName.clear();
- m_ESPsByPriority.clear();
- }
-
- ChangeBracket<PluginList> layoutChange(this);
-
- QStringList primaryPlugins = m_GamePlugin->primaryPlugins();
- GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
- const bool lightPluginsAreSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
-
- m_CurrentProfile = profileName;
-
- QStringList availablePlugins;
-
- std::vector<FileEntryPtr> files = baseDirectory.getFiles();
- for (FileEntryPtr current : files) {
- if (current.get() == nullptr) {
- continue;
- }
- QString filename = ToQString(current->getName());
-
- if (filename.endsWith(".esp", Qt::CaseInsensitive) ||
- filename.endsWith(".esm", Qt::CaseInsensitive) ||
- filename.endsWith(".esl", Qt::CaseInsensitive)) {
-
- availablePlugins.append(filename);
-
- if (m_ESPsByName.find(filename) != m_ESPsByName.end()) {
- continue;
- }
-
- bool forceEnabled = Settings::instance().game().forceEnableCoreFiles() &&
- primaryPlugins.contains(filename, Qt::CaseInsensitive);
-
- bool archive = false;
- try {
- FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive));
-
- //name without extension
- QString baseName = QFileInfo(filename).completeBaseName();
-
- QString iniPath = baseName + ".ini";
- bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr;
- std::set<QString> loadedArchives;
- QString candidateName;
- for (FileEntryPtr archiveCandidate : files) {
- candidateName = ToQString(archiveCandidate->getName());
- if (candidateName.startsWith(baseName, Qt::CaseInsensitive) &&
- (candidateName.endsWith(".bsa", Qt::CaseInsensitive) ||
- candidateName.endsWith(".ba2", Qt::CaseInsensitive))) {
- loadedArchives.insert(candidateName);
- }
- }
-
- QString originName = ToQString(origin.getName());
- unsigned int modIndex = ModInfo::getIndex(originName);
- if (modIndex != UINT_MAX) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- originName = modInfo->name();
- }
-
- m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported));
- m_ESPs.rbegin()->priority = -1;
- } catch (const std::exception &e) {
- reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what()));
- }
- }
- }
-
- for (const auto &espName : m_ESPsByName) {
- if (!availablePlugins.contains(espName.first, Qt::CaseInsensitive)) {
- m_ESPs[espName.second].name = "";
- }
- }
-
- m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(),
- [](const ESPInfo &info) -> bool {
- return info.name.isEmpty();
- }),
- m_ESPs.end());
-
- fixPriorities();
-
- // functions in GamePlugins will use the IPluginList interface of this, so
- // indices need to work. priority will be off however
- updateIndices();
-
- if (gamePlugins) {
- gamePlugins->readPluginLists(m_Organizer.managedGameOrganizer()->pluginList());
- }
-
- fixPrimaryPlugins();
- fixPluginRelationships();
-
- testMasters();
-
- updateIndices();
-
- readLockedOrderFrom(lockedOrderFile);
-
- layoutChange.finish();
-
- refreshLoadOrder();
- emit dataChanged(this->index(0, 0),
- this->index(static_cast<int>(m_ESPs.size()), columnCount()));
-
- m_Refreshed();
-}
-
-void PluginList::fixPrimaryPlugins()
-{
- if (!m_Organizer.settings().game().forceEnableCoreFiles()) {
- return;
- }
-
- // This function ensures that the primary plugins are first and in the correct order
- QStringList primaryPlugins = m_Organizer.managedGame()->primaryPlugins();
- int prio = 0;
- bool somethingChanged = false;
- for (QString plugin : primaryPlugins) {
- std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
- // Plugin is present?
- if (iter != m_ESPsByName.end()) {
- if (prio != m_ESPs[iter->second].priority) {
- // Priority is wrong! Fix it!
- int newPrio = prio;
- setPluginPriority(iter->second, newPrio, true /* isForced */);
- somethingChanged = true;
- }
- prio++;
- }
- }
-
- if (somethingChanged) {
- writePluginsList();
- }
-}
-
-void PluginList::fixPluginRelationships()
-{
- TimeThis timer("PluginList::fixPluginRelationships");
-
- // Count the types of plugins
- int masterCount = 0;
- for (auto plugin : m_ESPs) {
- if (plugin.hasLightExtension ||
- plugin.hasMasterExtension ||
- plugin.isMasterFlagged) {
- masterCount++;
- }
- }
-
- // Ensure masters are up top and normal plugins are down below
- for (int i = 0; i < m_ESPs.size(); i++) {
- ESPInfo& plugin = m_ESPs[i];
- if (plugin.hasLightExtension ||
- plugin.hasMasterExtension ||
- plugin.isMasterFlagged) {
- if (plugin.priority > masterCount) {
- int newPriority = masterCount + 1;
- setPluginPriority(i, newPriority);
- }
- }
- else {
- if (plugin.priority < masterCount) {
- int newPriority = masterCount + 1;
- setPluginPriority(i, newPriority);
- }
- }
- }
-
- // Ensure master/child relationships are observed
- for (int i = 0; i < m_ESPs.size(); i++) {
- ESPInfo& plugin = m_ESPs[i];
- int newPriority = plugin.priority;
- for (auto master : plugin.masters) {
- auto iter = m_ESPsByName.find(master);
- if (iter != m_ESPsByName.end()) {
- newPriority = std::max(newPriority, m_ESPs[iter->second].priority);
- }
- }
- if (newPriority != plugin.priority) {
- setPluginPriority(i, newPriority);
- }
- }
-}
-
-void PluginList::fixPriorities()
-{
- std::vector<std::pair<int, int>> espPrios;
-
- for (int i = 0; i < m_ESPs.size(); ++i) {
- int prio = m_ESPs[i].priority;
- if (prio == -1) {
- prio = INT_MAX;
- }
- espPrios.push_back(std::make_pair(prio, i));
- }
-
- std::sort(espPrios.begin(), espPrios.end(),
- [](const std::pair<int, int> &lhs, const std::pair<int, int> &rhs) {
- return lhs.first < rhs.first;
- });
-
- for (int i = 0; i < espPrios.size(); ++i) {
- m_ESPs[espPrios[i].second].priority = i;
- }
-}
-
-void PluginList::enableESP(const QString &name, bool enable)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- auto enabled = m_ESPs[iter->second].enabled;
- m_ESPs[iter->second].enabled =
- enable || m_ESPs[iter->second].forceEnabled;
-
- emit writePluginsList();
- if (enabled != m_ESPs[iter->second].enabled) {
- pluginStatesChanged({ name }, state(name));
- }
- } else {
- reportError(tr("Plugin not found: %1").arg(qUtf8Printable(name)));
- }
-}
-
-int PluginList::findPluginByPriority(int priority)
-{
- for (int i = 0; i < m_ESPs.size(); i++ ) {
- if (m_ESPs[i].priority == priority) {
- return i;
- }
- }
- log::error("No plugin with priority {}", priority);
- return -1;
-}
-
-void PluginList::setEnabled(const QModelIndexList& indices, bool enabled)
-{
- QStringList dirty;
- for (auto& idx : indices) {
- if (m_ESPs[idx.row()].enabled != enabled) {
- m_ESPs[idx.row()].enabled = enabled;
- dirty.append(m_ESPs[idx.row()].name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty,
- enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
- }
-}
-
-void PluginList::setEnabledAll(bool enabled)
-{
- QStringList dirty;
- for (ESPInfo &info : m_ESPs) {
- if (info.enabled != enabled) {
- info.enabled = enabled;
- dirty.append(info.name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty,
- enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
- }
-}
-
-void PluginList::sendToPriority(const QModelIndexList& indices, int newPriority)
-{
- std::vector<int> pluginsToMove;
- for (auto& idx : indices) {
- if (!m_ESPs[idx.row()].forceEnabled) {
- pluginsToMove.push_back(idx.row());
- }
- }
- if (pluginsToMove.size()) {
- changePluginPriority(pluginsToMove, newPriority);
- }
-}
-
-void PluginList::shiftPluginsPriority(const QModelIndexList& indices, int offset)
-{
- // retrieve the plugin index and sort them by priority to avoid issue
- // when moving them
- std::vector<int> allIndex;
- for (auto& idx : indices) {
- allIndex.push_back(idx.row());
- }
- std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) {
- bool cmp = m_ESPs[lhs].priority < m_ESPs[rhs].priority;
- return offset > 0 ? !cmp : cmp;
- });
-
- for (auto index : allIndex) {
- int newPriority = m_ESPs[index].priority + offset;
- if (newPriority >= 0 && newPriority < rowCount()) {
- setPluginPriority(index, newPriority);
- }
- }
-
- refreshLoadOrder();
-}
-
-void PluginList::toggleState(const QModelIndexList& indices)
-{
- QModelIndex minRow, maxRow;
- for (auto& idx : indices) {
- if (!minRow.isValid() || (idx.row() < minRow.row())) {
- minRow = idx;
- }
- if (!maxRow.isValid() || (idx.row() > maxRow.row())) {
- maxRow = idx;
- }
- int oldState = idx.data(Qt::CheckStateRole).toInt();
- setData(idx, oldState == Qt::Unchecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
- }
-
- emit dataChanged(minRow, maxRow);
-}
-
-bool PluginList::isEnabled(const QString &name)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- return m_ESPs[iter->second].enabled;
- } else {
- return false;
- }
-}
-
-void PluginList::clearInformation(const QString &name)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name].messages.clear();
- }
-}
-
-void PluginList::clearAdditionalInformation()
-{
- m_AdditionalInfo.clear();
-}
-
-void PluginList::addInformation(const QString &name, const QString &message)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name].messages.append(message);
- } else {
- log::warn("failed to associate message for \"{}\"", name);
- }
-}
-
-void PluginList::addLootReport(const QString& name, Loot::Plugin plugin)
-{
- auto iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name].loot = std::move(plugin);
- } else {
- log::warn("failed to associate loot report for \"{}\"", name);
- }
-}
-
-bool PluginList::isEnabled(int index)
-{
- return m_ESPs.at(index).enabled;
-}
-
-void PluginList::readLockedOrderFrom(const QString &fileName)
-{
- m_LockedOrder.clear();
-
- QFile file(fileName);
- if (!file.exists()) {
- // no locked load order, that's ok
- return;
- }
-
- file.open(QIODevice::ReadOnly);
- int lineNumber = 0;
- while (!file.atEnd())
- {
- QByteArray line = file.readLine();
- ++lineNumber;
-
- // Skip empty lines or commented out lines (#)
- if ((line.size() <= 0) || (line.at(0) == '#'))
- {
- continue;
- }
-
- QList<QByteArray> fields = line.split('|');
- if (fields.count() != 2)
- {
- // Don't know how to parse this so run away
- log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
- continue;
- }
-
- // Read the plugin name and priority
- QString pluginName = QString::fromUtf8(fields.at(0));
- int priority = fields.at(1).trimmed().toInt();
- if (priority < 0)
- {
- // WTF do you mean a negative priority?
- log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
- continue;
- }
-
- // Determine the index of the plugin
- auto it = m_ESPsByName.find(pluginName);
- if (it == m_ESPsByName.end())
- {
- // Plugin does not exist in the current set of plugins
- m_LockedOrder[pluginName] = priority;
- continue;
- }
- int pluginIndex = it->second;
-
- // Do not allow locking forced plugins
- if (m_ESPs[pluginIndex].forceEnabled)
- {
- continue;
- }
-
- // If the priority is larger than the number of plugins, just keep it locked
- if (priority >= m_ESPsByPriority.size())
- {
- m_LockedOrder[pluginName] = priority;
- continue;
- }
-
- // These are some helper functions for figuring out what is already locked
- auto findLocked = [&](const std::pair<QString, int>& a) { return a.second == priority; };
- auto alreadyLocked = [&](){ return std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), findLocked) != m_LockedOrder.end(); };
-
- // See if we can just set the given priority
- if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
- {
- m_LockedOrder[pluginName] = priority;
- continue;
- }
-
- // Find the next higher priority we can set the plugin to
- while (++priority < m_ESPs.size())
- {
- if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
- {
- m_LockedOrder[pluginName] = priority;
- break;
- }
- }
-
- // See if we walked off the end of the plugin list
- if (priority >= m_ESPs.size())
- {
- // I guess go ahead and lock it here at the end of the list?
- m_LockedOrder[pluginName] = priority;
- continue;
- }
- } /* while (!file.atEnd()) */
- file.close();
-}
-
-void PluginList::writeLockedOrder(const QString &fileName) const
-{
- SafeWriteFile file(fileName);
-
- file->resize(0);
- file->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8());
- for (auto iter = m_LockedOrder.begin(); iter != m_LockedOrder.end(); ++iter) {
- file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8());
- }
- file.commit();
-}
-
-void PluginList::saveTo(const QString &lockedOrderFileName) const
-{
- GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
- if (gamePlugins) {
- gamePlugins->writePluginLists(m_Organizer.managedGameOrganizer()->pluginList());
- }
-
- writeLockedOrder(lockedOrderFileName);
-}
-
-
-bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure)
-{
- if (m_GamePlugin->loadOrderMechanism() != IPluginGame::LoadOrderMechanism::FileTime) {
- // nothing to do
- return true;
- }
-
- log::debug("setting file times on esps");
-
- for (ESPInfo &esp : m_ESPs) {
- std::wstring espName = ToWString(esp.name);
- const FileEntryPtr fileEntry = directoryStructure.findFile(espName);
- if (fileEntry.get() != nullptr) {
- QString fileName;
- bool archive = false;
- int originid = fileEntry->getOrigin(archive);
-
- fileName = QString("%1\\%2")
- .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath())))
- .arg(esp.name);
-
- HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE,
- 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
- if (file == INVALID_HANDLE_VALUE) {
- if (::GetLastError() == ERROR_SHARING_VIOLATION) {
- // file is locked, probably the game is running
- return false;
- } else {
- throw windows_error(QObject::tr("failed to access %1").arg(fileName).toUtf8().constData());
- }
- }
-
- ULONGLONG temp = 0;
- temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
-
- FILETIME newWriteTime;
-
- newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
- newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
- esp.time = newWriteTime;
- fileEntry->setFileTime(newWriteTime);
- if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
- throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData());
- }
-
- CloseHandle(file);
- }
- }
- return true;
-}
-
-int PluginList::enabledCount() const
-{
- int enabled = 0;
- for (const auto &info : m_ESPs) {
- if (info.enabled) {
- ++enabled;
- }
- }
- return enabled;
-}
-
-QString PluginList::getIndexPriority(int index) const
-{
- return m_ESPs[index].index;
-}
-
-bool PluginList::isESPLocked(int index) const
-{
- return m_LockedOrder.find(m_ESPs.at(index).name) != m_LockedOrder.end();
-}
-
-void PluginList::lockESPIndex(int index, bool lock)
-{
- if (lock) {
- if (!m_ESPs.at(index).forceEnabled)
- m_LockedOrder[getName(index)] = m_ESPs.at(index).loadOrder;
- else
- return;
- } else {
- auto iter = m_LockedOrder.find(getName(index));
- if (iter != m_LockedOrder.end()) {
- m_LockedOrder.erase(iter);
- }
- }
- emit writePluginsList();
-}
-
-void PluginList::syncLoadOrder()
-{
- int loadOrder = 0;
- for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
- int index = m_ESPsByPriority[i];
-
- if (m_ESPs[index].enabled) {
- m_ESPs[index].loadOrder = loadOrder++;
- } else {
- m_ESPs[index].loadOrder = -1;
- }
- }
-}
-
-void PluginList::refreshLoadOrder()
-{
- ChangeBracket<PluginList> layoutChange(this);
- syncLoadOrder();
- // set priorities according to locked load order
- std::map<int, QString> lockedLoadOrder;
- std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(),
- [&lockedLoadOrder] (const std::pair<QString, int> &ele) {
- lockedLoadOrder[ele.second] = ele.first; });
-
- int targetPrio = 0;
- bool savePluginsList = false;
- // this is guaranteed to iterate from lowest key (load order) to highest
- for (auto iter = lockedLoadOrder.begin(); iter != lockedLoadOrder.end(); ++iter) {
- auto nameIter = m_ESPsByName.find(iter->second);
- if (nameIter != m_ESPsByName.end()) {
- // locked esp exists
-
- // find the location to insert at
- while ((targetPrio < static_cast<int>(m_ESPs.size() - 1)) &&
- (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
- ++targetPrio;
- }
-
- if (static_cast<size_t>(targetPrio) >= m_ESPs.size()) {
- continue;
- }
-
- int temp = targetPrio;
- int index = nameIter->second;
- if (m_ESPs[index].priority != temp) {
- setPluginPriority(index, temp);
- m_ESPs[index].loadOrder = iter->first;
- syncLoadOrder();
- savePluginsList = true;
- }
- }
- }
- if (savePluginsList) {
- emit writePluginsList();
- }
-}
-
-void PluginList::disconnectSlots() {
- m_PluginMoved.disconnect_all_slots();
- m_Refreshed.disconnect_all_slots();
- m_PluginStateChanged.disconnect_all_slots();
-}
-
-int PluginList::timeElapsedSinceLastChecked() const
-{
- return m_LastCheck.elapsed();
-}
-
-QStringList PluginList::pluginNames() const
-{
- QStringList result;
-
- for (const ESPInfo &info : m_ESPs) {
- result.append(info.name);
- }
-
- return result;
-}
-
-IPluginList::PluginStates PluginList::state(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return IPluginList::STATE_MISSING;
- } else {
- return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
- }
-}
-
-void PluginList::setState(const QString &name, PluginStates state) {
- auto iter = m_ESPsByName.find(name);
- if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) ||
- m_ESPs[iter->second].forceEnabled;
- } else {
- log::warn("Plugin not found: {}", name);
- }
-}
-
-void PluginList::setLoadOrder(const QStringList &pluginList)
-{
- for (ESPInfo &info : m_ESPs) {
- info.priority = -1;
- }
- int maxPriority = 0;
- for (const QString &plugin : pluginList) {
- auto iter = m_ESPsByName.find(plugin);
- if (iter !=m_ESPsByName.end()) {
- m_ESPs[iter->second].priority = maxPriority++;
- }
- }
-
- // use old priorities
- for (ESPInfo &info : m_ESPs) {
- if (info.priority == -1) {
- info.priority = maxPriority++;
- }
- }
- updateIndices();
-}
-
-int PluginList::priority(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return -1;
- } else {
- return m_ESPs[iter->second].priority;
- }
-}
-
-bool PluginList::setPriority(const QString& name, int newPriority) {
-
- if (newPriority < 0 || newPriority >= static_cast<int>(m_ESPsByPriority.size())) {
- return false;
- }
-
- auto oldPriority = priority(name);
- if (oldPriority == -1) {
- return false;
- }
-
- int rowIndex = findPluginByPriority(oldPriority);
-
- // We need to increment newPriority if its above the old one, otherwise the
- // plugin is place right below the new priority.
- if (oldPriority < newPriority) {
- newPriority += 1;
- }
- changePluginPriority({ rowIndex }, newPriority);
-
- return true;
-}
-
-int PluginList::loadOrder(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return -1;
- } else {
- return m_ESPs[iter->second].loadOrder;
- }
-}
-
-QStringList PluginList::masters(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return QStringList();
- } else {
- QStringList result;
- for (const QString &master : m_ESPs[iter->second].masters) {
- result.append(master);
- }
- return result;
- }
-}
-
-QString PluginList::origin(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return QString();
- } else {
- return m_ESPs[iter->second].originName;
- }
-}
-
-bool PluginList::hasMasterExtension(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].hasMasterExtension;
- }
-}
-
-bool PluginList::hasLightExtension(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].hasLightExtension;
- }
-}
-
-bool PluginList::isMasterFlagged(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].isMasterFlagged;
- }
-}
-
-bool PluginList::isLightFlagged(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].isLightFlagged;
- }
-}
-
-boost::signals2::connection PluginList::onPluginStateChanged(const std::function<void(const std::map<QString, PluginStates>&)>& func)
-{
- return m_PluginStateChanged.connect(func);
-}
-
-void PluginList::pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const {
- if (pluginNames.isEmpty()) {
- return;
- }
- std::map<QString, IPluginList::PluginStates> infos;
- for (auto& name : pluginNames) {
- infos[name] = state;
- }
- m_PluginStateChanged(infos);
-}
-
-boost::signals2::connection PluginList::onRefreshed(const std::function<void ()> &callback)
-{
- return m_Refreshed.connect(callback);
-}
-
-boost::signals2::connection PluginList::onPluginMoved(const std::function<void (const QString &, int, int)> &func)
-{
- return m_PluginMoved.connect(func);
-}
-
-void PluginList::updateIndices()
-{
- m_ESPsByName.clear();
- m_ESPsByPriority.clear();
- m_ESPsByPriority.resize(m_ESPs.size());
- for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
- if (m_ESPs[i].priority < 0) {
- continue;
- }
- if (m_ESPs[i].priority >= static_cast<int>(m_ESPs.size())) {
- log::error("invalid plugin priority: {}", m_ESPs[i].priority);
- continue;
- }
- m_ESPsByName[m_ESPs[i].name] = i;
- m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].priority)) = i;
- }
-
- generatePluginIndexes();
-}
-
-void PluginList::generatePluginIndexes()
-{
- int numESLs = 0;
- int numSkipped = 0;
-
- GamePlugins* gamePlugins = m_GamePlugin->feature<GamePlugins>();
- const bool lightPluginsSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
-
- for (int l = 0; l < m_ESPs.size(); ++l) {
- int i = m_ESPsByPriority.at(l);
- if (!m_ESPs[i].enabled) {
- m_ESPs[i].index = QString();
- ++numSkipped;
- continue;
- }
- if (lightPluginsSupported && (m_ESPs[i].hasLightExtension || m_ESPs[i].isLightFlagged)) {
- int ESLpos = 254 + ((numESLs + 1) / 4096);
- m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
- ++numESLs;
- } else {
- m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
- }
- }
- emit esplist_changed();
-}
-
-int PluginList::rowCount(const QModelIndex &parent) const
-{
- if (!parent.isValid()) {
- return static_cast<int>(m_ESPs.size());
- } else {
- return 0;
- }
-}
-
-int PluginList::columnCount(const QModelIndex &) const
-{
- return COL_LASTCOLUMN + 1;
-}
-
-void PluginList::testMasters()
-{
- std::set<QString, FileNameComparator> enabledMasters;
- for (const auto& iter: m_ESPs) {
- if (iter.enabled) {
- enabledMasters.insert(iter.name);
- }
- }
-
- for (auto& iter: m_ESPs) {
- iter.masterUnset.clear();
- if (iter.enabled) {
- for (const auto& master: iter.masters) {
- if (enabledMasters.find(master) == enabledMasters.end()) {
- iter.masterUnset.insert(master);
- }
- }
- }
- }
-}
-
-QVariant PluginList::data(const QModelIndex &modelIndex, int role) const
-{
- int index = modelIndex.row();
-
- if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
- return displayData(modelIndex);
- } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) {
- return checkstateData(modelIndex);
- } else if (role == Qt::ForegroundRole) {
- return foregroundData(modelIndex);
- } else if (role == Qt::BackgroundRole) {
- return backgroundData(modelIndex);
- } else if (role == Qt::FontRole) {
- return fontData(modelIndex);
- } else if (role == Qt::TextAlignmentRole) {
- return alignmentData(modelIndex);
- } else if (role == Qt::ToolTipRole) {
- return tooltipData(modelIndex);
- } else if (role == Qt::UserRole + 1) {
- return iconData(modelIndex);
- }
- return QVariant();
-}
-
-QVariant PluginList::displayData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- switch (modelIndex.column())
- {
- case COL_NAME:
- return m_ESPs[index].name;
-
- case COL_PRIORITY:
- return m_ESPs[index].priority;
-
- case COL_MODINDEX:
- return m_ESPs[index].index;
-
- default:
- return {};
- }
-}
-
-QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if (m_ESPs[index].forceEnabled) {
- return {};
- }
-
- return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked;
-}
-
-QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) {
- return QBrush(Qt::gray);
- }
-
- return {};
-}
-
-QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if (m_ESPs[index].modSelected) {
- return Settings::instance().colors().pluginListContained();
- }
-
- return {};
-}
-
-QVariant PluginList::fontData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- QFont result;
-
- if (m_ESPs[index].hasMasterExtension ||
- m_ESPs[index].isMasterFlagged ||
- m_ESPs[index].hasLightExtension) {
- result.setItalic(true);
- result.setWeight(QFont::Bold);
- } else if (m_ESPs[index].isLightFlagged) {
- result.setItalic(true);
- }
-
- return result;
-}
-
-QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if (modelIndex.column() == 0) {
- return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
- } else {
- return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
- }
-}
-
-QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
- const auto& esp = m_ESPs[index];
-
- QString toolTip;
-
- toolTip += "<b>" + tr("Origin") + "</b>: " + esp.originName;
-
- if (esp.forceEnabled) {
- toolTip +=
- "<br><b><i>" +
- tr("This plugin can't be disabled (enforced by the game).") +
- "</i></b>";
- } else {
- if (!esp.author.isEmpty()) {
- toolTip +=
- "<br><b>" + tr("Author") + "</b>: " +
- TruncateString(esp.author);
- }
-
- if (esp.description.size() > 0) {
- toolTip +=
- "<br><b>" + tr("Description") + "</b>: " +
- TruncateString(esp.description);
- }
-
- if (esp.masterUnset.size() > 0) {
- toolTip +=
- "<br><b>" + tr("Missing Masters") + "</b>: " +
- "<b>" + TruncateString(QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) + "</b>";
- }
-
- std::set<QString> enabledMasters;
- std::set_difference(esp.masters.begin(), esp.masters.end(),
- esp.masterUnset.begin(), esp.masterUnset.end(),
- std::inserter(enabledMasters, enabledMasters.end()));
-
- if (!enabledMasters.empty()) {
- toolTip +=
- "<br><b>" + tr("Enabled Masters") + "</b>: " +
- TruncateString(SetJoin(enabledMasters, ", "));
- }
-
- if (!esp.archives.empty()) {
- toolTip +=
- "<br><b>" + tr("Loads Archives") + "</b>: " +
- TruncateString(QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) +
- "<br>" + tr(
- "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)");
- }
-
- if (esp.hasIni) {
- toolTip +=
- "<br><b>" + tr("Loads INI settings") + "</b>: "
- "<br>" + tr(
- "There is an ini file connected to this plugin. Its settings will "
- "be added to your game settings, overwriting in case of conflicts.");
- }
-
- if (esp.isLightFlagged && !esp.hasLightExtension) {
- toolTip +=
- "<br><br>" + tr(
- "This ESP is flagged as an ESL. It will adhere to the ESP load "
- "order but the records will be loaded in ESL space.");
- }
- }
-
-
- // additional info
- auto itor = m_AdditionalInfo.find(esp.name);
-
- if (itor != m_AdditionalInfo.end()) {
- if (!itor->second.messages.isEmpty()) {
- toolTip += "<hr><ul style=\"margin-left:15px; -qt-list-indent: 0;\">";
-
- for (auto&& message : itor->second.messages) {
- toolTip += "<li>" + message + "</li>";
- }
-
- toolTip += "</ul>";
- }
-
- // loot
- toolTip += makeLootTooltip(itor->second.loot);
- }
-
- return toolTip;
-}
-
-QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
-{
- QString s;
-
- for (auto&& f : loot.incompatibilities) {
- s +=
- "<li>" + tr("Incompatible with %1")
- .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
- "</li>";
- }
-
- for (auto&& m : loot.missingMasters) {
- s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
- }
-
- for (auto&& m : loot.messages) {
- s += "<li>";
-
- switch (m.type)
- {
- case log::Warning:
- s += tr("Warning") + ": ";
- break;
-
- case log::Error:
- s += tr("Error") + ": ";
- break;
-
- case log::Info: // fall-through
- case log::Debug:
- default:
- // nothing
- break;
- }
-
- s += m.text + "</li>";
- }
-
- for (auto&& d : loot.dirty) {
- s += "<li>" + d.toString(false) + "</li>";
- }
-
- for (auto&& c : loot.clean) {
- s += "<li>" + c.toString(true) + "</li>";
- }
-
- if (!s.isEmpty()) {
- s =
- "<hr>"
- "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; -qt-list-indent: 0;\">" +
- s +
- "</ul>";
- }
-
- return s;
-}
-
-QVariant PluginList::iconData(const QModelIndex &modelIndex) const
-{
- int index = modelIndex.row();
-
- QVariantList result;
-
- const auto& esp = m_ESPs[index];
-
- auto infoItor = m_AdditionalInfo.find(esp.name);
-
- const AdditionalInfo* info = nullptr;
- if (infoItor != m_AdditionalInfo.end()) {
- info = &infoItor->second;
- }
-
- if (isProblematic(esp, info)) {
- result.append(":/MO/gui/warning");
- }
-
- if (m_LockedOrder.find(esp.name) != m_LockedOrder.end()) {
- result.append(":/MO/gui/locked");
- }
-
- if (hasInfo(esp, info)) {
- result.append(":/MO/gui/information");
- }
-
- if (esp.hasIni) {
- result.append(":/MO/gui/attachment");
- }
-
- if (!esp.archives.empty()) {
- result.append(":/MO/gui/archive_conflict_neutral");
- }
-
- if (esp.isLightFlagged && !esp.hasLightExtension) {
- result.append(":/MO/gui/awaiting");
- }
-
- if (info && !info->loot.dirty.empty()) {
- result.append(":/MO/gui/edit_clear");
- }
-
- return result;
-}
-
-bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const
-{
- if (esp.masterUnset.size() > 0) {
- return true;
- }
-
- if (info) {
- if (!info->loot.incompatibilities.empty()) {
- return true;
- }
-
- if (!info->loot.missingMasters.empty()) {
- return true;
- }
- }
-
- return false;
-}
-
-bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const
-{
- if (info) {
- if (!info->messages.empty()) {
- return true;
- }
-
- if (!info->loot.messages.empty()) {
- return true;
- }
- }
-
- return false;
-}
-
-bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role)
-{
- QString modName = modIndex.data().toString();
- IPluginList::PluginStates oldState = state(modName);
-
- bool result = false;
-
- if (role == Qt::CheckStateRole) {
- m_ESPs[modIndex.row()].enabled =
- value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled;
- m_LastCheck.restart();
- emit dataChanged(modIndex, modIndex);
-
- refreshLoadOrder();
- emit writePluginsList();
-
- result = true;
- } else if (role == Qt::EditRole) {
- if (modIndex.column() == COL_PRIORITY) {
- bool ok = false;
- int newPriority = value.toInt(&ok);
- if (ok) {
- setPluginPriority(modIndex.row(), newPriority);
- result = true;
- }
- refreshLoadOrder();
- emit writePluginsList();
- }
- }
-
- IPluginList::PluginStates newState = state(modName);
- if (oldState != newState) {
- try {
- pluginStatesChanged({ modName }, newState);
- testMasters();
- emit dataChanged(
- this->index(0, 0),
- this->index(static_cast<int>(m_ESPs.size()), columnCount()));
- } catch (const std::exception &e) {
- log::error("failed to invoke state changed notification: {}", e.what());
- } catch (...) {
- log::error("failed to invoke state changed notification: unknown exception");
- }
- }
-
- return result;
-}
-
-QVariant PluginList::headerData(int section, Qt::Orientation orientation,
- int role) const
-{
- if (orientation == Qt::Horizontal) {
- if (role == Qt::DisplayRole) {
- return getColumnName(section);
- } else if (role == Qt::ToolTipRole) {
- return getColumnToolTip(section);
- }
- }
- return QAbstractItemModel::headerData(section, orientation, role);
-}
-
-Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const
-{
- int index = modelIndex.row();
- Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
-
- if (modelIndex.isValid()) {
- if (!m_ESPs[index].forceEnabled) {
- result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
- }
- if (modelIndex.column() == COL_PRIORITY) {
- result |= Qt::ItemIsEditable;
- }
- result &= ~Qt::ItemIsDropEnabled;
- } else {
- result |= Qt::ItemIsDropEnabled;
- }
-
- return result;
-}
-
-void PluginList::setPluginPriority(int row, int &newPriority, bool isForced)
-{
- int newPriorityTemp = newPriority;
-
- // enforce valid range
- if (newPriorityTemp < 0)
- newPriorityTemp = 0;
- else if (newPriorityTemp >= static_cast<int>(m_ESPsByPriority.size()))
- newPriorityTemp = static_cast<int>(m_ESPsByPriority.size()) - 1;
-
- if (!m_ESPs[row].isMasterFlagged &&
- !m_ESPs[row].hasLightExtension &&
- !m_ESPs[row].hasMasterExtension) {
- // don't allow esps to be moved above esms
- while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged ||
- m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension ||
- m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension)) {
- ++newPriorityTemp;
- }
- } else {
- // don't allow esms to be moved below esps
- while ((newPriorityTemp > 0) &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension) {
- --newPriorityTemp;
- }
- // also don't allow "regular" esms to be moved above primary plugins
- while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) {
- ++newPriorityTemp;
- }
- }
-
- int oldPriority = m_ESPs.at(row).priority;
- if (newPriorityTemp < oldPriority) { // moving up
- // don't allow plugins to be moved above their masters
- for (auto master : m_ESPs[row].masters) {
- auto iter = m_ESPsByName.find(master);
- if (iter != m_ESPsByName.end()) {
- int masterPriority = m_ESPs[iter->second].priority;
- if (masterPriority >= newPriorityTemp)
- {
- newPriorityTemp = masterPriority + 1;
- }
- }
- }
- }
- else if (newPriorityTemp > oldPriority) { // moving down
- // don't allow masters to be moved below their children
- for (int i = oldPriority + 1; i <= newPriorityTemp; i++) {
- PluginList::ESPInfo* otherInfo = &m_ESPs.at(m_ESPsByPriority[i]);
- for (auto master : otherInfo->masters) {
- if (master.compare(m_ESPs[row].name, Qt::CaseInsensitive) == 0) {
- newPriorityTemp = otherInfo->priority - 1;
- break;
- }
- }
- }
- }
-
- try {
- if (newPriorityTemp != oldPriority) {
- if (newPriorityTemp > oldPriority) {
- // priority is higher than the old, so the gap we left is in lower priorities
- for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) {
- --m_ESPs.at(m_ESPsByPriority.at(i)).priority;
- }
- emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount()));
- }
- else {
- for (int i = newPriorityTemp; i < oldPriority; ++i) {
- ++m_ESPs.at(m_ESPsByPriority.at(i)).priority;
- }
- emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount()));
- ++newPriority;
- }
-
- m_ESPs.at(row).priority = newPriorityTemp;
- emit dataChanged(index(row, 0), index(row, columnCount()));
- m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp);
- }
- } catch (const std::out_of_range&) {
- reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name));
- }
-
- updateIndices();
-}
-
-void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
-{
- ChangeBracket<PluginList> layoutChange(this);
- const std::vector<ESPInfo> &esp = m_ESPs;
-
- int minPriority = INT_MAX;
- int maxPriority = INT_MIN;
-
- // don't try to move plugins before force-enabled plugins
- for (std::vector<ESPInfo>::const_iterator iter = m_ESPs.begin();
- iter != m_ESPs.end(); ++iter) {
- if (iter->forceEnabled) {
- newPriority = std::max(newPriority, iter->priority + 1);
- }
- maxPriority = std::max(maxPriority, iter->priority + 1);
- minPriority = std::min(minPriority, iter->priority);
- }
-
- // limit the new priority to existing priorities
- newPriority = std::min(newPriority, maxPriority);
- newPriority = std::max(newPriority, minPriority);
-
- // sort the moving plugins by ascending priorities
- std::sort(rows.begin(), rows.end(),
- [&esp](const int &LHS, const int &RHS) {
- return esp[LHS].priority < esp[RHS].priority;
- });
-
- // if at least on plugin is increasing in priority, the target index is
- // that of the row BELOW the dropped location, otherwise it's the one above
- for (std::vector<int>::const_iterator iter = rows.begin();
- iter != rows.end(); ++iter) {
- if (m_ESPs[*iter].priority < newPriority) {
- --newPriority;
- break;
- }
- }
-
- for (std::vector<int>::const_iterator iter = rows.begin(); iter != rows.end(); ++iter) {
- setPluginPriority(*iter, newPriority);
- }
-
- layoutChange.finish();
- refreshLoadOrder();
- emit writePluginsList();
-}
-
-bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent)
-{
- if (action == Qt::IgnoreAction) {
- return true;
- }
-
- QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist");
- QDataStream stream(&encoded, QIODevice::ReadOnly);
-
- std::vector<int> sourceRows;
-
- while (!stream.atEnd()) {
- int sourceRow, col;
- QMap<int, QVariant> roleDataMap;
- stream >> sourceRow >> col >> roleDataMap;
- if (col == 0) { // only add each row once
- sourceRows.push_back(sourceRow);
- }
- }
-
- if (row == -1) {
- row = parent.row();
- }
-
- int newPriority;
-
- if ((row < 0) ||
- (row >= static_cast<int>(m_ESPs.size()))) {
- newPriority = static_cast<int>(m_ESPs.size());
- } else {
- newPriority = m_ESPs[row].priority;
- }
- changePluginPriority(sourceRows, newPriority);
-
- return false;
-}
-
-QModelIndex PluginList::index(int row, int column, const QModelIndex&) const
-{
- if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) {
- return QModelIndex();
- }
- return createIndex(row, column, row);
-}
-
-QModelIndex PluginList::parent(const QModelIndex&) const
-{
- return QModelIndex();
-}
-
-PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
- const QString &originName, const QString &fullPath,
- bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
- : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled),
- priority(0), loadOrder(-1), originName(originName), hasIni(hasIni),
- archives(archives.begin(), archives.end()), modSelected(false)
-{
- try {
- ESP::File file(ToWString(fullPath));
- auto extension = name.right(3).toLower();
- hasMasterExtension = (extension == "esm");
- hasLightExtension = lightPluginsAreSupported && (extension == "esl");
- isMasterFlagged = file.isMaster();
- isLightFlagged = lightPluginsAreSupported && file.isLight();
-
- author = QString::fromLatin1(file.author().c_str());
- description = QString::fromLatin1(file.description().c_str());
-
- for (auto&& m : file.masters()) {
- masters.insert(QString::fromStdString(m));
- }
- } catch (const std::exception &e) {
- log::error("failed to parse plugin file {}: {}", fullPath, e.what());
- hasMasterExtension = false;
- hasLightExtension = false;
- isMasterFlagged = false;
- isLightFlagged = false;
- }
-}
-
-void PluginList::managedGameChanged(const IPluginGame *gamePlugin)
-{
- m_GamePlugin = gamePlugin;
-}
+/*
+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 "pluginlist.h"
+#include "settings.h"
+#include "scopeguard.h"
+#include "modinfo.h"
+#include "modlist.h"
+#include "viewmarkingscrollbar.h"
+#include "shared/directoryentry.h"
+#include "shared/filesorigin.h"
+#include "shared/fileentry.h"
+
+#include <utility.h>
+#include <iplugingame.h>
+#include <espfile.h>
+#include <report.h>
+#include "shared/windows_error.h"
+#include <safewritefile.h>
+#include <gameplugins.h>
+
+#include <QtDebug>
+#include <QMessageBox>
+#include <QMimeData>
+#include <QCoreApplication>
+#include <QDateTime>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QFileInfo>
+#include <QListWidgetItem>
+#include <QRegularExpression>
+#include <QString>
+#include <QApplication>
+#include <QKeyEvent>
+#include <QSortFilterProxyModel>
+
+#include <ctime>
+#include <algorithm>
+#include <stdexcept>
+
+#include "organizercore.h"
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+static QString TruncateString(const QString& text)
+{
+ QString new_text = text;
+
+ if (new_text.length() > 1024) {
+ new_text.truncate(1024);
+ new_text += "...";
+ }
+
+ return new_text;
+}
+
+
+PluginList::PluginList(OrganizerCore& organizer)
+ : QAbstractItemModel(&organizer)
+ , m_Organizer(organizer)
+ , m_FontMetrics(QFont())
+{
+ connect(this, SIGNAL(writePluginsList()), this, SLOT(generatePluginIndexes()));
+ m_LastCheck.start();
+}
+
+PluginList::~PluginList()
+{
+ m_Refreshed.disconnect_all_slots();
+ m_PluginMoved.disconnect_all_slots();
+ m_PluginStateChanged.disconnect_all_slots();
+}
+
+QString PluginList::getColumnName(int column)
+{
+ switch (column) {
+ case COL_NAME: return tr("Name");
+ case COL_PRIORITY: return tr("Priority");
+ case COL_MODINDEX: return tr("Mod Index");
+ case COL_FLAGS: return tr("Flags");
+ default: return tr("unknown");
+ }
+}
+
+
+QString PluginList::getColumnToolTip(int column)
+{
+ switch (column) {
+ case COL_NAME: return tr("Name of the plugin");
+ case COL_FLAGS: return tr("Emblems to highlight things that might require attention.");
+ case COL_PRIORITY: return tr("Load priority of plugins. The higher, the more \"important\" it is and thus "
+ "overwrites data from plugins with lower priority.");
+ case COL_MODINDEX: return tr("Determines the formids of objects originating from this mods.");
+ default: return tr("unknown");
+ }
+}
+
+void PluginList::highlightPlugins(
+ const std::vector<unsigned int>& modIndices,
+ const MOShared::DirectoryEntry &directoryEntry)
+{
+ auto* profile = m_Organizer.currentProfile();
+
+ for (auto &esp : m_ESPs) {
+ esp.modSelected = false;
+ }
+
+ for (auto& modIndex : modIndices) {
+ ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex);
+ if (!selectedMod.isNull() && profile->modEnabled(modIndex)) {
+ QDir dir(selectedMod->absolutePath());
+ QStringList plugins = dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl");
+ const MOShared::FilesOrigin& origin = directoryEntry.getOriginByName(selectedMod->internalName().toStdWString());
+ if (plugins.size() > 0) {
+ for (auto plugin : plugins) {
+ MOShared::FileEntryPtr file = directoryEntry.findFile(plugin.toStdWString());
+ if (file && file->getOrigin() != origin.getID()) {
+ const auto alternatives = file->getAlternatives();
+ if (std::find_if(alternatives.begin(), alternatives.end(), [&](const FileAlternative& element) { return element.originID() == origin.getID(); }) == alternatives.end())
+ continue;
+ }
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
+ if (iter != m_ESPsByName.end()) {
+ m_ESPs[iter->second].modSelected = true;
+ }
+ }
+ }
+ }
+ }
+
+ emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
+}
+
+void PluginList::refresh(const QString &profileName
+ , const DirectoryEntry &baseDirectory
+ , const QString &lockedOrderFile
+ , bool force)
+{
+ TimeThis tt("PluginList::refresh()");
+
+ if (force) {
+ m_ESPs.clear();
+ m_ESPsByName.clear();
+ m_ESPsByPriority.clear();
+ }
+
+ ChangeBracket<PluginList> layoutChange(this);
+
+ QStringList primaryPlugins = m_GamePlugin->primaryPlugins();
+ GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
+ const bool lightPluginsAreSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
+
+ m_CurrentProfile = profileName;
+
+ QStringList availablePlugins;
+
+ std::vector<FileEntryPtr> files = baseDirectory.getFiles();
+ for (FileEntryPtr current : files) {
+ if (current.get() == nullptr) {
+ continue;
+ }
+ QString filename = ToQString(current->getName());
+
+ if (filename.endsWith(".esp", Qt::CaseInsensitive) ||
+ filename.endsWith(".esm", Qt::CaseInsensitive) ||
+ filename.endsWith(".esl", Qt::CaseInsensitive)) {
+
+ availablePlugins.append(filename);
+
+ if (m_ESPsByName.find(filename) != m_ESPsByName.end()) {
+ continue;
+ }
+
+ bool forceEnabled = Settings::instance().game().forceEnableCoreFiles() &&
+ primaryPlugins.contains(filename, Qt::CaseInsensitive);
+
+ bool archive = false;
+ try {
+ FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive));
+
+ //name without extension
+ QString baseName = QFileInfo(filename).completeBaseName();
+
+ QString iniPath = baseName + ".ini";
+ bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr;
+ std::set<QString> loadedArchives;
+ QString candidateName;
+ for (FileEntryPtr archiveCandidate : files) {
+ candidateName = ToQString(archiveCandidate->getName());
+ if (candidateName.startsWith(baseName, Qt::CaseInsensitive) &&
+ (candidateName.endsWith(".bsa", Qt::CaseInsensitive) ||
+ candidateName.endsWith(".ba2", Qt::CaseInsensitive))) {
+ loadedArchives.insert(candidateName);
+ }
+ }
+
+ QString originName = ToQString(origin.getName());
+ unsigned int modIndex = ModInfo::getIndex(originName);
+ if (modIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ originName = modInfo->name();
+ }
+
+ m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported));
+ m_ESPs.rbegin()->priority = -1;
+ } catch (const std::exception &e) {
+ reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what()));
+ }
+ }
+ }
+
+ for (const auto &espName : m_ESPsByName) {
+ if (!availablePlugins.contains(espName.first, Qt::CaseInsensitive)) {
+ m_ESPs[espName.second].name = "";
+ }
+ }
+
+ m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(),
+ [](const ESPInfo &info) -> bool {
+ return info.name.isEmpty();
+ }),
+ m_ESPs.end());
+
+ fixPriorities();
+
+ // functions in GamePlugins will use the IPluginList interface of this, so
+ // indices need to work. priority will be off however
+ updateIndices();
+
+ if (gamePlugins) {
+ gamePlugins->readPluginLists(m_Organizer.managedGameOrganizer()->pluginList());
+ }
+
+ fixPrimaryPlugins();
+ fixPluginRelationships();
+
+ testMasters();
+
+ updateIndices();
+
+ readLockedOrderFrom(lockedOrderFile);
+
+ layoutChange.finish();
+
+ refreshLoadOrder();
+ emit dataChanged(this->index(0, 0),
+ this->index(static_cast<int>(m_ESPs.size()), columnCount()));
+
+ m_Refreshed();
+}
+
+void PluginList::fixPrimaryPlugins()
+{
+ if (!m_Organizer.settings().game().forceEnableCoreFiles()) {
+ return;
+ }
+
+ // This function ensures that the primary plugins are first and in the correct order
+ QStringList primaryPlugins = m_Organizer.managedGame()->primaryPlugins();
+ int prio = 0;
+ bool somethingChanged = false;
+ for (QString plugin : primaryPlugins) {
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
+ // Plugin is present?
+ if (iter != m_ESPsByName.end()) {
+ if (prio != m_ESPs[iter->second].priority) {
+ // Priority is wrong! Fix it!
+ int newPrio = prio;
+ setPluginPriority(iter->second, newPrio, true /* isForced */);
+ somethingChanged = true;
+ }
+ prio++;
+ }
+ }
+
+ if (somethingChanged) {
+ writePluginsList();
+ }
+}
+
+void PluginList::fixPluginRelationships()
+{
+ TimeThis timer("PluginList::fixPluginRelationships");
+
+ // Count the types of plugins
+ int masterCount = 0;
+ for (auto plugin : m_ESPs) {
+ if (plugin.hasLightExtension ||
+ plugin.hasMasterExtension ||
+ plugin.isMasterFlagged) {
+ masterCount++;
+ }
+ }
+
+ // Ensure masters are up top and normal plugins are down below
+ for (int i = 0; i < m_ESPs.size(); i++) {
+ ESPInfo& plugin = m_ESPs[i];
+ if (plugin.hasLightExtension ||
+ plugin.hasMasterExtension ||
+ plugin.isMasterFlagged) {
+ if (plugin.priority > masterCount) {
+ int newPriority = masterCount + 1;
+ setPluginPriority(i, newPriority);
+ }
+ }
+ else {
+ if (plugin.priority < masterCount) {
+ int newPriority = masterCount + 1;
+ setPluginPriority(i, newPriority);
+ }
+ }
+ }
+
+ // Ensure master/child relationships are observed
+ for (int i = 0; i < m_ESPs.size(); i++) {
+ ESPInfo& plugin = m_ESPs[i];
+ int newPriority = plugin.priority;
+ for (auto master : plugin.masters) {
+ auto iter = m_ESPsByName.find(master);
+ if (iter != m_ESPsByName.end()) {
+ newPriority = std::max(newPriority, m_ESPs[iter->second].priority);
+ }
+ }
+ if (newPriority != plugin.priority) {
+ setPluginPriority(i, newPriority);
+ }
+ }
+}
+
+void PluginList::fixPriorities()
+{
+ std::vector<std::pair<int, int>> espPrios;
+
+ for (int i = 0; i < m_ESPs.size(); ++i) {
+ int prio = m_ESPs[i].priority;
+ if (prio == -1) {
+ prio = INT_MAX;
+ }
+ espPrios.push_back(std::make_pair(prio, i));
+ }
+
+ std::sort(espPrios.begin(), espPrios.end(),
+ [](const std::pair<int, int> &lhs, const std::pair<int, int> &rhs) {
+ return lhs.first < rhs.first;
+ });
+
+ for (int i = 0; i < espPrios.size(); ++i) {
+ m_ESPs[espPrios[i].second].priority = i;
+ }
+}
+
+void PluginList::enableESP(const QString &name, bool enable)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ auto enabled = m_ESPs[iter->second].enabled;
+ m_ESPs[iter->second].enabled =
+ enable || m_ESPs[iter->second].forceEnabled;
+
+ emit writePluginsList();
+ if (enabled != m_ESPs[iter->second].enabled) {
+ pluginStatesChanged({ name }, state(name));
+ }
+ } else {
+ reportError(tr("Plugin not found: %1").arg(qUtf8Printable(name)));
+ }
+}
+
+int PluginList::findPluginByPriority(int priority)
+{
+ for (int i = 0; i < m_ESPs.size(); i++ ) {
+ if (m_ESPs[i].priority == priority) {
+ return i;
+ }
+ }
+ log::error("No plugin with priority {}", priority);
+ return -1;
+}
+
+void PluginList::setEnabled(const QModelIndexList& indices, bool enabled)
+{
+ QStringList dirty;
+ for (auto& idx : indices) {
+ if (m_ESPs[idx.row()].enabled != enabled) {
+ m_ESPs[idx.row()].enabled = enabled;
+ dirty.append(m_ESPs[idx.row()].name);
+ }
+ }
+ if (!dirty.isEmpty()) {
+ emit writePluginsList();
+ pluginStatesChanged(dirty,
+ enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
+ }
+}
+
+void PluginList::setEnabledAll(bool enabled)
+{
+ QStringList dirty;
+ for (ESPInfo &info : m_ESPs) {
+ if (info.enabled != enabled) {
+ info.enabled = enabled;
+ dirty.append(info.name);
+ }
+ }
+ if (!dirty.isEmpty()) {
+ emit writePluginsList();
+ pluginStatesChanged(dirty,
+ enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
+ }
+}
+
+void PluginList::sendToPriority(const QModelIndexList& indices, int newPriority)
+{
+ std::vector<int> pluginsToMove;
+ for (auto& idx : indices) {
+ if (!m_ESPs[idx.row()].forceEnabled) {
+ pluginsToMove.push_back(idx.row());
+ }
+ }
+ if (pluginsToMove.size()) {
+ changePluginPriority(pluginsToMove, newPriority);
+ }
+}
+
+void PluginList::shiftPluginsPriority(const QModelIndexList& indices, int offset)
+{
+ // retrieve the plugin index and sort them by priority to avoid issue
+ // when moving them
+ std::vector<int> allIndex;
+ for (auto& idx : indices) {
+ allIndex.push_back(idx.row());
+ }
+ std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) {
+ bool cmp = m_ESPs[lhs].priority < m_ESPs[rhs].priority;
+ return offset > 0 ? !cmp : cmp;
+ });
+
+ for (auto index : allIndex) {
+ int newPriority = m_ESPs[index].priority + offset;
+ if (newPriority >= 0 && newPriority < rowCount()) {
+ setPluginPriority(index, newPriority);
+ }
+ }
+
+ refreshLoadOrder();
+}
+
+void PluginList::toggleState(const QModelIndexList& indices)
+{
+ QModelIndex minRow, maxRow;
+ for (auto& idx : indices) {
+ if (!minRow.isValid() || (idx.row() < minRow.row())) {
+ minRow = idx;
+ }
+ if (!maxRow.isValid() || (idx.row() > maxRow.row())) {
+ maxRow = idx;
+ }
+ int oldState = idx.data(Qt::CheckStateRole).toInt();
+ setData(idx, oldState == Qt::Unchecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+ }
+
+ emit dataChanged(minRow, maxRow);
+}
+
+bool PluginList::isEnabled(const QString &name)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ return m_ESPs[iter->second].enabled;
+ } else {
+ return false;
+ }
+}
+
+void PluginList::clearInformation(const QString &name)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name].messages.clear();
+ }
+}
+
+void PluginList::clearAdditionalInformation()
+{
+ m_AdditionalInfo.clear();
+}
+
+void PluginList::addInformation(const QString &name, const QString &message)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name].messages.append(message);
+ } else {
+ log::warn("failed to associate message for \"{}\"", name);
+ }
+}
+
+void PluginList::addLootReport(const QString& name, Loot::Plugin plugin)
+{
+ auto iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name].loot = std::move(plugin);
+ } else {
+ log::warn("failed to associate loot report for \"{}\"", name);
+ }
+}
+
+bool PluginList::isEnabled(int index)
+{
+ return m_ESPs.at(index).enabled;
+}
+
+void PluginList::readLockedOrderFrom(const QString &fileName)
+{
+ m_LockedOrder.clear();
+
+ QFile file(fileName);
+ if (!file.exists()) {
+ // no locked load order, that's ok
+ return;
+ }
+
+ file.open(QIODevice::ReadOnly);
+ int lineNumber = 0;
+ while (!file.atEnd())
+ {
+ QByteArray line = file.readLine();
+ ++lineNumber;
+
+ // Skip empty lines or commented out lines (#)
+ if ((line.size() <= 0) || (line.at(0) == '#'))
+ {
+ continue;
+ }
+
+ QList<QByteArray> fields = line.split('|');
+ if (fields.count() != 2)
+ {
+ // Don't know how to parse this so run away
+ log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
+ continue;
+ }
+
+ // Read the plugin name and priority
+ QString pluginName = QString::fromUtf8(fields.at(0));
+ int priority = fields.at(1).trimmed().toInt();
+ if (priority < 0)
+ {
+ // WTF do you mean a negative priority?
+ log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
+ continue;
+ }
+
+ // Determine the index of the plugin
+ auto it = m_ESPsByName.find(pluginName);
+ if (it == m_ESPsByName.end())
+ {
+ // Plugin does not exist in the current set of plugins
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+ int pluginIndex = it->second;
+
+ // Do not allow locking forced plugins
+ if (m_ESPs[pluginIndex].forceEnabled)
+ {
+ continue;
+ }
+
+ // If the priority is larger than the number of plugins, just keep it locked
+ if (priority >= m_ESPsByPriority.size())
+ {
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+
+ // These are some helper functions for figuring out what is already locked
+ auto findLocked = [&](const std::pair<QString, int>& a) { return a.second == priority; };
+ auto alreadyLocked = [&](){ return std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), findLocked) != m_LockedOrder.end(); };
+
+ // See if we can just set the given priority
+ if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
+ {
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+
+ // Find the next higher priority we can set the plugin to
+ while (++priority < m_ESPs.size())
+ {
+ if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
+ {
+ m_LockedOrder[pluginName] = priority;
+ break;
+ }
+ }
+
+ // See if we walked off the end of the plugin list
+ if (priority >= m_ESPs.size())
+ {
+ // I guess go ahead and lock it here at the end of the list?
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+ } /* while (!file.atEnd()) */
+ file.close();
+}
+
+void PluginList::writeLockedOrder(const QString &fileName) const
+{
+ SafeWriteFile file(fileName);
+
+ file->resize(0);
+ file->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8());
+ for (auto iter = m_LockedOrder.begin(); iter != m_LockedOrder.end(); ++iter) {
+ file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8());
+ }
+ file.commit();
+}
+
+void PluginList::saveTo(const QString &lockedOrderFileName) const
+{
+ GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
+ if (gamePlugins) {
+ gamePlugins->writePluginLists(m_Organizer.managedGameOrganizer()->pluginList());
+ }
+
+ writeLockedOrder(lockedOrderFileName);
+}
+
+
+bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure)
+{
+ if (m_GamePlugin->loadOrderMechanism() != IPluginGame::LoadOrderMechanism::FileTime) {
+ // nothing to do
+ return true;
+ }
+
+ log::debug("setting file times on esps");
+
+ for (ESPInfo &esp : m_ESPs) {
+ std::wstring espName = ToWString(esp.name);
+ const FileEntryPtr fileEntry = directoryStructure.findFile(espName);
+ if (fileEntry.get() != nullptr) {
+ QString fileName;
+ bool archive = false;
+ int originid = fileEntry->getOrigin(archive);
+
+ fileName = QString("%1\\%2")
+ .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath())))
+ .arg(esp.name);
+
+ HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE,
+ 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (file == INVALID_HANDLE_VALUE) {
+ if (::GetLastError() == ERROR_SHARING_VIOLATION) {
+ // file is locked, probably the game is running
+ return false;
+ } else {
+ throw windows_error(QObject::tr("failed to access %1").arg(fileName).toUtf8().constData());
+ }
+ }
+
+ ULONGLONG temp = 0;
+ temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
+
+ FILETIME newWriteTime;
+
+ newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
+ newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
+ esp.time = newWriteTime;
+ fileEntry->setFileTime(newWriteTime);
+ if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
+ throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData());
+ }
+
+ CloseHandle(file);
+ }
+ }
+ return true;
+}
+
+int PluginList::enabledCount() const
+{
+ int enabled = 0;
+ for (const auto &info : m_ESPs) {
+ if (info.enabled) {
+ ++enabled;
+ }
+ }
+ return enabled;
+}
+
+QString PluginList::getIndexPriority(int index) const
+{
+ return m_ESPs[index].index;
+}
+
+bool PluginList::isESPLocked(int index) const
+{
+ return m_LockedOrder.find(m_ESPs.at(index).name) != m_LockedOrder.end();
+}
+
+void PluginList::lockESPIndex(int index, bool lock)
+{
+ if (lock) {
+ if (!m_ESPs.at(index).forceEnabled)
+ m_LockedOrder[getName(index)] = m_ESPs.at(index).loadOrder;
+ else
+ return;
+ } else {
+ auto iter = m_LockedOrder.find(getName(index));
+ if (iter != m_LockedOrder.end()) {
+ m_LockedOrder.erase(iter);
+ }
+ }
+ emit writePluginsList();
+}
+
+void PluginList::syncLoadOrder()
+{
+ int loadOrder = 0;
+ for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
+ int index = m_ESPsByPriority[i];
+
+ if (m_ESPs[index].enabled) {
+ m_ESPs[index].loadOrder = loadOrder++;
+ } else {
+ m_ESPs[index].loadOrder = -1;
+ }
+ }
+}
+
+void PluginList::refreshLoadOrder()
+{
+ ChangeBracket<PluginList> layoutChange(this);
+ syncLoadOrder();
+ // set priorities according to locked load order
+ std::map<int, QString> lockedLoadOrder;
+ std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(),
+ [&lockedLoadOrder] (const std::pair<QString, int> &ele) {
+ lockedLoadOrder[ele.second] = ele.first; });
+
+ int targetPrio = 0;
+ bool savePluginsList = false;
+ // this is guaranteed to iterate from lowest key (load order) to highest
+ for (auto iter = lockedLoadOrder.begin(); iter != lockedLoadOrder.end(); ++iter) {
+ auto nameIter = m_ESPsByName.find(iter->second);
+ if (nameIter != m_ESPsByName.end()) {
+ // locked esp exists
+
+ // find the location to insert at
+ while ((targetPrio < static_cast<int>(m_ESPs.size() - 1)) &&
+ (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
+ ++targetPrio;
+ }
+
+ if (static_cast<size_t>(targetPrio) >= m_ESPs.size()) {
+ continue;
+ }
+
+ int temp = targetPrio;
+ int index = nameIter->second;
+ if (m_ESPs[index].priority != temp) {
+ setPluginPriority(index, temp);
+ m_ESPs[index].loadOrder = iter->first;
+ syncLoadOrder();
+ savePluginsList = true;
+ }
+ }
+ }
+ if (savePluginsList) {
+ emit writePluginsList();
+ }
+}
+
+void PluginList::disconnectSlots() {
+ m_PluginMoved.disconnect_all_slots();
+ m_Refreshed.disconnect_all_slots();
+ m_PluginStateChanged.disconnect_all_slots();
+}
+
+int PluginList::timeElapsedSinceLastChecked() const
+{
+ return m_LastCheck.elapsed();
+}
+
+QStringList PluginList::pluginNames() const
+{
+ QStringList result;
+
+ for (const ESPInfo &info : m_ESPs) {
+ result.append(info.name);
+ }
+
+ return result;
+}
+
+IPluginList::PluginStates PluginList::state(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return IPluginList::STATE_MISSING;
+ } else {
+ return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
+ }
+}
+
+void PluginList::setState(const QString &name, PluginStates state) {
+ auto iter = m_ESPsByName.find(name);
+ if (iter != m_ESPsByName.end()) {
+ m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) ||
+ m_ESPs[iter->second].forceEnabled;
+ } else {
+ log::warn("Plugin not found: {}", name);
+ }
+}
+
+void PluginList::setLoadOrder(const QStringList &pluginList)
+{
+ for (ESPInfo &info : m_ESPs) {
+ info.priority = -1;
+ }
+ int maxPriority = 0;
+ for (const QString &plugin : pluginList) {
+ auto iter = m_ESPsByName.find(plugin);
+ if (iter !=m_ESPsByName.end()) {
+ m_ESPs[iter->second].priority = maxPriority++;
+ }
+ }
+
+ // use old priorities
+ for (ESPInfo &info : m_ESPs) {
+ if (info.priority == -1) {
+ info.priority = maxPriority++;
+ }
+ }
+ updateIndices();
+}
+
+int PluginList::priority(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return -1;
+ } else {
+ return m_ESPs[iter->second].priority;
+ }
+}
+
+bool PluginList::setPriority(const QString& name, int newPriority) {
+
+ if (newPriority < 0 || newPriority >= static_cast<int>(m_ESPsByPriority.size())) {
+ return false;
+ }
+
+ auto oldPriority = priority(name);
+ if (oldPriority == -1) {
+ return false;
+ }
+
+ int rowIndex = findPluginByPriority(oldPriority);
+
+ // We need to increment newPriority if its above the old one, otherwise the
+ // plugin is place right below the new priority.
+ if (oldPriority < newPriority) {
+ newPriority += 1;
+ }
+ changePluginPriority({ rowIndex }, newPriority);
+
+ return true;
+}
+
+int PluginList::loadOrder(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return -1;
+ } else {
+ return m_ESPs[iter->second].loadOrder;
+ }
+}
+
+QStringList PluginList::masters(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return QStringList();
+ } else {
+ QStringList result;
+ for (const QString &master : m_ESPs[iter->second].masters) {
+ result.append(master);
+ }
+ return result;
+ }
+}
+
+QString PluginList::origin(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return QString();
+ } else {
+ return m_ESPs[iter->second].originName;
+ }
+}
+
+bool PluginList::hasMasterExtension(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].hasMasterExtension;
+ }
+}
+
+bool PluginList::hasLightExtension(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].hasLightExtension;
+ }
+}
+
+bool PluginList::isMasterFlagged(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].isMasterFlagged;
+ }
+}
+
+bool PluginList::isLightFlagged(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].isLightFlagged;
+ }
+}
+
+boost::signals2::connection PluginList::onPluginStateChanged(const std::function<void(const std::map<QString, PluginStates>&)>& func)
+{
+ return m_PluginStateChanged.connect(func);
+}
+
+void PluginList::pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const {
+ if (pluginNames.isEmpty()) {
+ return;
+ }
+ std::map<QString, IPluginList::PluginStates> infos;
+ for (auto& name : pluginNames) {
+ infos[name] = state;
+ }
+ m_PluginStateChanged(infos);
+}
+
+boost::signals2::connection PluginList::onRefreshed(const std::function<void ()> &callback)
+{
+ return m_Refreshed.connect(callback);
+}
+
+boost::signals2::connection PluginList::onPluginMoved(const std::function<void (const QString &, int, int)> &func)
+{
+ return m_PluginMoved.connect(func);
+}
+
+void PluginList::updateIndices()
+{
+ m_ESPsByName.clear();
+ m_ESPsByPriority.clear();
+ m_ESPsByPriority.resize(m_ESPs.size());
+ for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
+ if (m_ESPs[i].priority < 0) {
+ continue;
+ }
+ if (m_ESPs[i].priority >= static_cast<int>(m_ESPs.size())) {
+ log::error("invalid plugin priority: {}", m_ESPs[i].priority);
+ continue;
+ }
+ m_ESPsByName[m_ESPs[i].name] = i;
+ m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].priority)) = i;
+ }
+
+ generatePluginIndexes();
+}
+
+void PluginList::generatePluginIndexes()
+{
+ int numESLs = 0;
+ int numSkipped = 0;
+
+ GamePlugins* gamePlugins = m_GamePlugin->feature<GamePlugins>();
+ const bool lightPluginsSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
+
+ for (int l = 0; l < m_ESPs.size(); ++l) {
+ int i = m_ESPsByPriority.at(l);
+ if (!m_ESPs[i].enabled) {
+ m_ESPs[i].index = QString();
+ ++numSkipped;
+ continue;
+ }
+ if (lightPluginsSupported && (m_ESPs[i].hasLightExtension || m_ESPs[i].isLightFlagged)) {
+ int ESLpos = 254 + ((numESLs + 1) / 4096);
+ m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
+ ++numESLs;
+ } else {
+ m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
+ }
+ }
+ emit esplist_changed();
+}
+
+int PluginList::rowCount(const QModelIndex &parent) const
+{
+ if (!parent.isValid()) {
+ return static_cast<int>(m_ESPs.size());
+ } else {
+ return 0;
+ }
+}
+
+int PluginList::columnCount(const QModelIndex &) const
+{
+ return COL_LASTCOLUMN + 1;
+}
+
+void PluginList::testMasters()
+{
+ std::set<QString, FileNameComparator> enabledMasters;
+ for (const auto& iter: m_ESPs) {
+ if (iter.enabled) {
+ enabledMasters.insert(iter.name);
+ }
+ }
+
+ for (auto& iter: m_ESPs) {
+ iter.masterUnset.clear();
+ if (iter.enabled) {
+ for (const auto& master: iter.masters) {
+ if (enabledMasters.find(master) == enabledMasters.end()) {
+ iter.masterUnset.insert(master);
+ }
+ }
+ }
+ }
+}
+
+QVariant PluginList::data(const QModelIndex &modelIndex, int role) const
+{
+ int index = modelIndex.row();
+
+ if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
+ return displayData(modelIndex);
+ } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) {
+ return checkstateData(modelIndex);
+ } else if (role == Qt::ForegroundRole) {
+ return foregroundData(modelIndex);
+ } else if (role == Qt::BackgroundRole) {
+ return backgroundData(modelIndex);
+ } else if (role == Qt::FontRole) {
+ return fontData(modelIndex);
+ } else if (role == Qt::TextAlignmentRole) {
+ return alignmentData(modelIndex);
+ } else if (role == Qt::ToolTipRole) {
+ return tooltipData(modelIndex);
+ } else if (role == Qt::UserRole + 1) {
+ return iconData(modelIndex);
+ }
+ return QVariant();
+}
+
+QVariant PluginList::displayData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ switch (modelIndex.column())
+ {
+ case COL_NAME:
+ return m_ESPs[index].name;
+
+ case COL_PRIORITY:
+ return m_ESPs[index].priority;
+
+ case COL_MODINDEX:
+ return m_ESPs[index].index;
+
+ default:
+ return {};
+ }
+}
+
+QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].forceEnabled) {
+ return {};
+ }
+
+ return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked;
+}
+
+QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) {
+ return QBrush(Qt::gray);
+ }
+
+ return {};
+}
+
+QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].modSelected) {
+ return Settings::instance().colors().pluginListContained();
+ }
+
+ return {};
+}
+
+QVariant PluginList::fontData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ QFont result;
+
+ if (m_ESPs[index].hasMasterExtension ||
+ m_ESPs[index].isMasterFlagged ||
+ m_ESPs[index].hasLightExtension) {
+ result.setItalic(true);
+ result.setWeight(QFont::Bold);
+ } else if (m_ESPs[index].isLightFlagged) {
+ result.setItalic(true);
+ }
+
+ return result;
+}
+
+QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (modelIndex.column() == 0) {
+ return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+ } else {
+ return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
+ }
+}
+
+QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+ const auto& esp = m_ESPs[index];
+
+ QString toolTip;
+
+ toolTip += "<b>" + tr("Origin") + "</b>: " + esp.originName;
+
+ if (esp.forceEnabled) {
+ toolTip +=
+ "<br><b><i>" +
+ tr("This plugin can't be disabled (enforced by the game).") +
+ "</i></b>";
+ } else {
+ if (!esp.author.isEmpty()) {
+ toolTip +=
+ "<br><b>" + tr("Author") + "</b>: " +
+ TruncateString(esp.author);
+ }
+
+ if (esp.description.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Description") + "</b>: " +
+ TruncateString(esp.description);
+ }
+
+ if (esp.masterUnset.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Missing Masters") + "</b>: " +
+ "<b>" + TruncateString(QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) + "</b>";
+ }
+
+ std::set<QString> enabledMasters;
+ std::set_difference(esp.masters.begin(), esp.masters.end(),
+ esp.masterUnset.begin(), esp.masterUnset.end(),
+ std::inserter(enabledMasters, enabledMasters.end()));
+
+ if (!enabledMasters.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Enabled Masters") + "</b>: " +
+ TruncateString(SetJoin(enabledMasters, ", "));
+ }
+
+ if (!esp.archives.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Loads Archives") + "</b>: " +
+ TruncateString(QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) +
+ "<br>" + tr(
+ "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)");
+ }
+
+ if (esp.hasIni) {
+ toolTip +=
+ "<br><b>" + tr("Loads INI settings") + "</b>: "
+ "<br>" + tr(
+ "There is an ini file connected to this plugin. Its settings will "
+ "be added to your game settings, overwriting in case of conflicts.");
+ }
+
+ if (esp.isLightFlagged && !esp.hasLightExtension) {
+ toolTip +=
+ "<br><br>" + tr(
+ "This ESP is flagged as an ESL. It will adhere to the ESP load "
+ "order but the records will be loaded in ESL space.");
+ }
+ }
+
+
+ // additional info
+ auto itor = m_AdditionalInfo.find(esp.name);
+
+ if (itor != m_AdditionalInfo.end()) {
+ if (!itor->second.messages.isEmpty()) {
+ toolTip += "<hr><ul style=\"margin-left:15px; -qt-list-indent: 0;\">";
+
+ for (auto&& message : itor->second.messages) {
+ toolTip += "<li>" + message + "</li>";
+ }
+
+ toolTip += "</ul>";
+ }
+
+ // loot
+ toolTip += makeLootTooltip(itor->second.loot);
+ }
+
+ return toolTip;
+}
+
+QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
+{
+ QString s;
+
+ for (auto&& f : loot.incompatibilities) {
+ s +=
+ "<li>" + tr("Incompatible with %1")
+ .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
+ "</li>";
+ }
+
+ for (auto&& m : loot.missingMasters) {
+ s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
+ }
+
+ for (auto&& m : loot.messages) {
+ s += "<li>";
+
+ switch (m.type)
+ {
+ case log::Warning:
+ s += tr("Warning") + ": ";
+ break;
+
+ case log::Error:
+ s += tr("Error") + ": ";
+ break;
+
+ case log::Info: // fall-through
+ case log::Debug:
+ default:
+ // nothing
+ break;
+ }
+
+ s += m.text + "</li>";
+ }
+
+ for (auto&& d : loot.dirty) {
+ s += "<li>" + d.toString(false) + "</li>";
+ }
+
+ for (auto&& c : loot.clean) {
+ s += "<li>" + c.toString(true) + "</li>";
+ }
+
+ if (!s.isEmpty()) {
+ s =
+ "<hr>"
+ "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; -qt-list-indent: 0;\">" +
+ s +
+ "</ul>";
+ }
+
+ return s;
+}
+
+QVariant PluginList::iconData(const QModelIndex &modelIndex) const
+{
+ int index = modelIndex.row();
+
+ QVariantList result;
+
+ const auto& esp = m_ESPs[index];
+
+ auto infoItor = m_AdditionalInfo.find(esp.name);
+
+ const AdditionalInfo* info = nullptr;
+ if (infoItor != m_AdditionalInfo.end()) {
+ info = &infoItor->second;
+ }
+
+ if (isProblematic(esp, info)) {
+ result.append(":/MO/gui/warning");
+ }
+
+ if (m_LockedOrder.find(esp.name) != m_LockedOrder.end()) {
+ result.append(":/MO/gui/locked");
+ }
+
+ if (hasInfo(esp, info)) {
+ result.append(":/MO/gui/information");
+ }
+
+ if (esp.hasIni) {
+ result.append(":/MO/gui/attachment");
+ }
+
+ if (!esp.archives.empty()) {
+ result.append(":/MO/gui/archive_conflict_neutral");
+ }
+
+ if (esp.isLightFlagged && !esp.hasLightExtension) {
+ result.append(":/MO/gui/awaiting");
+ }
+
+ if (info && !info->loot.dirty.empty()) {
+ result.append(":/MO/gui/edit_clear");
+ }
+
+ return result;
+}
+
+bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (esp.masterUnset.size() > 0) {
+ return true;
+ }
+
+ if (info) {
+ if (!info->loot.incompatibilities.empty()) {
+ return true;
+ }
+
+ if (!info->loot.missingMasters.empty()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (info) {
+ if (!info->messages.empty()) {
+ return true;
+ }
+
+ if (!info->loot.messages.empty()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role)
+{
+ QString modName = modIndex.data().toString();
+ IPluginList::PluginStates oldState = state(modName);
+
+ bool result = false;
+
+ if (role == Qt::CheckStateRole) {
+ m_ESPs[modIndex.row()].enabled =
+ value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled;
+ m_LastCheck.restart();
+ emit dataChanged(modIndex, modIndex);
+
+ refreshLoadOrder();
+ emit writePluginsList();
+
+ result = true;
+ } else if (role == Qt::EditRole) {
+ if (modIndex.column() == COL_PRIORITY) {
+ bool ok = false;
+ int newPriority = value.toInt(&ok);
+ if (ok) {
+ setPluginPriority(modIndex.row(), newPriority);
+ result = true;
+ }
+ refreshLoadOrder();
+ emit writePluginsList();
+ }
+ }
+
+ IPluginList::PluginStates newState = state(modName);
+ if (oldState != newState) {
+ try {
+ pluginStatesChanged({ modName }, newState);
+ testMasters();
+ emit dataChanged(
+ this->index(0, 0),
+ this->index(static_cast<int>(m_ESPs.size()), columnCount()));
+ } catch (const std::exception &e) {
+ log::error("failed to invoke state changed notification: {}", e.what());
+ } catch (...) {
+ log::error("failed to invoke state changed notification: unknown exception");
+ }
+ }
+
+ return result;
+}
+
+QVariant PluginList::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation == Qt::Horizontal) {
+ if (role == Qt::DisplayRole) {
+ return getColumnName(section);
+ } else if (role == Qt::ToolTipRole) {
+ return getColumnToolTip(section);
+ }
+ }
+ return QAbstractItemModel::headerData(section, orientation, role);
+}
+
+Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const
+{
+ int index = modelIndex.row();
+ Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
+
+ if (modelIndex.isValid()) {
+ if (!m_ESPs[index].forceEnabled) {
+ result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
+ }
+ if (modelIndex.column() == COL_PRIORITY) {
+ result |= Qt::ItemIsEditable;
+ }
+ result &= ~Qt::ItemIsDropEnabled;
+ } else {
+ result |= Qt::ItemIsDropEnabled;
+ }
+
+ return result;
+}
+
+void PluginList::setPluginPriority(int row, int &newPriority, bool isForced)
+{
+ int newPriorityTemp = newPriority;
+
+ // enforce valid range
+ if (newPriorityTemp < 0)
+ newPriorityTemp = 0;
+ else if (newPriorityTemp >= static_cast<int>(m_ESPsByPriority.size()))
+ newPriorityTemp = static_cast<int>(m_ESPsByPriority.size()) - 1;
+
+ if (!m_ESPs[row].isMasterFlagged &&
+ !m_ESPs[row].hasLightExtension &&
+ !m_ESPs[row].hasMasterExtension) {
+ // don't allow esps to be moved above esms
+ while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged ||
+ m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension ||
+ m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension)) {
+ ++newPriorityTemp;
+ }
+ } else {
+ // don't allow esms to be moved below esps
+ while ((newPriorityTemp > 0) &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension) {
+ --newPriorityTemp;
+ }
+ // also don't allow "regular" esms to be moved above primary plugins
+ while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) {
+ ++newPriorityTemp;
+ }
+ }
+
+ int oldPriority = m_ESPs.at(row).priority;
+ if (newPriorityTemp < oldPriority) { // moving up
+ // don't allow plugins to be moved above their masters
+ for (auto master : m_ESPs[row].masters) {
+ auto iter = m_ESPsByName.find(master);
+ if (iter != m_ESPsByName.end()) {
+ int masterPriority = m_ESPs[iter->second].priority;
+ if (masterPriority >= newPriorityTemp)
+ {
+ newPriorityTemp = masterPriority + 1;
+ }
+ }
+ }
+ }
+ else if (newPriorityTemp > oldPriority) { // moving down
+ // don't allow masters to be moved below their children
+ for (int i = oldPriority + 1; i <= newPriorityTemp; i++) {
+ PluginList::ESPInfo* otherInfo = &m_ESPs.at(m_ESPsByPriority[i]);
+ for (auto master : otherInfo->masters) {
+ if (master.compare(m_ESPs[row].name, Qt::CaseInsensitive) == 0) {
+ newPriorityTemp = otherInfo->priority - 1;
+ break;
+ }
+ }
+ }
+ }
+
+ try {
+ if (newPriorityTemp != oldPriority) {
+ if (newPriorityTemp > oldPriority) {
+ // priority is higher than the old, so the gap we left is in lower priorities
+ for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) {
+ --m_ESPs.at(m_ESPsByPriority.at(i)).priority;
+ }
+ emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount()));
+ }
+ else {
+ for (int i = newPriorityTemp; i < oldPriority; ++i) {
+ ++m_ESPs.at(m_ESPsByPriority.at(i)).priority;
+ }
+ emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount()));
+ ++newPriority;
+ }
+
+ m_ESPs.at(row).priority = newPriorityTemp;
+ emit dataChanged(index(row, 0), index(row, columnCount()));
+ m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp);
+ }
+ } catch (const std::out_of_range&) {
+ reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name));
+ }
+
+ updateIndices();
+}
+
+void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
+{
+ ChangeBracket<PluginList> layoutChange(this);
+ const std::vector<ESPInfo> &esp = m_ESPs;
+
+ int minPriority = INT_MAX;
+ int maxPriority = INT_MIN;
+
+ // don't try to move plugins before force-enabled plugins
+ for (std::vector<ESPInfo>::const_iterator iter = m_ESPs.begin();
+ iter != m_ESPs.end(); ++iter) {
+ if (iter->forceEnabled) {
+ newPriority = std::max(newPriority, iter->priority + 1);
+ }
+ maxPriority = std::max(maxPriority, iter->priority + 1);
+ minPriority = std::min(minPriority, iter->priority);
+ }
+
+ // limit the new priority to existing priorities
+ newPriority = std::min(newPriority, maxPriority);
+ newPriority = std::max(newPriority, minPriority);
+
+ // sort the moving plugins by ascending priorities
+ std::sort(rows.begin(), rows.end(),
+ [&esp](const int &LHS, const int &RHS) {
+ return esp[LHS].priority < esp[RHS].priority;
+ });
+
+ // if at least on plugin is increasing in priority, the target index is
+ // that of the row BELOW the dropped location, otherwise it's the one above
+ for (std::vector<int>::const_iterator iter = rows.begin();
+ iter != rows.end(); ++iter) {
+ if (m_ESPs[*iter].priority < newPriority) {
+ --newPriority;
+ break;
+ }
+ }
+
+ for (std::vector<int>::const_iterator iter = rows.begin(); iter != rows.end(); ++iter) {
+ setPluginPriority(*iter, newPriority);
+ }
+
+ layoutChange.finish();
+ refreshLoadOrder();
+ emit writePluginsList();
+}
+
+bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent)
+{
+ if (action == Qt::IgnoreAction) {
+ return true;
+ }
+
+ QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist");
+ QDataStream stream(&encoded, QIODevice::ReadOnly);
+
+ std::vector<int> sourceRows;
+
+ while (!stream.atEnd()) {
+ int sourceRow, col;
+ QMap<int, QVariant> roleDataMap;
+ stream >> sourceRow >> col >> roleDataMap;
+ if (col == 0) { // only add each row once
+ sourceRows.push_back(sourceRow);
+ }
+ }
+
+ if (row == -1) {
+ row = parent.row();
+ }
+
+ int newPriority;
+
+ if ((row < 0) ||
+ (row >= static_cast<int>(m_ESPs.size()))) {
+ newPriority = static_cast<int>(m_ESPs.size());
+ } else {
+ newPriority = m_ESPs[row].priority;
+ }
+ changePluginPriority(sourceRows, newPriority);
+
+ return false;
+}
+
+QModelIndex PluginList::index(int row, int column, const QModelIndex&) const
+{
+ if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) {
+ return QModelIndex();
+ }
+ return createIndex(row, column, row);
+}
+
+QModelIndex PluginList::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
+ const QString &originName, const QString &fullPath,
+ bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
+ : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled),
+ priority(0), loadOrder(-1), originName(originName), hasIni(hasIni),
+ archives(archives.begin(), archives.end()), modSelected(false)
+{
+ try {
+ ESP::File file(ToWString(fullPath));
+ auto extension = name.right(3).toLower();
+ hasMasterExtension = (extension == "esm");
+ hasLightExtension = lightPluginsAreSupported && (extension == "esl");
+ isMasterFlagged = file.isMaster();
+ isLightFlagged = lightPluginsAreSupported && file.isLight();
+
+ author = QString::fromLatin1(file.author().c_str());
+ description = QString::fromLatin1(file.description().c_str());
+
+ for (auto&& m : file.masters()) {
+ masters.insert(QString::fromStdString(m));
+ }
+ } catch (const std::exception &e) {
+ log::error("failed to parse plugin file {}: {}", fullPath, e.what());
+ hasMasterExtension = false;
+ hasLightExtension = false;
+ isMasterFlagged = false;
+ isLightFlagged = false;
+ }
+}
+
+void PluginList::managedGameChanged(const IPluginGame *gamePlugin)
+{
+ m_GamePlugin = gamePlugin;
+}
diff --git a/src/pluginlist.h b/src/pluginlist.h
index 56ec9d3d..c21a6cee 100644
--- a/src/pluginlist.h
+++ b/src/pluginlist.h
@@ -1,419 +1,419 @@
-/*
-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 PLUGINLIST_H
-#define PLUGINLIST_H
-
-#include <ifiletree.h>
-#include <ipluginlist.h>
-#include "profile.h"
-#include "loot.h"
-
-namespace MOBase { class IPluginGame; }
-
-#include <QString>
-#include <QListWidget>
-#include <QTimer>
-#include <QTime>
-#include <QElapsedTimer>
-#include <QTemporaryFile>
-
-#pragma warning(push)
-#pragma warning(disable: 4100)
-#ifndef Q_MOC_RUN
-#include <boost/signals2.hpp>
-#include <boost/ptr_container/ptr_vector.hpp>
-#endif
-
-#include <vector>
-#include <map>
-
-class OrganizerCore;
-
-
-template <class C>
-class ChangeBracket {
-public:
- ChangeBracket(C *model)
- : m_Model(nullptr)
- {
- QVariant var = model->property("__aboutToChange");
- bool aboutToChange = var.isValid() && var.toBool();
- if (!aboutToChange) {
- model->layoutAboutToBeChanged();
- model->setProperty("__aboutToChange", true);
- m_Model = model;
- }
- }
- ~ChangeBracket() {
- finish();
- }
-
- void finish() {
- if (m_Model != nullptr) {
- m_Model->layoutChanged();
- m_Model->setProperty("__aboutToChange", false);
- m_Model = nullptr;
- }
- }
-
-private:
- C *m_Model;
-};
-
-
-
-/**
- * @brief model representing the plugins (.esp/.esm) in the current virtual data folder
- **/
-class PluginList : public QAbstractItemModel
-{
- Q_OBJECT
- friend class ChangeBracket<PluginList>;
-public:
-
- enum EColumn {
- COL_NAME,
- COL_FLAGS,
- COL_PRIORITY,
- COL_MODINDEX,
-
- COL_LASTCOLUMN = COL_MODINDEX
- };
-
- using PluginStates = MOBase::IPluginList::PluginStates;
-
- friend class PluginListProxy;
-
- using SignalRefreshed = boost::signals2::signal<void ()>;
- using SignalPluginMoved = boost::signals2::signal<void (const QString &, int, int)>;
- using SignalPluginStateChanged = boost::signals2::signal<void (const std::map<QString, PluginStates>&)>;
-
-public:
-
- /**
- * @brief constructor
- *
- * @param parent parent object
- **/
- PluginList(OrganizerCore &organizer);
-
- ~PluginList();
-
- /**
- * @brief does a complete refresh of the list
- *
- * @param profileName name of the current profile
- * @param baseDirectory the root directory structure representing the virtual data directory
- * @param lockedOrderFile list of plugins that shouldn't change load order
- * @todo the profile is not used? If it was, we should pass the Profile-object instead
- **/
- void refresh(const QString &profileName
- , const MOShared::DirectoryEntry &baseDirectory
- , const QString &lockedOrderFile
- , bool refresh);
-
- /**
- * @brief enable a plugin based on its name
- *
- * @param name name of the plugin to enable
- * @param enable set to true to enable the esp, false to disable it
- **/
- void enableESP(const QString &name, bool enable = true);
-
- /**
- * @brief test if a plugin is enabled
- *
- * @param name name of the plugin to look up
- * @return true if the plugin is enabled, false otherwise
- **/
- bool isEnabled(const QString &name);
-
- /**
- * @brief clear all additional information we stored on plugins
- */
- void clearAdditionalInformation();
-
- /**
- * @brief reset additional information on a mod
- * @param name name of the plugin to clear the information of
- */
- void clearInformation(const QString &name);
-
- /**
- * @brief add additional information on a mod (i.e. from loot)
- * @param name name of the plugin to add information about
- * @param message the message to add to the plugin
- */
- void addInformation(const QString &name, const QString &message);
-
- /**
- * adds information from a loot report
- */
- void addLootReport(const QString& name, Loot::Plugin plugin);
-
- /**
- * @brief test if a plugin is enabled
- *
- * @param index index of the plugin to look up
- * @return true if the plugin is enabled, false otherwise
- * @throws std::out_of_range exception is thrown if index is invalid
- **/
- bool isEnabled(int index);
-
- /**
- * @brief save the plugin status to the specified file
- *
- * @param lockedOrderFileName path of the lockedorder.txt to write to
- **/
- void saveTo(const QString &lockedOrderFileName) const;
-
- /**
- * @brief save the current load order
- *
- * the load order used by the game is defined by the last modification time which this
- * function sets. An exception is newer version of skyrim where the load order is defined
- * by the order of files in plugins.txt
- * @param directoryStructure the root directory structure representing the virtual data directory
- * @return true on success or if there was nothing to save, false if the load order can't be saved, i.e. because files are locked
- * @todo since this works on actual files the load order can't be configured per-profile. Files of the same name
- * in different mods can also have different load orders which makes this very intransparent
- * @note also stores to disk the list of locked esps
- **/
- bool saveLoadOrder(MOShared::DirectoryEntry &directoryStructure);
-
- /**
- * @return number of enabled plugins in the list
- */
- int enabledCount() const;
-
- int timeElapsedSinceLastChecked() const;
-
- QString getName(int index) const { return m_ESPs.at(index).name; }
- int getPriority(int index) const { return m_ESPs.at(index).priority; }
- QString getIndexPriority(int index) const;
- bool isESPLocked(int index) const;
- void lockESPIndex(int index, bool lock);
-
- static QString getColumnName(int column);
- static QString getColumnToolTip(int column);
-
- // highlight plugins contained in the mods at the given indices
- //
- void highlightPlugins(
- const std::vector<unsigned int>& modIndices,
- const MOShared::DirectoryEntry &directoryEntry);
-
- void refreshLoadOrder();
-
- void disconnectSlots();
-
-public:
-
- QStringList pluginNames() const;
- PluginStates state(const QString &name) const;
- void setState(const QString &name, PluginStates state);
- int priority(const QString &name) const;
- int loadOrder(const QString &name) const;
- bool setPriority(const QString& name, int newPriority);
- QStringList masters(const QString &name) const;
- QString origin(const QString &name) const;
- void setLoadOrder(const QStringList& pluginList);
-
- bool hasMasterExtension(const QString& name) const;
- bool hasLightExtension(const QString& name) const;
- bool isMasterFlagged(const QString& name) const;
- bool isLightFlagged(const QString& name) const;
-
- boost::signals2::connection onRefreshed(const std::function<void()>& callback);
- boost::signals2::connection onPluginMoved(const std::function<void(const QString&, int, int)>& func);
- boost::signals2::connection onPluginStateChanged(const std::function<void (const std::map<QString, PluginStates>&)> &func);
-
-public: // implementation of the QAbstractTableModel interface
-
- virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
- virtual int columnCount(const QModelIndex &parent = QModelIndex()) const;
- virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
- virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
- virtual Qt::ItemFlags flags(const QModelIndex &index) const;
- virtual Qt::DropActions supportedDropActions() const { return Qt::MoveAction; }
- virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
- virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
- virtual QModelIndex parent(const QModelIndex &child) const;
-
-public slots:
-
- // enable/disable all plugins
- //
- void setEnabledAll(bool enabled);
-
- // enable/disable plugins at the given indices.
- //
- void setEnabled(const QModelIndexList& indices, bool enabled);
-
- // send plugins to the given priority
- //
- void sendToPriority(const QModelIndexList& indices, int priority);
-
- // shift the priority of mods at the given indices by the given offset
- //
- void shiftPluginsPriority(const QModelIndexList& indices, int offset);
-
- // toggle the active state of mods at the given indices
- //
- void toggleState(const QModelIndexList& indices);
-
- /**
- * @brief The currently managed game has changed
- * @param gamePlugin
- */
- void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
-
- /**
- * @brief Generate the plugin indexes because something was changed
- **/
- void generatePluginIndexes();
-
-signals:
-
- /**
- * @brief emitted when the plugin list changed, i.e. the load order was modified or a plugin was checked/unchecked
- * @note this is currently only used to signal that there are changes that can be saved, it does
- * not immediately cause anything to be written to disc
- **/
- void esplist_changed();
-
- void writePluginsList();
-
-
-private:
-
- struct ESPInfo
- {
- ESPInfo(
- const QString &name, bool enabled, const QString &originName,
- const QString &fullPath, bool hasIni, std::set<QString> archives,
- bool lightSupported);
-
- QString name;
- QString fullPath;
- bool enabled;
- bool forceEnabled;
- int priority;
- QString index;
- int loadOrder;
- FILETIME time;
- QString originName;
- bool hasMasterExtension;
- bool hasLightExtension;
- bool isMasterFlagged;
- bool isLightFlagged;
- bool modSelected;
- QString author;
- QString description;
- bool hasIni;
- std::set<QString, MOBase::FileNameComparator> archives;
- std::set<QString, MOBase::FileNameComparator> masters;
- mutable std::set<QString, MOBase::FileNameComparator> masterUnset;
-
- bool operator < (const ESPInfo& str) const
- {
- return (loadOrder < str.loadOrder);
- }
- };
-
- struct AdditionalInfo {
- QStringList messages;
- Loot::Plugin loot;
- };
-
-private:
-
- void syncLoadOrder();
- void updateIndices();
-
- void writeLockedOrder(const QString &fileName) const;
-
- void readLockedOrderFrom(const QString &fileName);
- void setPluginPriority(int row, int &newPriority, bool isForced=false);
- void changePluginPriority(std::vector<int> rows, int newPriority);
-
- void testMasters();
-
- void fixPrimaryPlugins();
- void fixPriorities();
- void fixPluginRelationships();
-
- int findPluginByPriority(int priority);
-
- /**
- * @brief Notify MO2 plugins that the states of the given plugins have changed to the given state.
- *
- * @param pluginNames Names of the plugin.
- * @param state New state of the plugin.
- *
- */
- void pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const;
-
-private:
-
- OrganizerCore& m_Organizer;
-
- std::vector<ESPInfo> m_ESPs;
- mutable std::map<QString, QByteArray> m_LastSaveHash;
-
- std::map<QString, int, MOBase::FileNameComparator> m_ESPsByName;
- std::vector<int> m_ESPsByPriority;
-
- std::map<QString, int, MOBase::FileNameComparator> m_LockedOrder;
-
- std::map<QString, AdditionalInfo, MOBase::FileNameComparator> m_AdditionalInfo; // maps esp names to boss information
-
- QString m_CurrentProfile;
- QFontMetrics m_FontMetrics;
-
- SignalRefreshed m_Refreshed;
- SignalPluginMoved m_PluginMoved;
- SignalPluginStateChanged m_PluginStateChanged;
-
- QTemporaryFile m_TempFile;
-
- QElapsedTimer m_LastCheck;
-
- const MOBase::IPluginGame *m_GamePlugin;
-
-
- QVariant displayData(const QModelIndex &modelIndex) const;
- QVariant checkstateData(const QModelIndex &modelIndex) const;
- QVariant foregroundData(const QModelIndex &modelIndex) const;
- QVariant backgroundData(const QModelIndex &modelIndex) const;
- QVariant fontData(const QModelIndex &modelIndex) const;
- QVariant alignmentData(const QModelIndex &modelIndex) const;
- QVariant tooltipData(const QModelIndex &modelIndex) const;
- QVariant iconData(const QModelIndex &modelIndex) const;
-
- QString makeLootTooltip(const Loot::Plugin& loot) const;
- bool isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const;
- bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const;
-};
-
-#pragma warning(pop)
-
-#endif // PLUGINLIST_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 PLUGINLIST_H
+#define PLUGINLIST_H
+
+#include <ifiletree.h>
+#include <ipluginlist.h>
+#include "profile.h"
+#include "loot.h"
+
+namespace MOBase { class IPluginGame; }
+
+#include <QString>
+#include <QListWidget>
+#include <QTimer>
+#include <QTime>
+#include <QElapsedTimer>
+#include <QTemporaryFile>
+
+#pragma warning(push)
+#pragma warning(disable: 4100)
+#ifndef Q_MOC_RUN
+#include <boost/signals2.hpp>
+#include <boost/ptr_container/ptr_vector.hpp>
+#endif
+
+#include <vector>
+#include <map>
+
+class OrganizerCore;
+
+
+template <class C>
+class ChangeBracket {
+public:
+ ChangeBracket(C *model)
+ : m_Model(nullptr)
+ {
+ QVariant var = model->property("__aboutToChange");
+ bool aboutToChange = var.isValid() && var.toBool();
+ if (!aboutToChange) {
+ model->layoutAboutToBeChanged();
+ model->setProperty("__aboutToChange", true);
+ m_Model = model;
+ }
+ }
+ ~ChangeBracket() {
+ finish();
+ }
+
+ void finish() {
+ if (m_Model != nullptr) {
+ m_Model->layoutChanged();
+ m_Model->setProperty("__aboutToChange", false);
+ m_Model = nullptr;
+ }
+ }
+
+private:
+ C *m_Model;
+};
+
+
+
+/**
+ * @brief model representing the plugins (.esp/.esm) in the current virtual data folder
+ **/
+class PluginList : public QAbstractItemModel
+{
+ Q_OBJECT
+ friend class ChangeBracket<PluginList>;
+public:
+
+ enum EColumn {
+ COL_NAME,
+ COL_FLAGS,
+ COL_PRIORITY,
+ COL_MODINDEX,
+
+ COL_LASTCOLUMN = COL_MODINDEX
+ };
+
+ using PluginStates = MOBase::IPluginList::PluginStates;
+
+ friend class PluginListProxy;
+
+ using SignalRefreshed = boost::signals2::signal<void ()>;
+ using SignalPluginMoved = boost::signals2::signal<void (const QString &, int, int)>;
+ using SignalPluginStateChanged = boost::signals2::signal<void (const std::map<QString, PluginStates>&)>;
+
+public:
+
+ /**
+ * @brief constructor
+ *
+ * @param parent parent object
+ **/
+ PluginList(OrganizerCore &organizer);
+
+ ~PluginList();
+
+ /**
+ * @brief does a complete refresh of the list
+ *
+ * @param profileName name of the current profile
+ * @param baseDirectory the root directory structure representing the virtual data directory
+ * @param lockedOrderFile list of plugins that shouldn't change load order
+ * @todo the profile is not used? If it was, we should pass the Profile-object instead
+ **/
+ void refresh(const QString &profileName
+ , const MOShared::DirectoryEntry &baseDirectory
+ , const QString &lockedOrderFile
+ , bool refresh);
+
+ /**
+ * @brief enable a plugin based on its name
+ *
+ * @param name name of the plugin to enable
+ * @param enable set to true to enable the esp, false to disable it
+ **/
+ void enableESP(const QString &name, bool enable = true);
+
+ /**
+ * @brief test if a plugin is enabled
+ *
+ * @param name name of the plugin to look up
+ * @return true if the plugin is enabled, false otherwise
+ **/
+ bool isEnabled(const QString &name);
+
+ /**
+ * @brief clear all additional information we stored on plugins
+ */
+ void clearAdditionalInformation();
+
+ /**
+ * @brief reset additional information on a mod
+ * @param name name of the plugin to clear the information of
+ */
+ void clearInformation(const QString &name);
+
+ /**
+ * @brief add additional information on a mod (i.e. from loot)
+ * @param name name of the plugin to add information about
+ * @param message the message to add to the plugin
+ */
+ void addInformation(const QString &name, const QString &message);
+
+ /**
+ * adds information from a loot report
+ */
+ void addLootReport(const QString& name, Loot::Plugin plugin);
+
+ /**
+ * @brief test if a plugin is enabled
+ *
+ * @param index index of the plugin to look up
+ * @return true if the plugin is enabled, false otherwise
+ * @throws std::out_of_range exception is thrown if index is invalid
+ **/
+ bool isEnabled(int index);
+
+ /**
+ * @brief save the plugin status to the specified file
+ *
+ * @param lockedOrderFileName path of the lockedorder.txt to write to
+ **/
+ void saveTo(const QString &lockedOrderFileName) const;
+
+ /**
+ * @brief save the current load order
+ *
+ * the load order used by the game is defined by the last modification time which this
+ * function sets. An exception is newer version of skyrim where the load order is defined
+ * by the order of files in plugins.txt
+ * @param directoryStructure the root directory structure representing the virtual data directory
+ * @return true on success or if there was nothing to save, false if the load order can't be saved, i.e. because files are locked
+ * @todo since this works on actual files the load order can't be configured per-profile. Files of the same name
+ * in different mods can also have different load orders which makes this very intransparent
+ * @note also stores to disk the list of locked esps
+ **/
+ bool saveLoadOrder(MOShared::DirectoryEntry &directoryStructure);
+
+ /**
+ * @return number of enabled plugins in the list
+ */
+ int enabledCount() const;
+
+ int timeElapsedSinceLastChecked() const;
+
+ QString getName(int index) const { return m_ESPs.at(index).name; }
+ int getPriority(int index) const { return m_ESPs.at(index).priority; }
+ QString getIndexPriority(int index) const;
+ bool isESPLocked(int index) const;
+ void lockESPIndex(int index, bool lock);
+
+ static QString getColumnName(int column);
+ static QString getColumnToolTip(int column);
+
+ // highlight plugins contained in the mods at the given indices
+ //
+ void highlightPlugins(
+ const std::vector<unsigned int>& modIndices,
+ const MOShared::DirectoryEntry &directoryEntry);
+
+ void refreshLoadOrder();
+
+ void disconnectSlots();
+
+public:
+
+ QStringList pluginNames() const;
+ PluginStates state(const QString &name) const;
+ void setState(const QString &name, PluginStates state);
+ int priority(const QString &name) const;
+ int loadOrder(const QString &name) const;
+ bool setPriority(const QString& name, int newPriority);
+ QStringList masters(const QString &name) const;
+ QString origin(const QString &name) const;
+ void setLoadOrder(const QStringList& pluginList);
+
+ bool hasMasterExtension(const QString& name) const;
+ bool hasLightExtension(const QString& name) const;
+ bool isMasterFlagged(const QString& name) const;
+ bool isLightFlagged(const QString& name) const;
+
+ boost::signals2::connection onRefreshed(const std::function<void()>& callback);
+ boost::signals2::connection onPluginMoved(const std::function<void(const QString&, int, int)>& func);
+ boost::signals2::connection onPluginStateChanged(const std::function<void (const std::map<QString, PluginStates>&)> &func);
+
+public: // implementation of the QAbstractTableModel interface
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual int columnCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const;
+ virtual Qt::DropActions supportedDropActions() const { return Qt::MoveAction; }
+ virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+ virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
+ virtual QModelIndex parent(const QModelIndex &child) const;
+
+public slots:
+
+ // enable/disable all plugins
+ //
+ void setEnabledAll(bool enabled);
+
+ // enable/disable plugins at the given indices.
+ //
+ void setEnabled(const QModelIndexList& indices, bool enabled);
+
+ // send plugins to the given priority
+ //
+ void sendToPriority(const QModelIndexList& indices, int priority);
+
+ // shift the priority of mods at the given indices by the given offset
+ //
+ void shiftPluginsPriority(const QModelIndexList& indices, int offset);
+
+ // toggle the active state of mods at the given indices
+ //
+ void toggleState(const QModelIndexList& indices);
+
+ /**
+ * @brief The currently managed game has changed
+ * @param gamePlugin
+ */
+ void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
+
+ /**
+ * @brief Generate the plugin indexes because something was changed
+ **/
+ void generatePluginIndexes();
+
+signals:
+
+ /**
+ * @brief emitted when the plugin list changed, i.e. the load order was modified or a plugin was checked/unchecked
+ * @note this is currently only used to signal that there are changes that can be saved, it does
+ * not immediately cause anything to be written to disc
+ **/
+ void esplist_changed();
+
+ void writePluginsList();
+
+
+private:
+
+ struct ESPInfo
+ {
+ ESPInfo(
+ const QString &name, bool enabled, const QString &originName,
+ const QString &fullPath, bool hasIni, std::set<QString> archives,
+ bool lightSupported);
+
+ QString name;
+ QString fullPath;
+ bool enabled;
+ bool forceEnabled;
+ int priority;
+ QString index;
+ int loadOrder;
+ FILETIME time;
+ QString originName;
+ bool hasMasterExtension;
+ bool hasLightExtension;
+ bool isMasterFlagged;
+ bool isLightFlagged;
+ bool modSelected;
+ QString author;
+ QString description;
+ bool hasIni;
+ std::set<QString, MOBase::FileNameComparator> archives;
+ std::set<QString, MOBase::FileNameComparator> masters;
+ mutable std::set<QString, MOBase::FileNameComparator> masterUnset;
+
+ bool operator < (const ESPInfo& str) const
+ {
+ return (loadOrder < str.loadOrder);
+ }
+ };
+
+ struct AdditionalInfo {
+ QStringList messages;
+ Loot::Plugin loot;
+ };
+
+private:
+
+ void syncLoadOrder();
+ void updateIndices();
+
+ void writeLockedOrder(const QString &fileName) const;
+
+ void readLockedOrderFrom(const QString &fileName);
+ void setPluginPriority(int row, int &newPriority, bool isForced=false);
+ void changePluginPriority(std::vector<int> rows, int newPriority);
+
+ void testMasters();
+
+ void fixPrimaryPlugins();
+ void fixPriorities();
+ void fixPluginRelationships();
+
+ int findPluginByPriority(int priority);
+
+ /**
+ * @brief Notify MO2 plugins that the states of the given plugins have changed to the given state.
+ *
+ * @param pluginNames Names of the plugin.
+ * @param state New state of the plugin.
+ *
+ */
+ void pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const;
+
+private:
+
+ OrganizerCore& m_Organizer;
+
+ std::vector<ESPInfo> m_ESPs;
+ mutable std::map<QString, QByteArray> m_LastSaveHash;
+
+ std::map<QString, int, MOBase::FileNameComparator> m_ESPsByName;
+ std::vector<int> m_ESPsByPriority;
+
+ std::map<QString, int, MOBase::FileNameComparator> m_LockedOrder;
+
+ std::map<QString, AdditionalInfo, MOBase::FileNameComparator> m_AdditionalInfo; // maps esp names to boss information
+
+ QString m_CurrentProfile;
+ QFontMetrics m_FontMetrics;
+
+ SignalRefreshed m_Refreshed;
+ SignalPluginMoved m_PluginMoved;
+ SignalPluginStateChanged m_PluginStateChanged;
+
+ QTemporaryFile m_TempFile;
+
+ QElapsedTimer m_LastCheck;
+
+ const MOBase::IPluginGame *m_GamePlugin;
+
+
+ QVariant displayData(const QModelIndex &modelIndex) const;
+ QVariant checkstateData(const QModelIndex &modelIndex) const;
+ QVariant foregroundData(const QModelIndex &modelIndex) const;
+ QVariant backgroundData(const QModelIndex &modelIndex) const;
+ QVariant fontData(const QModelIndex &modelIndex) const;
+ QVariant alignmentData(const QModelIndex &modelIndex) const;
+ QVariant tooltipData(const QModelIndex &modelIndex) const;
+ QVariant iconData(const QModelIndex &modelIndex) const;
+
+ QString makeLootTooltip(const Loot::Plugin& loot) const;
+ bool isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const;
+ bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const;
+};
+
+#pragma warning(pop)
+
+#endif // PLUGINLIST_H
diff --git a/src/pluginlistsortproxy.cpp b/src/pluginlistsortproxy.cpp
index 308a1398..3c10cfa5 100644
--- a/src/pluginlistsortproxy.cpp
+++ b/src/pluginlistsortproxy.cpp
@@ -1,153 +1,153 @@
-/*
-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 "pluginlistsortproxy.h"
-#include "messagedialog.h"
-#include <QMenu>
-#include <QCheckBox>
-#include <QApplication>
-#include <QWidgetAction>
-#include <QTreeView>
-
-
-PluginListSortProxy::PluginListSortProxy(QObject *parent)
- : QSortFilterProxyModel(parent),
- m_SortIndex(0), m_SortOrder(Qt::AscendingOrder)
-{
- m_EnabledColumns.set(PluginList::COL_NAME);
- m_EnabledColumns.set(PluginList::COL_PRIORITY);
- m_EnabledColumns.set(PluginList::COL_MODINDEX);
- this->setDynamicSortFilter(true);
-}
-
-void PluginListSortProxy::setEnabledColumns(unsigned int columns)
-{
- emit layoutAboutToBeChanged();
- for (int i = 0; i <= PluginList::COL_LASTCOLUMN; ++i) {
- m_EnabledColumns.set(i, (columns & (1 << i)) != 0);
- }
- emit layoutChanged();
-}
-
-void PluginListSortProxy::updateFilter(const QString &filter)
-{
- m_CurrentFilter = filter;
- invalidateFilter();
-}
-
-bool PluginListSortProxy::filterAcceptsRow(int row, const QModelIndex&) const
-{
- return filterMatchesPlugin(sourceModel()->data(sourceModel()->index(row, 0)).toString());
-}
-
-bool PluginListSortProxy::lessThan(const QModelIndex &left,
- const QModelIndex &right) const
-{
- PluginList *plugins = qobject_cast<PluginList*>(sourceModel());
- switch (left.column()) {
- case PluginList::COL_NAME: {
- return QString::compare(plugins->getName(left.row()), plugins->getName(right.row()), Qt::CaseInsensitive) < 0;
- } break;
- case PluginList::COL_FLAGS: {
- QVariantList lhsList = left.data(Qt::UserRole + 1).toList();
- QVariantList rhsList = right.data(Qt::UserRole + 1).toList();
- if (lhsList.size() != rhsList.size()) {
- return lhsList.size() < rhsList.size();
- } else {
- for (int i = 0; i < lhsList.size(); ++i) {
- if (lhsList.at(i) != rhsList.at(i)) {
- return lhsList.at(i).toString() < rhsList.at(i).toString();
- }
- }
- return false;
- }
- } break;
- case PluginList::COL_MODINDEX: {
- QString leftVal = plugins->getIndexPriority(left.row());
- QString rightVal = plugins->getIndexPriority(right.row());
- return leftVal < rightVal;
- } break;
- default: {
- return plugins->getPriority(left.row()) < plugins->getPriority(right.row());
- } break;
- }
-}
-
-bool PluginListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
- int row, int column, const QModelIndex &parent)
-{
- if ((sortColumn() != PluginList::COL_PRIORITY)
- && (sortColumn() != PluginList::COL_MODINDEX)) {
- QWidget *wid = qApp->activeWindow()->findChild<QTreeView*>("espList");
- MessageDialog::showMessage(tr("Drag&Drop is only supported when sorting by priority or mod index"), wid);
- return false;
- }
-
- if ((row == -1) && (column == -1)) {
- return this->sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
- }
- // in the regular model, when dropping between rows, the row-value passed to
- // the sourceModel is inconsistent between ascending and descending ordering.
- // This should fix that
- if (sortOrder() == Qt::DescendingOrder) {
- --row;
- }
-
- QModelIndex proxyIndex = index(row, column, parent);
- QModelIndex sourceIndex = mapToSource(proxyIndex);
- return this->sourceModel()->dropMimeData(data, action, sourceIndex.row(), sourceIndex.column(),
- sourceIndex.parent());
-}
-
-bool PluginListSortProxy::filterMatchesPlugin(const QString &plugin) const
-{
- if (!m_CurrentFilter.isEmpty()) {
-
- bool display = false;
- QString filterCopy = QString(m_CurrentFilter);
- filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
- QStringList ORList = filterCopy.split(";", Qt::SkipEmptyParts);
-
- bool segmentGood = true;
-
- //split in ORSegments that internally use AND logic
- for (auto& ORSegment : ORList) {
- QStringList ANDKeywords = ORSegment.split(" ", Qt::SkipEmptyParts);
- segmentGood = true;
-
- //check each word in the segment for match, each word needs to be matched but it doesn't matter where.
- for (auto& currentKeyword : ANDKeywords) {
- if (!plugin.contains(currentKeyword, Qt::CaseInsensitive)) {
- segmentGood = false;
- break;
- }
- }
-
- if (segmentGood) {
- //the last AND loop didn't break so the ORSegments is true so mod matches filter
- return true;
- }
-
- }//for ORList loop
-
- return false;
- }
- else
- return true;
-}
+/*
+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 "pluginlistsortproxy.h"
+#include "messagedialog.h"
+#include <QMenu>
+#include <QCheckBox>
+#include <QApplication>
+#include <QWidgetAction>
+#include <QTreeView>
+
+
+PluginListSortProxy::PluginListSortProxy(QObject *parent)
+ : QSortFilterProxyModel(parent),
+ m_SortIndex(0), m_SortOrder(Qt::AscendingOrder)
+{
+ m_EnabledColumns.set(PluginList::COL_NAME);
+ m_EnabledColumns.set(PluginList::COL_PRIORITY);
+ m_EnabledColumns.set(PluginList::COL_MODINDEX);
+ this->setDynamicSortFilter(true);
+}
+
+void PluginListSortProxy::setEnabledColumns(unsigned int columns)
+{
+ emit layoutAboutToBeChanged();
+ for (int i = 0; i <= PluginList::COL_LASTCOLUMN; ++i) {
+ m_EnabledColumns.set(i, (columns & (1 << i)) != 0);
+ }
+ emit layoutChanged();
+}
+
+void PluginListSortProxy::updateFilter(const QString &filter)
+{
+ m_CurrentFilter = filter;
+ invalidateFilter();
+}
+
+bool PluginListSortProxy::filterAcceptsRow(int row, const QModelIndex&) const
+{
+ return filterMatchesPlugin(sourceModel()->data(sourceModel()->index(row, 0)).toString());
+}
+
+bool PluginListSortProxy::lessThan(const QModelIndex &left,
+ const QModelIndex &right) const
+{
+ PluginList *plugins = qobject_cast<PluginList*>(sourceModel());
+ switch (left.column()) {
+ case PluginList::COL_NAME: {
+ return QString::compare(plugins->getName(left.row()), plugins->getName(right.row()), Qt::CaseInsensitive) < 0;
+ } break;
+ case PluginList::COL_FLAGS: {
+ QVariantList lhsList = left.data(Qt::UserRole + 1).toList();
+ QVariantList rhsList = right.data(Qt::UserRole + 1).toList();
+ if (lhsList.size() != rhsList.size()) {
+ return lhsList.size() < rhsList.size();
+ } else {
+ for (int i = 0; i < lhsList.size(); ++i) {
+ if (lhsList.at(i) != rhsList.at(i)) {
+ return lhsList.at(i).toString() < rhsList.at(i).toString();
+ }
+ }
+ return false;
+ }
+ } break;
+ case PluginList::COL_MODINDEX: {
+ QString leftVal = plugins->getIndexPriority(left.row());
+ QString rightVal = plugins->getIndexPriority(right.row());
+ return leftVal < rightVal;
+ } break;
+ default: {
+ return plugins->getPriority(left.row()) < plugins->getPriority(right.row());
+ } break;
+ }
+}
+
+bool PluginListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int row, int column, const QModelIndex &parent)
+{
+ if ((sortColumn() != PluginList::COL_PRIORITY)
+ && (sortColumn() != PluginList::COL_MODINDEX)) {
+ QWidget *wid = qApp->activeWindow()->findChild<QTreeView*>("espList");
+ MessageDialog::showMessage(tr("Drag&Drop is only supported when sorting by priority or mod index"), wid);
+ return false;
+ }
+
+ if ((row == -1) && (column == -1)) {
+ return this->sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
+ }
+ // in the regular model, when dropping between rows, the row-value passed to
+ // the sourceModel is inconsistent between ascending and descending ordering.
+ // This should fix that
+ if (sortOrder() == Qt::DescendingOrder) {
+ --row;
+ }
+
+ QModelIndex proxyIndex = index(row, column, parent);
+ QModelIndex sourceIndex = mapToSource(proxyIndex);
+ return this->sourceModel()->dropMimeData(data, action, sourceIndex.row(), sourceIndex.column(),
+ sourceIndex.parent());
+}
+
+bool PluginListSortProxy::filterMatchesPlugin(const QString &plugin) const
+{
+ if (!m_CurrentFilter.isEmpty()) {
+
+ bool display = false;
+ QString filterCopy = QString(m_CurrentFilter);
+ filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
+ QStringList ORList = filterCopy.split(";", Qt::SkipEmptyParts);
+
+ bool segmentGood = true;
+
+ //split in ORSegments that internally use AND logic
+ for (auto& ORSegment : ORList) {
+ QStringList ANDKeywords = ORSegment.split(" ", Qt::SkipEmptyParts);
+ segmentGood = true;
+
+ //check each word in the segment for match, each word needs to be matched but it doesn't matter where.
+ for (auto& currentKeyword : ANDKeywords) {
+ if (!plugin.contains(currentKeyword, Qt::CaseInsensitive)) {
+ segmentGood = false;
+ break;
+ }
+ }
+
+ if (segmentGood) {
+ //the last AND loop didn't break so the ORSegments is true so mod matches filter
+ return true;
+ }
+
+ }//for ORList loop
+
+ return false;
+ }
+ else
+ return true;
+}
diff --git a/src/pluginlistsortproxy.h b/src/pluginlistsortproxy.h
index 2acf83b3..85e0d135 100644
--- a/src/pluginlistsortproxy.h
+++ b/src/pluginlistsortproxy.h
@@ -1,68 +1,68 @@
-/*
-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 PLUGINLISTSORTPROXY_H
-#define PLUGINLISTSORTPROXY_H
-
-
-#include <bitset>
-#include <QSortFilterProxyModel>
-#include "pluginlist.h"
-
-
-class PluginListSortProxy : public QSortFilterProxyModel
-{
- Q_OBJECT
-public:
-
- enum ESorting {
- SORT_ASCENDING,
- SORT_DESCENDING
- };
-
-public:
-
- explicit PluginListSortProxy(QObject *parent = 0);
-
- void setEnabledColumns(unsigned int columns);
-
- virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
-
- bool filterMatchesPlugin(const QString &plugin) const;
-
-public slots:
-
- void updateFilter(const QString &filter);
-
-protected:
-
- virtual bool filterAcceptsRow(int row, const QModelIndex &parent) const;
- virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
-
-private:
-
- int m_SortIndex;
- Qt::SortOrder m_SortOrder;
-
- std::bitset<PluginList::COL_LASTCOLUMN + 1> m_EnabledColumns;
- QString m_CurrentFilter;
-
-};
-
-#endif // PLUGINLISTSORTPROXY_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 PLUGINLISTSORTPROXY_H
+#define PLUGINLISTSORTPROXY_H
+
+
+#include <bitset>
+#include <QSortFilterProxyModel>
+#include "pluginlist.h"
+
+
+class PluginListSortProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+
+ enum ESorting {
+ SORT_ASCENDING,
+ SORT_DESCENDING
+ };
+
+public:
+
+ explicit PluginListSortProxy(QObject *parent = 0);
+
+ void setEnabledColumns(unsigned int columns);
+
+ virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+
+ bool filterMatchesPlugin(const QString &plugin) const;
+
+public slots:
+
+ void updateFilter(const QString &filter);
+
+protected:
+
+ virtual bool filterAcceptsRow(int row, const QModelIndex &parent) const;
+ virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
+
+private:
+
+ int m_SortIndex;
+ Qt::SortOrder m_SortOrder;
+
+ std::bitset<PluginList::COL_LASTCOLUMN + 1> m_EnabledColumns;
+ QString m_CurrentFilter;
+
+};
+
+#endif // PLUGINLISTSORTPROXY_H
diff --git a/src/previewdialog.cpp b/src/previewdialog.cpp
index 91a5f13e..350f2980 100644
--- a/src/previewdialog.cpp
+++ b/src/previewdialog.cpp
@@ -1,64 +1,64 @@
-#include "previewdialog.h"
-#include "ui_previewdialog.h"
-#include "settings.h"
-#include <QFileInfo>
-
-PreviewDialog::PreviewDialog(const QString &fileName, QWidget *parent) :
- QDialog(parent),
- ui(new Ui::PreviewDialog)
-{
- ui->setupUi(this);
- ui->nameLabel->setText(QFileInfo(fileName).fileName());
- ui->nextButton->setEnabled(false);
- ui->previousButton->setEnabled(false);
-}
-
-PreviewDialog::~PreviewDialog()
-{
- delete ui;
-}
-
-int PreviewDialog::exec()
-{
- GeometrySaver gs(Settings::instance(), this);
- return QDialog::exec();
-}
-
-void PreviewDialog::addVariant(const QString &modName, QWidget *widget)
-{
- widget->setProperty("modName", modName);
- ui->variantsStack->addWidget(widget);
- if (ui->variantsStack->count() > 1) {
- ui->nextButton->setEnabled(true);
- ui->previousButton->setEnabled(true);
- }
-}
-
-int PreviewDialog::numVariants() const
-{
- return ui->variantsStack->count();
-}
-
-void PreviewDialog::on_variantsStack_currentChanged(int index)
-{
- ui->modLabel->setText(ui->variantsStack->widget(index)->property("modName").toString());
-}
-
-void PreviewDialog::on_closeButton_clicked()
-{
- this->accept();
-}
-
-void PreviewDialog::on_previousButton_clicked()
-{
- int i = ui->variantsStack->currentIndex() - 1;
- if (i < 0) {
- i = ui->variantsStack->count() - 1;
- }
- ui->variantsStack->setCurrentIndex(i);
-}
-
-void PreviewDialog::on_nextButton_clicked()
-{
- ui->variantsStack->setCurrentIndex((ui->variantsStack->currentIndex() + 1) % ui->variantsStack->count());
-}
+#include "previewdialog.h"
+#include "ui_previewdialog.h"
+#include "settings.h"
+#include <QFileInfo>
+
+PreviewDialog::PreviewDialog(const QString &fileName, QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::PreviewDialog)
+{
+ ui->setupUi(this);
+ ui->nameLabel->setText(QFileInfo(fileName).fileName());
+ ui->nextButton->setEnabled(false);
+ ui->previousButton->setEnabled(false);
+}
+
+PreviewDialog::~PreviewDialog()
+{
+ delete ui;
+}
+
+int PreviewDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
+void PreviewDialog::addVariant(const QString &modName, QWidget *widget)
+{
+ widget->setProperty("modName", modName);
+ ui->variantsStack->addWidget(widget);
+ if (ui->variantsStack->count() > 1) {
+ ui->nextButton->setEnabled(true);
+ ui->previousButton->setEnabled(true);
+ }
+}
+
+int PreviewDialog::numVariants() const
+{
+ return ui->variantsStack->count();
+}
+
+void PreviewDialog::on_variantsStack_currentChanged(int index)
+{
+ ui->modLabel->setText(ui->variantsStack->widget(index)->property("modName").toString());
+}
+
+void PreviewDialog::on_closeButton_clicked()
+{
+ this->accept();
+}
+
+void PreviewDialog::on_previousButton_clicked()
+{
+ int i = ui->variantsStack->currentIndex() - 1;
+ if (i < 0) {
+ i = ui->variantsStack->count() - 1;
+ }
+ ui->variantsStack->setCurrentIndex(i);
+}
+
+void PreviewDialog::on_nextButton_clicked()
+{
+ ui->variantsStack->setCurrentIndex((ui->variantsStack->currentIndex() + 1) % ui->variantsStack->count());
+}
diff --git a/src/previewdialog.h b/src/previewdialog.h
index 9525f127..213bf078 100644
--- a/src/previewdialog.h
+++ b/src/previewdialog.h
@@ -1,40 +1,40 @@
-#ifndef PREVIEWDIALOG_H
-#define PREVIEWDIALOG_H
-
-#include <QDialog>
-
-namespace Ui {
-class PreviewDialog;
-}
-
-class PreviewDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit PreviewDialog(const QString &fileName, QWidget *parent = 0);
- ~PreviewDialog();
-
- // also saves and restores geometry
- //
- int exec() override;
-
- void addVariant(const QString &modName, QWidget *widget);
- int numVariants() const;
-
-private slots:
-
- void on_variantsStack_currentChanged(int arg1);
-
- void on_closeButton_clicked();
-
- void on_previousButton_clicked();
-
- void on_nextButton_clicked();
-
-private:
-
- Ui::PreviewDialog *ui;
-};
-
-#endif // PREVIEWDIALOG_H
+#ifndef PREVIEWDIALOG_H
+#define PREVIEWDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+class PreviewDialog;
+}
+
+class PreviewDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit PreviewDialog(const QString &fileName, QWidget *parent = 0);
+ ~PreviewDialog();
+
+ // also saves and restores geometry
+ //
+ int exec() override;
+
+ void addVariant(const QString &modName, QWidget *widget);
+ int numVariants() const;
+
+private slots:
+
+ void on_variantsStack_currentChanged(int arg1);
+
+ void on_closeButton_clicked();
+
+ void on_previousButton_clicked();
+
+ void on_nextButton_clicked();
+
+private:
+
+ Ui::PreviewDialog *ui;
+};
+
+#endif // PREVIEWDIALOG_H
diff --git a/src/previewgenerator.cpp b/src/previewgenerator.cpp
index 22332af3..7006ad1b 100644
--- a/src/previewgenerator.cpp
+++ b/src/previewgenerator.cpp
@@ -1,58 +1,58 @@
-/*
-Copyright (C) 2014 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 "previewgenerator.h"
-
-#include <QFileInfo>
-#include <QLabel>
-#include <QImageReader>
-#include <QTextEdit>
-#include <utility.h>
-
-#include "plugincontainer.h"
-
-using namespace MOBase;
-
-PreviewGenerator::PreviewGenerator(const PluginContainer& pluginContainer) :
- m_PluginContainer(pluginContainer) {
- m_MaxSize = QGuiApplication::primaryScreen()->size() * 0.8;
-}
-
-bool PreviewGenerator::previewSupported(const QString &fileExtension) const
-{
- auto& previews = m_PluginContainer.plugins<IPluginPreview>();
- for (auto* preview : previews) {
- if (preview->supportedExtensions().contains(fileExtension)) {
- return true;
- }
- }
- return false;
-}
-
-QWidget *PreviewGenerator::genPreview(const QString &fileName) const
-{
- const QString ext = QFileInfo(fileName).suffix().toLower();
- auto& previews = m_PluginContainer.plugins<IPluginPreview>();
- for (auto* preview : previews) {
- if (m_PluginContainer.isEnabled(preview) && preview->supportedExtensions().contains(ext)) {
- return preview->genFilePreview(fileName, m_MaxSize);
- }
- }
- return nullptr;
-}
+/*
+Copyright (C) 2014 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 "previewgenerator.h"
+
+#include <QFileInfo>
+#include <QLabel>
+#include <QImageReader>
+#include <QTextEdit>
+#include <utility.h>
+
+#include "plugincontainer.h"
+
+using namespace MOBase;
+
+PreviewGenerator::PreviewGenerator(const PluginContainer& pluginContainer) :
+ m_PluginContainer(pluginContainer) {
+ m_MaxSize = QGuiApplication::primaryScreen()->size() * 0.8;
+}
+
+bool PreviewGenerator::previewSupported(const QString &fileExtension) const
+{
+ auto& previews = m_PluginContainer.plugins<IPluginPreview>();
+ for (auto* preview : previews) {
+ if (preview->supportedExtensions().contains(fileExtension)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+QWidget *PreviewGenerator::genPreview(const QString &fileName) const
+{
+ const QString ext = QFileInfo(fileName).suffix().toLower();
+ auto& previews = m_PluginContainer.plugins<IPluginPreview>();
+ for (auto* preview : previews) {
+ if (m_PluginContainer.isEnabled(preview) && preview->supportedExtensions().contains(ext)) {
+ return preview->genFilePreview(fileName, m_MaxSize);
+ }
+ }
+ return nullptr;
+}
diff --git a/src/previewgenerator.h b/src/previewgenerator.h
index 8e491116..3b0076b6 100644
--- a/src/previewgenerator.h
+++ b/src/previewgenerator.h
@@ -1,47 +1,47 @@
-/*
-Copyright (C) 2014 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 PREVIEWGENERATOR_H
-#define PREVIEWGENERATOR_H
-
-#include <QString>
-#include <QWidget>
-#include <map>
-#include <functional>
-#include <ipluginpreview.h>
-
-class PluginContainer;
-
-class PreviewGenerator
-{
-public:
- PreviewGenerator(const PluginContainer& pluginContainer);
-
- bool previewSupported(const QString &fileExtension) const;
-
- QWidget *genPreview(const QString &fileName) const;
-
-private:
-
- const PluginContainer& m_PluginContainer;
- QSize m_MaxSize;
-
-};
-
-#endif // PREVIEWGENERATOR_H
+/*
+Copyright (C) 2014 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 PREVIEWGENERATOR_H
+#define PREVIEWGENERATOR_H
+
+#include <QString>
+#include <QWidget>
+#include <map>
+#include <functional>
+#include <ipluginpreview.h>
+
+class PluginContainer;
+
+class PreviewGenerator
+{
+public:
+ PreviewGenerator(const PluginContainer& pluginContainer);
+
+ bool previewSupported(const QString &fileExtension) const;
+
+ QWidget *genPreview(const QString &fileName) const;
+
+private:
+
+ const PluginContainer& m_PluginContainer;
+ QSize m_MaxSize;
+
+};
+
+#endif // PREVIEWGENERATOR_H
diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp
index d72f8c20..f099f533 100644
--- a/src/problemsdialog.cpp
+++ b/src/problemsdialog.cpp
@@ -1,113 +1,113 @@
-#include "problemsdialog.h"
-#include "ui_problemsdialog.h"
-#include "organizercore.h"
-#include <utility.h>
-#include <iplugin.h>
-#include <iplugindiagnose.h>
-#include <QPushButton>
-#include <Shellapi.h>
-
-#include "plugincontainer.h"
-
-using namespace MOBase;
-
-
-ProblemsDialog::ProblemsDialog(const PluginContainer& pluginContainer, QWidget *parent) :
- QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginContainer(pluginContainer),
- m_hasProblems(false)
-{
- ui->setupUi(this);
-
- runDiagnosis();
-
- connect(ui->problemsWidget, SIGNAL(itemSelectionChanged()), this, SLOT(selectionChanged()));
- connect(ui->descriptionText, SIGNAL(anchorClicked(QUrl)), this, SLOT(urlClicked(QUrl)));
-}
-
-
-ProblemsDialog::~ProblemsDialog()
-{
- delete ui;
-}
-
-int ProblemsDialog::exec()
-{
- GeometrySaver gs(Settings::instance(), this);
- return QDialog::exec();
-}
-
-void ProblemsDialog::runDiagnosis()
-{
- m_hasProblems = false;
- ui->problemsWidget->clear();
-
- for (IPluginDiagnose *diagnose : m_PluginContainer.plugins<IPluginDiagnose>()) {
- if (!m_PluginContainer.isEnabled(diagnose)) {
- continue;
- }
-
- std::vector<unsigned int> activeProblems = diagnose->activeProblems();
- foreach (unsigned int key, activeProblems) {
- QTreeWidgetItem *newItem = new QTreeWidgetItem();
- newItem->setText(0, diagnose->shortDescription(key));
- newItem->setData(0, Qt::UserRole, diagnose->fullDescription(key));
-
- ui->problemsWidget->addTopLevelItem(newItem);
- m_hasProblems = true;
-
- if (diagnose->hasGuidedFix(key)) {
- newItem->setText(1, tr("Fix"));
- QPushButton *fixButton = new QPushButton(tr("Fix"));
- fixButton->setProperty("fix", QVariant::fromValue(reinterpret_cast<void*>(diagnose)));
- fixButton->setProperty("key", key);
- connect(fixButton, SIGNAL(clicked()), this, SLOT(startFix()));
- ui->problemsWidget->setItemWidget(newItem, 1, fixButton);
- } else {
- newItem->setText(1, tr("No guided fix"));
- }
- }
- }
-
- if (!m_hasProblems) {
- auto* item = new QTreeWidgetItem;
-
- item->setText(0, tr("(There are no notifications)"));
- item->setText(1, "");
- item->setData(0, Qt::UserRole, QString());
-
- QFont font = item->font(0);
- font.setItalic(true);
- item->setFont(0, font);
-
- ui->problemsWidget->addTopLevelItem(item);
- }
-}
-
-bool ProblemsDialog::hasProblems() const
-{
- return m_hasProblems;
-}
-
-void ProblemsDialog::selectionChanged()
-{
- QString text = ui->problemsWidget->currentItem()->data(0, Qt::UserRole).toString();
- ui->descriptionText->setText(text);
- ui->descriptionText->setLineWrapMode(text.contains('\n') ? QTextEdit::NoWrap : QTextEdit::WidgetWidth);
-}
-
-void ProblemsDialog::startFix()
-{
- QObject *fixButton = QObject::sender();
- if (fixButton == NULL) {
- log::warn("no button");
- return;
- }
- IPluginDiagnose *plugin = reinterpret_cast<IPluginDiagnose*>(fixButton ->property("fix").value<void*>());
- plugin->startGuidedFix(fixButton ->property("key").toUInt());
- runDiagnosis();
-}
-
-void ProblemsDialog::urlClicked(const QUrl &url)
-{
- shell::Open(url);
-}
+#include "problemsdialog.h"
+#include "ui_problemsdialog.h"
+#include "organizercore.h"
+#include <utility.h>
+#include <iplugin.h>
+#include <iplugindiagnose.h>
+#include <QPushButton>
+#include <Shellapi.h>
+
+#include "plugincontainer.h"
+
+using namespace MOBase;
+
+
+ProblemsDialog::ProblemsDialog(const PluginContainer& pluginContainer, QWidget *parent) :
+ QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginContainer(pluginContainer),
+ m_hasProblems(false)
+{
+ ui->setupUi(this);
+
+ runDiagnosis();
+
+ connect(ui->problemsWidget, SIGNAL(itemSelectionChanged()), this, SLOT(selectionChanged()));
+ connect(ui->descriptionText, SIGNAL(anchorClicked(QUrl)), this, SLOT(urlClicked(QUrl)));
+}
+
+
+ProblemsDialog::~ProblemsDialog()
+{
+ delete ui;
+}
+
+int ProblemsDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
+void ProblemsDialog::runDiagnosis()
+{
+ m_hasProblems = false;
+ ui->problemsWidget->clear();
+
+ for (IPluginDiagnose *diagnose : m_PluginContainer.plugins<IPluginDiagnose>()) {
+ if (!m_PluginContainer.isEnabled(diagnose)) {
+ continue;
+ }
+
+ std::vector<unsigned int> activeProblems = diagnose->activeProblems();
+ foreach (unsigned int key, activeProblems) {
+ QTreeWidgetItem *newItem = new QTreeWidgetItem();
+ newItem->setText(0, diagnose->shortDescription(key));
+ newItem->setData(0, Qt::UserRole, diagnose->fullDescription(key));
+
+ ui->problemsWidget->addTopLevelItem(newItem);
+ m_hasProblems = true;
+
+ if (diagnose->hasGuidedFix(key)) {
+ newItem->setText(1, tr("Fix"));
+ QPushButton *fixButton = new QPushButton(tr("Fix"));
+ fixButton->setProperty("fix", QVariant::fromValue(reinterpret_cast<void*>(diagnose)));
+ fixButton->setProperty("key", key);
+ connect(fixButton, SIGNAL(clicked()), this, SLOT(startFix()));
+ ui->problemsWidget->setItemWidget(newItem, 1, fixButton);
+ } else {
+ newItem->setText(1, tr("No guided fix"));
+ }
+ }
+ }
+
+ if (!m_hasProblems) {
+ auto* item = new QTreeWidgetItem;
+
+ item->setText(0, tr("(There are no notifications)"));
+ item->setText(1, "");
+ item->setData(0, Qt::UserRole, QString());
+
+ QFont font = item->font(0);
+ font.setItalic(true);
+ item->setFont(0, font);
+
+ ui->problemsWidget->addTopLevelItem(item);
+ }
+}
+
+bool ProblemsDialog::hasProblems() const
+{
+ return m_hasProblems;
+}
+
+void ProblemsDialog::selectionChanged()
+{
+ QString text = ui->problemsWidget->currentItem()->data(0, Qt::UserRole).toString();
+ ui->descriptionText->setText(text);
+ ui->descriptionText->setLineWrapMode(text.contains('\n') ? QTextEdit::NoWrap : QTextEdit::WidgetWidth);
+}
+
+void ProblemsDialog::startFix()
+{
+ QObject *fixButton = QObject::sender();
+ if (fixButton == NULL) {
+ log::warn("no button");
+ return;
+ }
+ IPluginDiagnose *plugin = reinterpret_cast<IPluginDiagnose*>(fixButton ->property("fix").value<void*>());
+ plugin->startGuidedFix(fixButton ->property("key").toUInt());
+ runDiagnosis();
+}
+
+void ProblemsDialog::urlClicked(const QUrl &url)
+{
+ shell::Open(url);
+}
diff --git a/src/problemsdialog.h b/src/problemsdialog.h
index 504a0a10..9fe5acd0 100644
--- a/src/problemsdialog.h
+++ b/src/problemsdialog.h
@@ -1,45 +1,45 @@
-#ifndef PROBLEMSDIALOG_H
-#define PROBLEMSDIALOG_H
-
-
-#include <QDialog>
-#include <QUrl>
-#include <iplugindiagnose.h>
-
-
-namespace Ui {
-class ProblemsDialog;
-}
-
-class PluginContainer;
-
-class ProblemsDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit ProblemsDialog(PluginContainer const& pluginContainer, QWidget *parent = 0);
- ~ProblemsDialog();
-
- // also saves and restores geometry
- //
- int exec() override;
-
- bool hasProblems() const;
-
-private:
- void runDiagnosis();
-
-private slots:
- void selectionChanged();
- void urlClicked(const QUrl &url);
-
- void startFix();
-
-private:
- Ui::ProblemsDialog *ui;
- const PluginContainer& m_PluginContainer;
- bool m_hasProblems;
-};
-
-#endif // PROBLEMSDIALOG_H
+#ifndef PROBLEMSDIALOG_H
+#define PROBLEMSDIALOG_H
+
+
+#include <QDialog>
+#include <QUrl>
+#include <iplugindiagnose.h>
+
+
+namespace Ui {
+class ProblemsDialog;
+}
+
+class PluginContainer;
+
+class ProblemsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ProblemsDialog(PluginContainer const& pluginContainer, QWidget *parent = 0);
+ ~ProblemsDialog();
+
+ // also saves and restores geometry
+ //
+ int exec() override;
+
+ bool hasProblems() const;
+
+private:
+ void runDiagnosis();
+
+private slots:
+ void selectionChanged();
+ void urlClicked(const QUrl &url);
+
+ void startFix();
+
+private:
+ Ui::ProblemsDialog *ui;
+ const PluginContainer& m_PluginContainer;
+ bool m_hasProblems;
+};
+
+#endif // PROBLEMSDIALOG_H
diff --git a/src/profileinputdialog.cpp b/src/profileinputdialog.cpp
index 48b35b82..bac04586 100644
--- a/src/profileinputdialog.cpp
+++ b/src/profileinputdialog.cpp
@@ -1,48 +1,48 @@
-/*
-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 "profileinputdialog.h"
-#include "ui_profileinputdialog.h"
-#include "filesystemutilities.h"
-
-ProfileInputDialog::ProfileInputDialog(QWidget *parent) :
- QDialog(parent),
- ui(new Ui::ProfileInputDialog)
-{
- ui->setupUi(this);
-}
-
-ProfileInputDialog::~ProfileInputDialog()
-{
- delete ui;
-}
-
-QString ProfileInputDialog::getName() const
-{
- QString result = ui->nameEdit->text();
- MOBase::fixDirectoryName(result);
- return result;
-}
-
-
-bool ProfileInputDialog::getPreferDefaultSettings() const
-{
- return ui->defaultSettingsBox->checkState() == Qt::Checked;
-}
-
+/*
+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 "profileinputdialog.h"
+#include "ui_profileinputdialog.h"
+#include "filesystemutilities.h"
+
+ProfileInputDialog::ProfileInputDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::ProfileInputDialog)
+{
+ ui->setupUi(this);
+}
+
+ProfileInputDialog::~ProfileInputDialog()
+{
+ delete ui;
+}
+
+QString ProfileInputDialog::getName() const
+{
+ QString result = ui->nameEdit->text();
+ MOBase::fixDirectoryName(result);
+ return result;
+}
+
+
+bool ProfileInputDialog::getPreferDefaultSettings() const
+{
+ return ui->defaultSettingsBox->checkState() == Qt::Checked;
+}
+
diff --git a/src/profileinputdialog.h b/src/profileinputdialog.h
index 8a132666..a114b9b0 100644
--- a/src/profileinputdialog.h
+++ b/src/profileinputdialog.h
@@ -1,44 +1,44 @@
-/*
-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 PROFILEINPUTDIALOG_H
-#define PROFILEINPUTDIALOG_H
-
-#include <QDialog>
-
-namespace Ui {
-class ProfileInputDialog;
-}
-
-class ProfileInputDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit ProfileInputDialog(QWidget *parent = 0);
- ~ProfileInputDialog();
-
- QString getName() const;
- bool getPreferDefaultSettings() const;
-
-private:
- Ui::ProfileInputDialog *ui;
-};
-
-#endif // PROFILEINPUTDIALOG_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 PROFILEINPUTDIALOG_H
+#define PROFILEINPUTDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+class ProfileInputDialog;
+}
+
+class ProfileInputDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ProfileInputDialog(QWidget *parent = 0);
+ ~ProfileInputDialog();
+
+ QString getName() const;
+ bool getPreferDefaultSettings() const;
+
+private:
+ Ui::ProfileInputDialog *ui;
+};
+
+#endif // PROFILEINPUTDIALOG_H
diff --git a/src/profilesdialog.cpp b/src/profilesdialog.cpp
index 3ab70936..4abd95ac 100644
--- a/src/profilesdialog.cpp
+++ b/src/profilesdialog.cpp
@@ -1,403 +1,403 @@
-/*
-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 "profilesdialog.h"
-#include "ui_profilesdialog.h"
-
-#include "shared/appconfig.h"
-#include "bsainvalidation.h"
-#include "iplugingame.h"
-#include "organizercore.h"
-#include "profile.h"
-#include "profileinputdialog.h"
-#include "report.h"
-#include "transfersavesdialog.h"
-#include "filesystemutilities.h"
-#include "settings.h"
-#include "localsavegames.h"
-
-#include <QDir>
-#include <QDirIterator>
-#include <QInputDialog>
-#include <QLineEdit>
-#include <QListWidgetItem>
-#include <QMessageBox>
-#include <QWhatsThis>
-
-#include <Windows.h>
-
-#include <exception>
-
-using namespace MOBase;
-using namespace MOShared;
-
-Q_DECLARE_METATYPE(Profile::Ptr)
-
-
-ProfilesDialog::ProfilesDialog(const QString &profileName, OrganizerCore &organizer, QWidget *parent)
- : TutorableDialog("Profiles", parent)
- , ui(new Ui::ProfilesDialog)
- , m_FailState(false)
- , m_Game(organizer.managedGame())
- , m_ActiveProfileName("")
-{
- ui->setupUi(this);
-
- QDir profilesDir(Settings::instance().paths().profiles());
- profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
-
- QDirIterator profileIter(profilesDir);
-
- while (profileIter.hasNext()) {
- profileIter.next();
- QListWidgetItem *item = addItem(profileIter.filePath());
- if (profileName == profileIter.fileName()) {
- ui->profilesList->setCurrentItem(item);
- m_ActiveProfileName = profileName;
- }
- }
-
- BSAInvalidation *invalidation = m_Game->feature<BSAInvalidation>();
-
- if (invalidation == nullptr) {
- ui->invalidationBox->setToolTip(tr("Archive invalidation isn't required for this game."));
- ui->invalidationBox->setEnabled(false);
- }
-
- if (!m_Game->feature<LocalSavegames>()) {
- ui->localSavesBox->setToolTip(tr("This game does not support profile-specific game saves."));
- ui->localSavesBox->setEnabled(false);
- }
-
- connect(this, &ProfilesDialog::profileCreated, &organizer, &OrganizerCore::profileCreated);
- connect(this, &ProfilesDialog::profileRenamed, &organizer, &OrganizerCore::profileRenamed);
- connect(this, &ProfilesDialog::profileRemoved, &organizer, &OrganizerCore::profileRemoved);
-}
-
-ProfilesDialog::~ProfilesDialog()
-{
- delete ui;
-}
-
-int ProfilesDialog::exec()
-{
- GeometrySaver gs(Settings::instance(), this);
- return QDialog::exec();
-}
-
-void ProfilesDialog::showEvent(QShowEvent *event)
-{
- TutorableDialog::showEvent(event);
-
- if (ui->profilesList->count() == 0) {
- QPoint pos = ui->profilesList->mapToGlobal(QPoint(0, 0));
- pos.rx() += ui->profilesList->width() / 2;
- pos.ry() += (ui->profilesList->height() / 2) - 20;
- QWhatsThis::showText(pos,
- QObject::tr("Before you can use ModOrganizer, you need to create at least one profile. "
- "ATTENTION: Run the game at least once before creating a profile!"), ui->profilesList);
- }
-}
-
-void ProfilesDialog::on_close_clicked()
-{
- close();
-}
-
-void ProfilesDialog::on_select_clicked()
-{
- const Profile::Ptr currentProfile = ui->profilesList->currentItem()
- ->data(Qt::UserRole)
- .value<Profile::Ptr>();
-
- if (!currentProfile) {
- return;
- }
-
- m_Selected = currentProfile->name();
- close();
-}
-
-std::optional<QString> ProfilesDialog::selectedProfile() const
-{
- return m_Selected;
-}
-
-QListWidgetItem *ProfilesDialog::addItem(const QString &name)
-{
- QDir profileDir(name);
- QListWidgetItem *newItem = new QListWidgetItem(profileDir.dirName(), ui->profilesList);
- try {
- newItem->setData(Qt::UserRole, QVariant::fromValue(Profile::Ptr(new Profile(profileDir, m_Game))));
- m_FailState = false;
- } catch (const std::exception& e) {
- reportError(tr("failed to create profile: %1").arg(e.what()));
- }
- return newItem;
-}
-
-void ProfilesDialog::createProfile(const QString &name, bool useDefaultSettings)
-{
- try {
- QListWidgetItem *newItem = new QListWidgetItem(name, ui->profilesList);
- auto profile = Profile::Ptr(new Profile(name, m_Game, useDefaultSettings));
- newItem->setData(Qt::UserRole, QVariant::fromValue(profile));
- ui->profilesList->addItem(newItem);
- m_FailState = false;
- ui->profilesList->setCurrentItem(newItem);
- emit profileCreated(profile.get());
- } catch (const std::exception&) {
- m_FailState = true;
- throw;
- }
-}
-
-void ProfilesDialog::createProfile(const QString &name, const Profile &reference)
-{
- try {
- QListWidgetItem *newItem = new QListWidgetItem(name, ui->profilesList);
- auto profile = Profile::Ptr(Profile::createPtrFrom(name, reference, m_Game));
- newItem->setData(Qt::UserRole, QVariant::fromValue(profile));
- ui->profilesList->addItem(newItem);
- m_FailState = false;
- ui->profilesList->setCurrentItem(newItem);
- emit profileCreated(profile.get());
- } catch (const std::exception&) {
- m_FailState = true;
- throw;
- }
-}
-
-void ProfilesDialog::on_addProfileButton_clicked()
-{
- ProfileInputDialog dialog(this);
- bool okClicked = dialog.exec();
- QString name = dialog.getName();
-
- if (okClicked && (name.size() > 0)) {
- try {
- createProfile(name, dialog.getPreferDefaultSettings());
- } catch (const std::exception &e) {
- reportError(tr("failed to create profile: %1").arg(e.what()));
- }
- }
-}
-
-void ProfilesDialog::on_copyProfileButton_clicked()
-{
- bool okClicked;
- QString name = QInputDialog::getText(
- this, tr("Name"), tr("Please enter a name for the new profile"),
- QLineEdit::Normal, QString(), &okClicked);
- fixDirectoryName(name);
- if (okClicked) {
- if (name.size() > 0) {
- try {
- const Profile::Ptr currentProfile = ui->profilesList->currentItem()
- ->data(Qt::UserRole)
- .value<Profile::Ptr>();
- createProfile(name, *currentProfile);
- } catch (const std::exception &e) {
- reportError(tr("failed to copy profile: %1").arg(e.what()));
- }
- } else {
- QMessageBox::warning(this, tr("Invalid name"),
- tr("Invalid profile name"));
- }
- }
-}
-
-void ProfilesDialog::on_removeProfileButton_clicked()
-{
- Profile::Ptr profileToDelete = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
- if (profileToDelete->name() == m_ActiveProfileName) {
- QMessageBox::warning(this, tr("Deleting active profile"),
- tr("Unable to delete active profile. Please change to a different profile first."));
- return;
- }
-
- QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), tr("Are you sure you want to remove this profile (including profile-specific save games, if any)?"),
- QMessageBox::Yes | QMessageBox::No, this);
-
- if (confirmBox.exec() == QMessageBox::Yes) {
- QString profilePath;
- if (profileToDelete.get() == nullptr) {
- profilePath = Settings::instance().paths().profiles()
- + "/" + ui->profilesList->currentItem()->text();
- if (QMessageBox::question(this, tr("Profile broken"),
- tr("This profile you're about to delete seems to be broken or the path is invalid. "
- "I'm about to delete the following folder: \"%1\". Proceed?").arg(profilePath), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
- return;
- }
- } else {
- // on destruction, the profile object would write the profile.ini file again, so
- // we have to get rid of the it before deleting the directory
- profilePath = profileToDelete->absolutePath();
- }
- QListWidgetItem* item = ui->profilesList->takeItem(ui->profilesList->currentRow());
- if (item != nullptr) {
- delete item;
- }
- if (!shellDelete(QStringList(profilePath))) {
- log::warn("Failed to shell-delete \"{}\" (errorcode {}), trying regular delete", profilePath, ::GetLastError());
- if (!removeDir(profilePath)) {
- log::warn("regular delete failed too");
- }
- }
-
- emit profileRemoved(profileToDelete->name());
- }
-}
-
-
-void ProfilesDialog::on_renameButton_clicked()
-{
- Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
-
- if (currentProfile->name() == m_ActiveProfileName) {
- QMessageBox::warning(this, tr("Renaming active profile"),
- tr("The active profile cannot be renamed. Please change to a different profile first."));
- return;
- }
-
- bool valid = false;
- QString name;
-
- while (!valid) {
- bool ok = false;
- name = QInputDialog::getText(this, tr("Rename Profile"), tr("New Name"),
- QLineEdit::Normal, currentProfile->name(),
- &ok);
- valid = fixDirectoryName(name);
- if (!ok) {
- return;
- }
- }
-
- ui->profilesList->currentItem()->setText(name);
-
- QString oldName = currentProfile->name();
- currentProfile->rename(name);
-
- emit profileRenamed(currentProfile.get(), oldName, name);
-}
-
-
-void ProfilesDialog::on_invalidationBox_stateChanged(int state)
-{
- QListWidgetItem *currentItem = ui->profilesList->currentItem();
- if (currentItem == nullptr) {
- return;
- }
- if (!ui->invalidationBox->isEnabled()) {
- return;
- }
- try {
- QVariant currentProfileVariant = currentItem->data(Qt::UserRole);
- if (!currentProfileVariant.isValid() || currentProfileVariant.isNull()) {
- return;
- }
- const Profile::Ptr currentProfile = currentItem->data(Qt::UserRole).value<Profile::Ptr>();
- if (state == Qt::Unchecked) {
- currentProfile->deactivateInvalidation();
- } else {
- currentProfile->activateInvalidation();
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to change archive invalidation state: %1").arg(e.what()));
- }
-}
-
-
-void ProfilesDialog::on_profilesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem*)
-{
- if (current != nullptr) {
- if (!current->data(Qt::UserRole).isValid()) return;
- const Profile::Ptr currentProfile = current->data(Qt::UserRole).value<Profile::Ptr>();
-
- try {
- bool invalidationSupported = false;
- ui->invalidationBox->blockSignals(true);
- ui->invalidationBox->setChecked(currentProfile->invalidationActive(&invalidationSupported));
- ui->invalidationBox->setEnabled(invalidationSupported);
- ui->invalidationBox->blockSignals(false);
-
- bool localSaves = currentProfile->localSavesEnabled();
- ui->transferButton->setEnabled(localSaves);
- // prevent the stateChanged-event for the saves-box from triggering, otherwise it may think local saves
- // were disabled and delete the files/rename the dir
- ui->localSavesBox->blockSignals(true);
- ui->localSavesBox->setChecked(localSaves);
- ui->localSavesBox->blockSignals(false);
-
- ui->copyProfileButton->setEnabled(true);
- ui->removeProfileButton->setEnabled(true);
- ui->renameButton->setEnabled(true);
-
- ui->localIniFilesBox->blockSignals(true);
- ui->localIniFilesBox->setChecked(currentProfile->localSettingsEnabled());
- ui->localIniFilesBox->blockSignals(false);
- } catch (const std::exception& E) {
- reportError(tr("failed to determine if invalidation is active: %1").arg(E.what()));
- ui->copyProfileButton->setEnabled(false);
- ui->removeProfileButton->setEnabled(false);
- ui->renameButton->setEnabled(false);
- ui->invalidationBox->setChecked(false);
- }
- } else {
- ui->invalidationBox->setChecked(false);
- ui->copyProfileButton->setEnabled(false);
- ui->removeProfileButton->setEnabled(false);
- ui->renameButton->setEnabled(false);
- }
-}
-
-void ProfilesDialog::on_profilesList_itemActivated(QListWidgetItem* item)
-{
- on_select_clicked();
-}
-
-void ProfilesDialog::on_localSavesBox_stateChanged(int state)
-{
- Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
-
- if (currentProfile->enableLocalSaves(state == Qt::Checked)) {
- ui->transferButton->setEnabled(state == Qt::Checked);
- } else {
- // revert checkbox-state
- ui->localSavesBox->setChecked(state != Qt::Checked);
- }
-}
-
-void ProfilesDialog::on_transferButton_clicked()
-{
- const Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
- TransferSavesDialog transferDialog(*currentProfile, m_Game, this);
- transferDialog.exec();
-}
-
-void ProfilesDialog::on_localIniFilesBox_stateChanged(int state)
-{
- Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
-
- if (!currentProfile->enableLocalSettings(state == Qt::Checked)) {
- // revert checkbox-state
- ui->localIniFilesBox->setChecked(state != Qt::Checked);
- }
-}
+/*
+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 "profilesdialog.h"
+#include "ui_profilesdialog.h"
+
+#include "shared/appconfig.h"
+#include "bsainvalidation.h"
+#include "iplugingame.h"
+#include "organizercore.h"
+#include "profile.h"
+#include "profileinputdialog.h"
+#include "report.h"
+#include "transfersavesdialog.h"
+#include "filesystemutilities.h"
+#include "settings.h"
+#include "localsavegames.h"
+
+#include <QDir>
+#include <QDirIterator>
+#include <QInputDialog>
+#include <QLineEdit>
+#include <QListWidgetItem>
+#include <QMessageBox>
+#include <QWhatsThis>
+
+#include <Windows.h>
+
+#include <exception>
+
+using namespace MOBase;
+using namespace MOShared;
+
+Q_DECLARE_METATYPE(Profile::Ptr)
+
+
+ProfilesDialog::ProfilesDialog(const QString &profileName, OrganizerCore &organizer, QWidget *parent)
+ : TutorableDialog("Profiles", parent)
+ , ui(new Ui::ProfilesDialog)
+ , m_FailState(false)
+ , m_Game(organizer.managedGame())
+ , m_ActiveProfileName("")
+{
+ ui->setupUi(this);
+
+ QDir profilesDir(Settings::instance().paths().profiles());
+ profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
+
+ QDirIterator profileIter(profilesDir);
+
+ while (profileIter.hasNext()) {
+ profileIter.next();
+ QListWidgetItem *item = addItem(profileIter.filePath());
+ if (profileName == profileIter.fileName()) {
+ ui->profilesList->setCurrentItem(item);
+ m_ActiveProfileName = profileName;
+ }
+ }
+
+ BSAInvalidation *invalidation = m_Game->feature<BSAInvalidation>();
+
+ if (invalidation == nullptr) {
+ ui->invalidationBox->setToolTip(tr("Archive invalidation isn't required for this game."));
+ ui->invalidationBox->setEnabled(false);
+ }
+
+ if (!m_Game->feature<LocalSavegames>()) {
+ ui->localSavesBox->setToolTip(tr("This game does not support profile-specific game saves."));
+ ui->localSavesBox->setEnabled(false);
+ }
+
+ connect(this, &ProfilesDialog::profileCreated, &organizer, &OrganizerCore::profileCreated);
+ connect(this, &ProfilesDialog::profileRenamed, &organizer, &OrganizerCore::profileRenamed);
+ connect(this, &ProfilesDialog::profileRemoved, &organizer, &OrganizerCore::profileRemoved);
+}
+
+ProfilesDialog::~ProfilesDialog()
+{
+ delete ui;
+}
+
+int ProfilesDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
+void ProfilesDialog::showEvent(QShowEvent *event)
+{
+ TutorableDialog::showEvent(event);
+
+ if (ui->profilesList->count() == 0) {
+ QPoint pos = ui->profilesList->mapToGlobal(QPoint(0, 0));
+ pos.rx() += ui->profilesList->width() / 2;
+ pos.ry() += (ui->profilesList->height() / 2) - 20;
+ QWhatsThis::showText(pos,
+ QObject::tr("Before you can use ModOrganizer, you need to create at least one profile. "
+ "ATTENTION: Run the game at least once before creating a profile!"), ui->profilesList);
+ }
+}
+
+void ProfilesDialog::on_close_clicked()
+{
+ close();
+}
+
+void ProfilesDialog::on_select_clicked()
+{
+ const Profile::Ptr currentProfile = ui->profilesList->currentItem()
+ ->data(Qt::UserRole)
+ .value<Profile::Ptr>();
+
+ if (!currentProfile) {
+ return;
+ }
+
+ m_Selected = currentProfile->name();
+ close();
+}
+
+std::optional<QString> ProfilesDialog::selectedProfile() const
+{
+ return m_Selected;
+}
+
+QListWidgetItem *ProfilesDialog::addItem(const QString &name)
+{
+ QDir profileDir(name);
+ QListWidgetItem *newItem = new QListWidgetItem(profileDir.dirName(), ui->profilesList);
+ try {
+ newItem->setData(Qt::UserRole, QVariant::fromValue(Profile::Ptr(new Profile(profileDir, m_Game))));
+ m_FailState = false;
+ } catch (const std::exception& e) {
+ reportError(tr("failed to create profile: %1").arg(e.what()));
+ }
+ return newItem;
+}
+
+void ProfilesDialog::createProfile(const QString &name, bool useDefaultSettings)
+{
+ try {
+ QListWidgetItem *newItem = new QListWidgetItem(name, ui->profilesList);
+ auto profile = Profile::Ptr(new Profile(name, m_Game, useDefaultSettings));
+ newItem->setData(Qt::UserRole, QVariant::fromValue(profile));
+ ui->profilesList->addItem(newItem);
+ m_FailState = false;
+ ui->profilesList->setCurrentItem(newItem);
+ emit profileCreated(profile.get());
+ } catch (const std::exception&) {
+ m_FailState = true;
+ throw;
+ }
+}
+
+void ProfilesDialog::createProfile(const QString &name, const Profile &reference)
+{
+ try {
+ QListWidgetItem *newItem = new QListWidgetItem(name, ui->profilesList);
+ auto profile = Profile::Ptr(Profile::createPtrFrom(name, reference, m_Game));
+ newItem->setData(Qt::UserRole, QVariant::fromValue(profile));
+ ui->profilesList->addItem(newItem);
+ m_FailState = false;
+ ui->profilesList->setCurrentItem(newItem);
+ emit profileCreated(profile.get());
+ } catch (const std::exception&) {
+ m_FailState = true;
+ throw;
+ }
+}
+
+void ProfilesDialog::on_addProfileButton_clicked()
+{
+ ProfileInputDialog dialog(this);
+ bool okClicked = dialog.exec();
+ QString name = dialog.getName();
+
+ if (okClicked && (name.size() > 0)) {
+ try {
+ createProfile(name, dialog.getPreferDefaultSettings());
+ } catch (const std::exception &e) {
+ reportError(tr("failed to create profile: %1").arg(e.what()));
+ }
+ }
+}
+
+void ProfilesDialog::on_copyProfileButton_clicked()
+{
+ bool okClicked;
+ QString name = QInputDialog::getText(
+ this, tr("Name"), tr("Please enter a name for the new profile"),
+ QLineEdit::Normal, QString(), &okClicked);
+ fixDirectoryName(name);
+ if (okClicked) {
+ if (name.size() > 0) {
+ try {
+ const Profile::Ptr currentProfile = ui->profilesList->currentItem()
+ ->data(Qt::UserRole)
+ .value<Profile::Ptr>();
+ createProfile(name, *currentProfile);
+ } catch (const std::exception &e) {
+ reportError(tr("failed to copy profile: %1").arg(e.what()));
+ }
+ } else {
+ QMessageBox::warning(this, tr("Invalid name"),
+ tr("Invalid profile name"));
+ }
+ }
+}
+
+void ProfilesDialog::on_removeProfileButton_clicked()
+{
+ Profile::Ptr profileToDelete = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
+ if (profileToDelete->name() == m_ActiveProfileName) {
+ QMessageBox::warning(this, tr("Deleting active profile"),
+ tr("Unable to delete active profile. Please change to a different profile first."));
+ return;
+ }
+
+ QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), tr("Are you sure you want to remove this profile (including profile-specific save games, if any)?"),
+ QMessageBox::Yes | QMessageBox::No, this);
+
+ if (confirmBox.exec() == QMessageBox::Yes) {
+ QString profilePath;
+ if (profileToDelete.get() == nullptr) {
+ profilePath = Settings::instance().paths().profiles()
+ + "/" + ui->profilesList->currentItem()->text();
+ if (QMessageBox::question(this, tr("Profile broken"),
+ tr("This profile you're about to delete seems to be broken or the path is invalid. "
+ "I'm about to delete the following folder: \"%1\". Proceed?").arg(profilePath), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
+ return;
+ }
+ } else {
+ // on destruction, the profile object would write the profile.ini file again, so
+ // we have to get rid of the it before deleting the directory
+ profilePath = profileToDelete->absolutePath();
+ }
+ QListWidgetItem* item = ui->profilesList->takeItem(ui->profilesList->currentRow());
+ if (item != nullptr) {
+ delete item;
+ }
+ if (!shellDelete(QStringList(profilePath))) {
+ log::warn("Failed to shell-delete \"{}\" (errorcode {}), trying regular delete", profilePath, ::GetLastError());
+ if (!removeDir(profilePath)) {
+ log::warn("regular delete failed too");
+ }
+ }
+
+ emit profileRemoved(profileToDelete->name());
+ }
+}
+
+
+void ProfilesDialog::on_renameButton_clicked()
+{
+ Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
+
+ if (currentProfile->name() == m_ActiveProfileName) {
+ QMessageBox::warning(this, tr("Renaming active profile"),
+ tr("The active profile cannot be renamed. Please change to a different profile first."));
+ return;
+ }
+
+ bool valid = false;
+ QString name;
+
+ while (!valid) {
+ bool ok = false;
+ name = QInputDialog::getText(this, tr("Rename Profile"), tr("New Name"),
+ QLineEdit::Normal, currentProfile->name(),
+ &ok);
+ valid = fixDirectoryName(name);
+ if (!ok) {
+ return;
+ }
+ }
+
+ ui->profilesList->currentItem()->setText(name);
+
+ QString oldName = currentProfile->name();
+ currentProfile->rename(name);
+
+ emit profileRenamed(currentProfile.get(), oldName, name);
+}
+
+
+void ProfilesDialog::on_invalidationBox_stateChanged(int state)
+{
+ QListWidgetItem *currentItem = ui->profilesList->currentItem();
+ if (currentItem == nullptr) {
+ return;
+ }
+ if (!ui->invalidationBox->isEnabled()) {
+ return;
+ }
+ try {
+ QVariant currentProfileVariant = currentItem->data(Qt::UserRole);
+ if (!currentProfileVariant.isValid() || currentProfileVariant.isNull()) {
+ return;
+ }
+ const Profile::Ptr currentProfile = currentItem->data(Qt::UserRole).value<Profile::Ptr>();
+ if (state == Qt::Unchecked) {
+ currentProfile->deactivateInvalidation();
+ } else {
+ currentProfile->activateInvalidation();
+ }
+ } catch (const std::exception &e) {
+ reportError(tr("failed to change archive invalidation state: %1").arg(e.what()));
+ }
+}
+
+
+void ProfilesDialog::on_profilesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem*)
+{
+ if (current != nullptr) {
+ if (!current->data(Qt::UserRole).isValid()) return;
+ const Profile::Ptr currentProfile = current->data(Qt::UserRole).value<Profile::Ptr>();
+
+ try {
+ bool invalidationSupported = false;
+ ui->invalidationBox->blockSignals(true);
+ ui->invalidationBox->setChecked(currentProfile->invalidationActive(&invalidationSupported));
+ ui->invalidationBox->setEnabled(invalidationSupported);
+ ui->invalidationBox->blockSignals(false);
+
+ bool localSaves = currentProfile->localSavesEnabled();
+ ui->transferButton->setEnabled(localSaves);
+ // prevent the stateChanged-event for the saves-box from triggering, otherwise it may think local saves
+ // were disabled and delete the files/rename the dir
+ ui->localSavesBox->blockSignals(true);
+ ui->localSavesBox->setChecked(localSaves);
+ ui->localSavesBox->blockSignals(false);
+
+ ui->copyProfileButton->setEnabled(true);
+ ui->removeProfileButton->setEnabled(true);
+ ui->renameButton->setEnabled(true);
+
+ ui->localIniFilesBox->blockSignals(true);
+ ui->localIniFilesBox->setChecked(currentProfile->localSettingsEnabled());
+ ui->localIniFilesBox->blockSignals(false);
+ } catch (const std::exception& E) {
+ reportError(tr("failed to determine if invalidation is active: %1").arg(E.what()));
+ ui->copyProfileButton->setEnabled(false);
+ ui->removeProfileButton->setEnabled(false);
+ ui->renameButton->setEnabled(false);
+ ui->invalidationBox->setChecked(false);
+ }
+ } else {
+ ui->invalidationBox->setChecked(false);
+ ui->copyProfileButton->setEnabled(false);
+ ui->removeProfileButton->setEnabled(false);
+ ui->renameButton->setEnabled(false);
+ }
+}
+
+void ProfilesDialog::on_profilesList_itemActivated(QListWidgetItem* item)
+{
+ on_select_clicked();
+}
+
+void ProfilesDialog::on_localSavesBox_stateChanged(int state)
+{
+ Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
+
+ if (currentProfile->enableLocalSaves(state == Qt::Checked)) {
+ ui->transferButton->setEnabled(state == Qt::Checked);
+ } else {
+ // revert checkbox-state
+ ui->localSavesBox->setChecked(state != Qt::Checked);
+ }
+}
+
+void ProfilesDialog::on_transferButton_clicked()
+{
+ const Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
+ TransferSavesDialog transferDialog(*currentProfile, m_Game, this);
+ transferDialog.exec();
+}
+
+void ProfilesDialog::on_localIniFilesBox_stateChanged(int state)
+{
+ Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
+
+ if (!currentProfile->enableLocalSettings(state == Qt::Checked)) {
+ // revert checkbox-state
+ ui->localIniFilesBox->setChecked(state != Qt::Checked);
+ }
+}
diff --git a/src/profilesdialog.h b/src/profilesdialog.h
index 005bc33e..37fedb67 100644
--- a/src/profilesdialog.h
+++ b/src/profilesdialog.h
@@ -1,132 +1,132 @@
-/*
-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 PROFILESDIALOG_H
-#define PROFILESDIALOG_H
-
-#include "tutorabledialog.h"
-class Profile;
-class OrganizerCore;
-
-class QListWidget;
-class QListWidgetItem;
-#include <QObject>
-class QString;
-
-namespace Ui { class ProfilesDialog; }
-
-namespace MOBase { class IPluginGame; }
-
-
-/**
- * @brief Dialog that can be used to create/delete/modify profiles
- **/
-class ProfilesDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT
-
-public:
-
- /**
- * @brief constructor
- *
- * @param profileName currently enabled profile
- * @param organizer
- * @param parent parent widget
- **/
- explicit ProfilesDialog(const QString &profileName, OrganizerCore &organizer, QWidget *parent = 0);
- ~ProfilesDialog();
-
- // also saves and restores geometry
- //
- int exec() override;
-
- /**
- * @return true if creation of a new profile failed
- * @todo the notion of a fail state makes little sense in the current dialog
- **/
- bool failed() const { return m_FailState; }
-
- // if the dialog was closed with the 'select' button, returns the name of the
- // selected profile; if the dialog was closed with 'cancel', returns empty
- //
- std::optional<QString> selectedProfile() const;
-
-signals:
-
- /**
- * @brief Signal emitted when a profile is created.
- */
- void profileCreated(Profile* profile);
-
- /**
- * @brief Signal emitted when a profile is renamed.
- */
- void profileRenamed(Profile* profile, QString const& oldName, QString const& newName);
-
- /**
- * @brief Signal emitted when a profile has been removed.
- */
- void profileRemoved(QString const& profileName);
-
-protected:
-
- virtual void showEvent(QShowEvent *event);
-
-private slots:
- void on_localIniFilesBox_stateChanged(int state);
-
-private:
-
- QListWidgetItem *addItem(const QString &name);
- void createProfile(const QString &name, bool useDefaultSettings);
- void createProfile(const QString &name, const Profile &reference);
-
-private slots:
-
- void on_close_clicked();
- void on_select_clicked();
-
- void on_addProfileButton_clicked();
-
- void on_invalidationBox_stateChanged(int arg1);
-
- void on_copyProfileButton_clicked();
-
- void on_profilesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_profilesList_itemActivated(QListWidgetItem* item);
-
- void on_removeProfileButton_clicked();
-
- void on_localSavesBox_stateChanged(int arg1);
-
- void on_transferButton_clicked();
-
- void on_renameButton_clicked();
-
-private:
- Ui::ProfilesDialog *ui;
- QListWidget *m_ProfilesList;
- bool m_FailState;
- MOBase::IPluginGame const *m_Game;
- QString m_ActiveProfileName;
- std::optional<QString> m_Selected;
-};
-
-#endif // PROFILESDIALOG_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 PROFILESDIALOG_H
+#define PROFILESDIALOG_H
+
+#include "tutorabledialog.h"
+class Profile;
+class OrganizerCore;
+
+class QListWidget;
+class QListWidgetItem;
+#include <QObject>
+class QString;
+
+namespace Ui { class ProfilesDialog; }
+
+namespace MOBase { class IPluginGame; }
+
+
+/**
+ * @brief Dialog that can be used to create/delete/modify profiles
+ **/
+class ProfilesDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
+
+public:
+
+ /**
+ * @brief constructor
+ *
+ * @param profileName currently enabled profile
+ * @param organizer
+ * @param parent parent widget
+ **/
+ explicit ProfilesDialog(const QString &profileName, OrganizerCore &organizer, QWidget *parent = 0);
+ ~ProfilesDialog();
+
+ // also saves and restores geometry
+ //
+ int exec() override;
+
+ /**
+ * @return true if creation of a new profile failed
+ * @todo the notion of a fail state makes little sense in the current dialog
+ **/
+ bool failed() const { return m_FailState; }
+
+ // if the dialog was closed with the 'select' button, returns the name of the
+ // selected profile; if the dialog was closed with 'cancel', returns empty
+ //
+ std::optional<QString> selectedProfile() const;
+
+signals:
+
+ /**
+ * @brief Signal emitted when a profile is created.
+ */
+ void profileCreated(Profile* profile);
+
+ /**
+ * @brief Signal emitted when a profile is renamed.
+ */
+ void profileRenamed(Profile* profile, QString const& oldName, QString const& newName);
+
+ /**
+ * @brief Signal emitted when a profile has been removed.
+ */
+ void profileRemoved(QString const& profileName);
+
+protected:
+
+ virtual void showEvent(QShowEvent *event);
+
+private slots:
+ void on_localIniFilesBox_stateChanged(int state);
+
+private:
+
+ QListWidgetItem *addItem(const QString &name);
+ void createProfile(const QString &name, bool useDefaultSettings);
+ void createProfile(const QString &name, const Profile &reference);
+
+private slots:
+
+ void on_close_clicked();
+ void on_select_clicked();
+
+ void on_addProfileButton_clicked();
+
+ void on_invalidationBox_stateChanged(int arg1);
+
+ void on_copyProfileButton_clicked();
+
+ void on_profilesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
+ void on_profilesList_itemActivated(QListWidgetItem* item);
+
+ void on_removeProfileButton_clicked();
+
+ void on_localSavesBox_stateChanged(int arg1);
+
+ void on_transferButton_clicked();
+
+ void on_renameButton_clicked();
+
+private:
+ Ui::ProfilesDialog *ui;
+ QListWidget *m_ProfilesList;
+ bool m_FailState;
+ MOBase::IPluginGame const *m_Game;
+ QString m_ActiveProfileName;
+ std::optional<QString> m_Selected;
+};
+
+#endif // PROFILESDIALOG_H
diff --git a/src/qtgroupingproxy.cpp b/src/qtgroupingproxy.cpp
index d00bd288..bfbf955c 100644
--- a/src/qtgroupingproxy.cpp
+++ b/src/qtgroupingproxy.cpp
@@ -1,1038 +1,1038 @@
-/****************************************************************************************
- * Copyright (c) 2007-2011 Bart Cerneels <bart.cerneels@kde.org> *
- * *
- * This program is free software; you can redistribute it and/or modify it under *
- * the terms of the GNU General Public License as published by the Free Software *
- * Foundation; either version 2 of the License, or (at your option) any later *
- * version. *
- * *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
- * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License along with *
- * this program. If not, see <http://www.gnu.org/licenses/>. *
- ****************************************************************************************/
-
-// Modifications 2013-03-27 to 2013-03-29 by Sebastian Herbord
-
-
-#include "qtgroupingproxy.h"
-#include <log.h>
-
-#include <QDebug>
-#include <QIcon>
-#include <QInputDialog>
-
-using namespace MOBase;
-
-/*!
- \class QtGroupingProxy
- \brief The QtGroupingProxy class will group source model rows by adding a new top tree-level.
- The source model can be flat or tree organized, but only the original top level rows are used
- for determining the grouping.
- \ingroup model-view
-*/
-
-QtGroupingProxy::QtGroupingProxy(QModelIndex rootNode, int groupedColumn, int groupedRole, unsigned int flags, int aggregateRole)
- : QAbstractProxyModel()
- , m_rootNode(rootNode)
- , m_groupedColumn(0)
- , m_groupedRole(groupedRole)
- , m_aggregateRole(aggregateRole)
- , m_flags(flags)
-{
- if (groupedColumn != -1) {
- setGroupedColumn(groupedColumn);
- }
-}
-
-QtGroupingProxy::~QtGroupingProxy()
-{
-}
-
-void QtGroupingProxy::setSourceModel(QAbstractItemModel* model)
-{
- if (sourceModel()) {
- disconnect(sourceModel(), nullptr, this, nullptr);
- }
-
- QAbstractProxyModel::setSourceModel(model);
-
- if (sourceModel()) {
- // signal proxies
- connect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex&, int, int)),
- SLOT(modelRowsInserted(const QModelIndex&, int, int)));
- connect(sourceModel(), SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)),
- SLOT(modelRowsAboutToBeInserted(const QModelIndex&, int, int)));
- connect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
- SLOT(modelRowsRemoved(const QModelIndex&, int, int)));
- connect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)),
- SLOT(modelRowsAboutToBeRemoved(QModelIndex, int, int)));
- connect(sourceModel(), SIGNAL(layoutChanged()), SLOT(buildTree()));
- connect(sourceModel(), SIGNAL(dataChanged(QModelIndex, QModelIndex)),
- SLOT(modelDataChanged(QModelIndex, QModelIndex)));
- connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(resetModel()));
-
- buildTree();
- }
-}
-
-void
-QtGroupingProxy::setGroupedColumn( int groupedColumn )
-{
- m_groupedColumn = groupedColumn;
- buildTree();
-}
-
-/** Maps to what groups the source row belongs by returning the data of those groups.
- *
- * @returns a list of data for the rows the argument belongs to. In common cases this list will
- * contain only one entry. An empty list means that the source item will be placed in the root of
- * this proxyModel. There is no support for hiding source items.
- *
- * Group data can be pre-loaded in the return value so it's added to the cache maintained by this
- * class. This is required if you want to have data that is not present in the source model.
- */
-QList<RowData>
-QtGroupingProxy::belongsTo( const QModelIndex &idx )
-{
- QList<RowData> rowDataList;
-
- //get all the data for this index from the model
- ItemData itemData = sourceModel()->itemData( idx );
- if (m_groupedRole != Qt::DisplayRole) {
- itemData[Qt::DisplayRole] = itemData[m_groupedRole];
- }
-
- // invalid value in grouped role -> ungrouped
- if (!itemData[Qt::DisplayRole].isValid()) {
- return rowDataList;
- }
-
- QMapIterator<int, QVariant> i( itemData );
- while( i.hasNext() )
- {
- i.next();
- int role = i.key();
- QVariant variant = i.value();
-
- if ( variant.type() == QVariant::List )
- {
- //a list of variants get's expanded to multiple rows
- QVariantList list = variant.toList();
- for( int i = 0; i < list.length(); i++ )
- {
- //take an existing row data or create a new one
- RowData rowData = (rowDataList.count() > i) ? rowDataList.takeAt( i )
- : RowData();
-
- //we only gather data for the first column
- ItemData indexData = rowData.contains( 0 ) ? rowData.take( 0 ) : ItemData();
- indexData.insert( role, list.value( i ) );
- rowData.insert( 0, indexData );
- //for the grouped column the data should not be gathered from the children
- //this will allow filtering on the content of this column with a
- //QSortFilterProxyModel
- rowData.insert( m_groupedColumn, indexData );
- rowDataList.insert( i, rowData );
- }
- break;
- }
- else if( !variant.isNull() )
- {
- //it's just a normal item. Copy all the data and break this loop.
- RowData rowData;
- rowData.insert( 0, itemData );
- rowDataList << rowData;
- break;
- }
- }
-
- return rowDataList;
-}
-
-/* m_groupMap layout
-* key : index of the group in m_groupMaps
-* value : a QList of the original rows in sourceModel() for the children of this group
-*
-* key = -1 contains a QList of the non-grouped indexes
-*
-* TODO: sub-groups
-*/
-void
-QtGroupingProxy::buildTree()
-{
- if( !sourceModel() )
- return;
- beginResetModel();
-
- m_groupMap.clear();
- //don't clear the data maps since most of it will probably be needed again.
- m_parentCreateList.clear();
-
- int max = sourceModel()->rowCount( m_rootNode );
-
- //WARNING: these have to be added in order because the addToGroups function is optimized for
- //modelRowsInserted(). Failure to do so will result in wrong data shown in the view at best.
- for( int row = 0; row < max; row++ )
- {
- QModelIndex idx = sourceModel()->index( row, m_groupedColumn, m_rootNode );
- addSourceRow( idx );
- }
- //dumpGroups();
-
- if (m_flags & FLAG_NOSINGLE) {
- // awkward: flatten single-item groups as a post-processing steps.
-
- int currentKey = 0;
- quint32 quint32max = std::numeric_limits<quint32>::max();
- std::vector<int> rmgroups;
-
- QMap<quint32, QList<int> > temp;
-
- for (auto iter = m_groupMap.begin(); iter != m_groupMap.end(); ++iter) {
- if ((iter.key() == quint32max) ||
- (iter->count() < 2)) {
- temp[quint32max].append(iter.value());
- if (iter.key() != quint32max) {
- rmgroups.push_back(iter.key());
- }
- } else {
- temp[currentKey++] = *iter;
- }
- }
- m_groupMap = temp;
-
- // second loop is necessary because qt containers can't be iterated from end to front
- // and removing by index from begin to end is ugly
- std::sort(rmgroups.begin(), rmgroups.end(), [] (int lhs, int rhs) { return rhs < lhs; });
- for (auto iter = rmgroups.begin(); iter != rmgroups.end(); ++iter) {
- m_groupMaps.removeAt(*iter);
- }
- }
-
- endResetModel();
-}
-
-QList<int>
-QtGroupingProxy::addSourceRow( const QModelIndex &idx )
-{
- QList<int> updatedGroups;
- QList<RowData> groupData = belongsTo( idx );
-
- //an empty list here means it's supposed to go in root.
- if( groupData.isEmpty() )
- {
- updatedGroups << -1;
- if( !m_groupMap.keys().contains( std::numeric_limits<quint32>::max() ) )
- m_groupMap.insert( std::numeric_limits<quint32>::max(), QList<int>() ); //add an empty placeholder
- }
-
- //an item can be in multiple groups
- foreach( RowData data, groupData )
- {
- int updatedGroup = -1;
- if( !data.isEmpty() )
- {
- foreach( const RowData &cachedData, m_groupMaps )
- {
- //when this matches the index belongs to an existing group
- if( data[0][Qt::DisplayRole] == cachedData[0][Qt::DisplayRole] )
- {
- data = cachedData;
- break;
- }
- }
-
- updatedGroup = m_groupMaps.indexOf( data );
- //-1 means not found
- if( updatedGroup == -1 )
- {
- //new groups are added to the end of the existing list
- m_groupMaps << data;
- updatedGroup = m_groupMaps.count() - 1;
- }
-
- if( !m_groupMap.keys().contains( updatedGroup ) )
- m_groupMap.insert( updatedGroup, QList<int>() ); //add an empty placeholder
- }
-
- if( !updatedGroups.contains( updatedGroup ) )
- updatedGroups << updatedGroup;
- }
-
- //update m_groupMap to the new source-model layout (one row added)
- QMutableMapIterator<quint32, QList<int> > i( m_groupMap );
- while( i.hasNext() )
- {
- i.next();
- QList<int> &groupList = i.value();
- int insertedProxyRow = groupList.count();
- for( ; insertedProxyRow > 0 ; insertedProxyRow-- )
- {
- int &rowValue = groupList[insertedProxyRow-1];
- if( idx.row() <= rowValue )
- {
- //increment the rows that come after the new row since they moved one place up.
- rowValue++;
- }
- else
- {
- break;
- }
- }
-
- if( updatedGroups.contains( i.key() ) )
- {
- //the row needs to be added to this group
- groupList.insert( insertedProxyRow, idx.row() );
- }
- }
-
- return updatedGroups;
-}
-
-/** Each ModelIndex has in it's internalId a position in the parentCreateList.
- * struct ParentCreate are the instructions to recreate the parent index.
- * It contains the proxy row number of the parent and the postion in this list of the grandfather.
- * This function creates the ParentCreate structs and saves them in a list.
- */
-int
-QtGroupingProxy::indexOfParentCreate( const QModelIndex &parent ) const
-{
- if( !parent.isValid() )
- return -1;
-
- struct ParentCreate pc;
- for( int i = 0 ; i < m_parentCreateList.size() ; i++ )
- {
- pc = m_parentCreateList[i];
- if( pc.parentCreateIndex == parent.internalId() && pc.row == parent.row() )
- return i;
- }
- //there is no parentCreate yet for this index, so let's create one.
- pc.parentCreateIndex = parent.internalId();
- pc.row = parent.row();
- m_parentCreateList << pc;
-
- return m_parentCreateList.size() - 1;
-}
-
-QModelIndex
-QtGroupingProxy::index( int row, int column, const QModelIndex &parent ) const
-{
- if( !hasIndex(row, column, parent) ) {
- return QModelIndex();
- }
-
- if( parent.column() > 0 ) {
- return QModelIndex();
- }
-
- /* We save the instructions to make the parent of the index in a struct.
- * The place of the struct in the list is stored in the internalId
- */
- int parentCreateIndex = indexOfParentCreate( parent );
-
- return createIndex( row, column, parentCreateIndex );
-}
-
-QModelIndex
-QtGroupingProxy::parent( const QModelIndex &index ) const
-{
- if( !index.isValid() )
- return QModelIndex();
-
- int parentCreateIndex = index.internalId();
- if( parentCreateIndex == -1 || parentCreateIndex >= m_parentCreateList.count() )
- return QModelIndex();
-
- struct ParentCreate pc = m_parentCreateList[parentCreateIndex];
-
- //only items at column 0 have children
- return createIndex( pc.row, 0, pc.parentCreateIndex );
-}
-
-int
-QtGroupingProxy::rowCount( const QModelIndex &index ) const
-{
- if( !index.isValid() )
- {
- //the number of top level groups + the number of non-grouped items
- int rows = m_groupMaps.count() + m_groupMap.value( std::numeric_limits<quint32>::max() ).count();
- return rows;
- }
-
- //TODO:group in group support.
- if( isGroup( index ) )
- {
- qint64 groupIndex = index.row();
- int rows = m_groupMap.value( groupIndex ).count();
- return rows;
- } else {
- QModelIndex originalIndex = mapToSource( index );
- int rowCount = sourceModel()->rowCount( originalIndex );
- return rowCount;
- }
-}
-
-int
-QtGroupingProxy::columnCount( const QModelIndex &index ) const
-{
- if( !index.isValid() )
- return sourceModel()->columnCount( m_rootNode );
-
- if( index.column() != 0 )
- return 0;
-
- return sourceModel()->columnCount( mapToSource( index ) );
-}
-
-
-static bool variantLess(const QVariant &LHS, const QVariant &RHS)
-{
- if ((LHS.type() == RHS.type()) &&
- ((LHS.type() == QVariant::Int) || (LHS.type() == QVariant::UInt))) {
- return LHS.toInt() < RHS.toInt();
- }
-
- // this should always work (comparing empty strings in the worst case) but
- // the results may be wrong
- return LHS.toString() < RHS.toString();
-}
-
-
-static QVariant variantMax(const QVariantList &variants)
-{
- QVariant result = variants.first();
- foreach (const QVariant &iter, variants) {
- if (variantLess(result, iter)) {
- result = iter;
- }
- }
- return result;
-}
-
-
-static QVariant variantMin(const QVariantList &variants)
-{
- QVariant result = variants.first();
- foreach (const QVariant &iter, variants) {
- if (variantLess(iter, result)) {
- result = iter;
- }
- }
- return result;
-}
-
-
-QVariant
-QtGroupingProxy::data( const QModelIndex &index, int role ) const
-{
- if( !index.isValid() )
- return QVariant();
-
- int row = index.row();
- int column = index.column();
- if( isGroup( index ) )
- {
- if ((role != Qt::DisplayRole) && (role != Qt::EditRole)) {
- switch (role) {
- case Qt::ForegroundRole: {
- return QBrush(Qt::gray);
- } break;
- case Qt::FontRole: {
- QFont font(m_groupMaps[row][column].value(Qt::FontRole).value<QFont>());
- font.setItalic(true);
- return font;
- } break;
- case Qt::TextAlignmentRole: {
- return Qt::AlignHCenter;
- } break;
- case Qt::UserRole: {
- return m_groupMaps[row][column].value( Qt::DisplayRole ).toString();
- } break;
- case Qt::CheckStateRole: {
- if (column != 0) return QVariant();
- int childCount = m_groupMap.value( row ).count();
- int checked = 0;
- QModelIndex parentIndex = this->index( row, 0, index.parent() );
- for( int childRow = 0; childRow < childCount; ++childRow )
- {
- QModelIndex childIndex = this->index( childRow, 0, parentIndex );
- QVariant data = mapToSource( childIndex ).data( Qt::CheckStateRole );
- if (data.toInt() == 2) ++checked;
- }
- if (checked == childCount) return Qt::Checked;
- else if (checked == 0) return Qt::Unchecked;
- else return Qt::PartiallyChecked;
- } break;
- default: {
- QModelIndex parentIndex = this->index( row, 0, index.parent() );
- if (m_groupMap.value( row ).count() > 0) {
- return this->index(0, column, parentIndex).data(role);
- } else {
- return QVariant();
- }
- // return m_groupMaps[row][column].value( role );
- } break;
- }
- }
-
- //use cached or precalculated data
- if( m_groupMaps[row][column].contains( Qt::DisplayRole ) )
- {
- if ((m_flags & FLAG_NOGROUPNAME) != 0) {
- QModelIndex parentIndex = this->index( row, 0, index.parent() );
- QModelIndex childIndex = this->index( 0, column, parentIndex );
- return childIndex.data(role).toString();
- } else {
- return m_groupMaps[row][column].value( role ).toString();
- }
- }
-
- //for column 0 we gather data from the grouped column instead
- if( column == 0 )
- column = m_groupedColumn;
-
- //map all data from children to columns of group to allow grouping one level up
- QVariantList variantsOfChildren;
- int childCount = m_groupMap.value( row ).count();
- if( childCount == 0 )
- return QVariant();
-
- int function = AGGR_NONE;
- if (m_aggregateRole >= Qt::UserRole) {
- QModelIndex parentIndex = this->index( row, 0, index.parent() );
- QModelIndex childIndex = this->index( 0, column, parentIndex );
- function = mapToSource(childIndex).data(m_aggregateRole).toInt();
- }
-
- //Need a parentIndex with column == 0 because only those have children.
- QModelIndex parentIndex = this->index( row, 0, index.parent() );
- for( int childRow = 0; childRow < childCount; childRow++ )
- {
- QModelIndex childIndex = this->index( childRow, column, parentIndex );
- QVariant data = mapToSource( childIndex ).data( role );
-
- if( data.isValid() && !variantsOfChildren.contains( data ) )
- variantsOfChildren << data;
- }
-
- //saving in cache
- ItemData roleMap = m_groupMaps[row].value( column );
- foreach( const QVariant &variant, variantsOfChildren )
- {
- if( roleMap[ role ] != variant ) {
- roleMap.insert( role, variantsOfChildren );
- }
- }
-
- if( variantsOfChildren.count() == 0 )
- return QVariant();
-
- //only one unique variant? No need to return a list
- switch (function) {
- case AGGR_EMPTY: return QVariant();
- case AGGR_FIRST: return variantsOfChildren.first();
- case AGGR_MAX: return variantMax(variantsOfChildren);
- case AGGR_MIN: return variantMin(variantsOfChildren);
- default: {
- if( variantsOfChildren.count() == 1 )
- return variantsOfChildren.first();
-
- return variantsOfChildren;
- } break;
- }
- }
-
- return mapToSource( index ).data( role );
-}
-
-bool
-QtGroupingProxy::setData( const QModelIndex &idx, const QVariant &value, int role )
-{
- if( !idx.isValid() )
- return false;
-
- //no need to set data to exactly the same value
- if( idx.data( role ) == value )
- return false;
-
- if( isGroup( idx ) )
- {
- ItemData columnData = m_groupMaps[idx.row()][idx.column()];
-
- columnData.insert( role, value );
- //QItemDelegate will always use Qt::EditRole
- if( role == Qt::EditRole )
- columnData.insert( Qt::DisplayRole, value );
-
- //and make sure it's stored in the map
- m_groupMaps[idx.row()].insert( idx.column(), columnData );
-
- int columnToChange = idx.column() ? idx.column() : m_groupedColumn;
- foreach( int originalRow, m_groupMap.value( idx.row() ) )
- {
- QModelIndex childIdx = sourceModel()->index( originalRow, columnToChange,
- m_rootNode );
- if( childIdx.isValid() )
- sourceModel()->setData( childIdx, value, role );
- }
- //TODO: we might need to reload the data from the children at this point
-
- emit dataChanged( idx, idx );
- return true;
- }
-
- return sourceModel()->setData( mapToSource( idx ), value, role );
-}
-
-bool
-QtGroupingProxy::isGroup( const QModelIndex &index ) const
-{
- int parentCreateIndex = index.internalId();
- if( parentCreateIndex == -1 && index.row() < m_groupMaps.count() )
- return true;
- return false;
-}
-
-QModelIndex
-QtGroupingProxy::mapToSource( const QModelIndex &index ) const
-{
- if( !index.isValid() ) {
- return m_rootNode;
- }
-
- if( isGroup( index ) )
- {
- return m_rootNode;
- }
-
- QModelIndex proxyParent = index.parent();
- QModelIndex originalParent = mapToSource( proxyParent );
-
- int originalRow = index.row();
- if( originalParent == m_rootNode )
- {
- int indexInGroup = index.row();
- if( !proxyParent.isValid() )
- indexInGroup -= m_groupMaps.count();
-
- QList<int> childRows = m_groupMap.value( proxyParent.row() );
- if( childRows.isEmpty() || indexInGroup >= childRows.count() || indexInGroup < 0 )
- return QModelIndex();
-
- originalRow = childRows.at( indexInGroup );
- }
- return sourceModel()->index( originalRow, index.column(), originalParent );
-}
-
-QModelIndexList
-QtGroupingProxy::mapToSource( const QModelIndexList& list ) const
-{
- QModelIndexList originalList;
- foreach( const QModelIndex &index, list )
- {
- QModelIndex originalIndex = mapToSource( index );
- if( originalIndex.isValid() )
- originalList << originalIndex;
- }
- return originalList;
-}
-
-QModelIndex
-QtGroupingProxy::mapFromSource( const QModelIndex &idx ) const
-{
- if( !idx.isValid() )
- return QModelIndex();
-
- QModelIndex proxyParent;
- QModelIndex sourceParent = idx.parent();
-
- int proxyRow = idx.row();
- int sourceRow = idx.row();
-
- if( sourceParent.isValid() && ( sourceParent != m_rootNode ) )
- {
- //idx is a child of one of the items in the source model
- proxyParent = mapFromSource( sourceParent );
- }
- else
- {
- //idx is an item in the top level of the source model (child of the rootnode)
- int groupRow = -1;
- QMapIterator<quint32, QList<int> > iterator( m_groupMap );
- while( iterator.hasNext() )
- {
- iterator.next();
- if( iterator.value().contains( sourceRow ) )
- {
- groupRow = iterator.key();
- break;
- }
- }
-
- if( groupRow != -1 ) //it's in a group, let's find the correct row.
- {
- proxyParent = this->index( groupRow, 0, QModelIndex() );
- proxyRow = m_groupMap.value( groupRow ).indexOf( sourceRow );
- }
- else
- {
- proxyParent = QModelIndex();
- // if the proxy item is not in a group it will be below the groups.
- int groupLength = m_groupMaps.count();
- int i = m_groupMap.value( std::numeric_limits<quint32>::max() ).indexOf( sourceRow );
-
- proxyRow = groupLength + i;
- }
- }
-
- return this->index( proxyRow, idx.column(), proxyParent );
-}
-
-Qt::ItemFlags
-QtGroupingProxy::flags( const QModelIndex &idx ) const
-{
- if( !idx.isValid() )
- {
- Qt::ItemFlags rootFlags = sourceModel()->flags( m_rootNode );
- if( rootFlags.testFlag( Qt::ItemIsDropEnabled ) )
- return Qt::ItemFlags( Qt::ItemIsDropEnabled );
-
- return Qt::ItemFlags(0);
- }
-
- //only if the grouped column has the editable flag set allow the
- //actions leading to setData on the source (edit & drop)
- if( isGroup( idx ) )
- {
- // dumpGroups();
- Qt::ItemFlags defaultFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable );
- //Qt::ItemFlags defaultFlags(Qt::ItemIsEnabled);
- bool groupIsEditable = true;
-
- if (idx.column() == 0) {
- bool checkable = true;
- foreach ( int originalRow, m_groupMap.value( idx.row() ) )
- {
- QModelIndex originalIdx = sourceModel()->index( originalRow, 0,
- m_rootNode.parent() );
- if ( (originalIdx.flags() & Qt::ItemIsUserCheckable) == 0 )
- {
- checkable = false;
- }
- }
-
- if ( checkable ) {
- defaultFlags |= Qt::ItemIsUserCheckable;
- }
- }
-
- //it's possible to have empty groups
- if( m_groupMap.value( idx.row() ).count() == 0 )
- {
- //check the flags of this column with the root node
- QModelIndex originalRootNode = sourceModel()->index( m_rootNode.row(), m_groupedColumn,
- m_rootNode.parent() );
- groupIsEditable = originalRootNode.flags().testFlag( Qt::ItemIsEditable );
- }
- else
- {
- foreach( int originalRow, m_groupMap.value( idx.row() ) )
- {
- QModelIndex originalIdx = sourceModel()->index( originalRow, m_groupedColumn,
- m_rootNode );
-
- groupIsEditable = groupIsEditable
- ? originalIdx.flags().testFlag( Qt::ItemIsEditable )
- : false;
- if( !groupIsEditable ) //all children need to have an editable grouped column
- break;
- }
- }
- if( groupIsEditable )
- return ( defaultFlags | Qt::ItemIsEditable | Qt::ItemIsDropEnabled );
- return defaultFlags;
- }
-
- QModelIndex originalIdx = mapToSource( idx );
- Qt::ItemFlags originalItemFlags = sourceModel()->flags( originalIdx );
-
- //check the source model to see if the grouped column is editable;
- QModelIndex groupedColumnIndex =
- sourceModel()->index( originalIdx.row(), m_groupedColumn, originalIdx.parent() );
- bool groupIsEditable = sourceModel()->flags( groupedColumnIndex ).testFlag( Qt::ItemIsEditable );
-
- if( groupIsEditable )
- return originalItemFlags | Qt::ItemIsDragEnabled;
- return originalItemFlags;
-}
-
-QVariant
-QtGroupingProxy::headerData( int section, Qt::Orientation orientation, int role ) const
-{
- return sourceModel()->headerData( section, orientation, role );
-}
-
-bool
-QtGroupingProxy::canFetchMore( const QModelIndex &parent ) const
-{
- if( !parent.isValid() )
- return false;
-
- if( isGroup( parent ) )
- return false;
-
- return sourceModel()->canFetchMore( mapToSource( parent ) );
-}
-
-void
-QtGroupingProxy::fetchMore ( const QModelIndex & parent )
-{
- if( !parent.isValid() )
- return;
-
- if( isGroup( parent ) )
- return;
-
- return sourceModel()->fetchMore( mapToSource( parent ) );
-}
-
-QModelIndex
-QtGroupingProxy::addEmptyGroup( const RowData &data )
-{
- int newRow = m_groupMaps.count();
- beginInsertRows( QModelIndex(), newRow, newRow );
- m_groupMaps << data;
- endInsertRows();
- return index( newRow, 0, QModelIndex() );
-}
-
-bool
-QtGroupingProxy::removeGroup( const QModelIndex &idx )
-{
- beginRemoveRows( idx.parent(), idx.row(), idx.row() );
- m_groupMap.remove( idx.row() );
- m_groupMaps.removeAt( idx.row() );
- m_parentCreateList.removeAt( idx.internalId() );
- endRemoveRows();
-
- //TODO: only true if all data could be unset.
- return true;
-}
-
-bool
-QtGroupingProxy::hasChildren( const QModelIndex &parent ) const
-{
- if( !parent.isValid() ) {
- return true;
- }
-
- if( isGroup( parent ) ) {
- return !m_groupMap.value( parent.row() ).isEmpty();
- }
-
- return sourceModel()->hasChildren( mapToSource( parent ) );
-}
-
-bool
-QtGroupingProxy::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
-{
- QModelIndex idx = index(row, column, parent);
- if (isGroup(idx)) {
- QList<int> childRows = m_groupMap.value(idx.row());
- int max = *std::max_element(childRows.begin(), childRows.end());
-
- QModelIndex newIdx = mapToSource(index(max, column, idx));
- return sourceModel()->dropMimeData(data, action, max, column, newIdx);
- } else {
- if (row == -1) {
- return sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
- } else {
- QModelIndex idx = mapToSource(index(row, column, parent));
- return sourceModel()->dropMimeData(data, action, idx.row(), idx.column(), idx.parent());
- }
- }
-}
-
-void
-QtGroupingProxy::modelRowsAboutToBeInserted( const QModelIndex &parent, int start, int end )
-{
- if( parent != m_rootNode )
- {
- //an item will be added to an original index, remap and pass it on
- QModelIndex proxyParent = mapFromSource( parent );
- beginInsertRows( proxyParent, start, end );
- }
-}
-
-void
-QtGroupingProxy::modelRowsInserted( const QModelIndex &parent, int start, int end )
-{
- if( parent == m_rootNode )
- {
- //top level of the model changed, these new rows need to be put in groups
- for( int modelRow = start; modelRow <= end ; modelRow++ )
- {
- addSourceRow( sourceModel()->index( modelRow, m_groupedColumn, m_rootNode ) );
- }
- }
- else
- {
- //an item was added to an original index, remap and pass it on
- QModelIndex proxyParent = mapFromSource( parent );
-
- QString s;
- QDebug debug(&s);
- debug << proxyParent;
- log::debug("{}", s);
-
- //beginInsertRows had to be called in modelRowsAboutToBeInserted()
- endInsertRows();
- }
-}
-
-void
-QtGroupingProxy::modelRowsAboutToBeRemoved( const QModelIndex &parent, int start, int end )
-{
- if( parent == m_rootNode )
- {
- QMap<quint32, QList<int> >::const_iterator i;
- //HACK, we are going to call beginRemoveRows() multiple times without
- // endRemoveRows() if a source index is in multiple groups.
- // This can be a problem for some views/proxies, but Q*Views can handle it.
- // TODO: investigate a queue for applying proxy model changes in the correct order
- for( i = m_groupMap.constBegin(); i != m_groupMap.constEnd(); ++i )
- {
- int groupIndex = i.key();
- const QList<int> &groupList = i.value();
- QModelIndex proxyParent = index( groupIndex, 0 );
- foreach( int originalRow, groupList )
- {
- if( originalRow >= start && originalRow <= end )
- {
- int proxyRow = groupList.indexOf( originalRow );
- if( groupIndex == -1 ) //adjust for non-grouped (root level) original items
- proxyRow += m_groupMaps.count();
- //TODO: optimize for continues original rows in the same group
- beginRemoveRows( proxyParent, proxyRow, proxyRow );
- }
- }
- }
- }
- else
- {
- //child item(s) of an original item will be removed, remap and pass it on
- QModelIndex proxyParent = mapFromSource( parent );
- beginRemoveRows( proxyParent, start, end );
- }
-}
-
-void
-QtGroupingProxy::modelRowsRemoved( const QModelIndex &parent, int start, int end )
-{
- if( parent == m_rootNode )
- {
- //TODO: can be optimised by iterating over m_groupMap and checking start <= r < end
-
- //rather than increasing i we change the stored sourceRows in-place and reuse argument start
- //X-times (where X = end - start).
- for( int i = start; i <= end; i++ )
- {
- //HACK: we are going to iterate the hash in reverse so calls to endRemoveRows()
- // are matched up with the beginRemoveRows() in modelRowsAboutToBeRemoved()
- //NOTE: easier to do reverse with java style iterator
- QMutableMapIterator<quint32, QList<int> > iter( m_groupMap );
- iter.toBack();
- while( iter.hasPrevious() )
- {
- iter.previous();
- int groupIndex = iter.key();
- //has to be a modifiable reference for remove and replace operations
- QList<int> &groupList = iter.value();
- int rowIndex = groupList.indexOf( start );
- if( rowIndex != -1 )
- {
- QModelIndex proxyParent = index( groupIndex, 0 );
- groupList.removeAt( rowIndex );
- }
- //Now decrement all source rows that are after the removed row
- for( int j = 0; j < groupList.count(); j++ )
- {
- int sourceRow = groupList.at( j );
- if( sourceRow > start )
- groupList.replace( j, sourceRow-1 );
- }
- if( rowIndex != -1)
- endRemoveRows(); //end remove operation only after group was updated.
- }
- }
-
- return;
- }
-
- //beginRemoveRows had to be called in modelRowsAboutToBeRemoved();
- endRemoveRows();
-}
-
-void
-QtGroupingProxy::resetModel()
-{
- buildTree();
-}
-
-void
-QtGroupingProxy::modelDataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight )
-{
- //TODO: need to look in the groupedColumn and see if it changed and changed grouping accordingly
- QModelIndex proxyTopLeft = mapFromSource( topLeft );
- if( !proxyTopLeft.isValid() )
- return;
-
- if( topLeft == bottomRight )
- {
- emit dataChanged( proxyTopLeft, proxyTopLeft );
- }
- else
- {
- QModelIndex proxyBottomRight = mapFromSource( bottomRight );
- emit dataChanged( proxyTopLeft, proxyBottomRight );
- }
-}
-
-bool
-QtGroupingProxy::isAGroupSelected( const QModelIndexList& list ) const
-{
- foreach( const QModelIndex &index, list )
- {
- if( isGroup( index ) )
- return true;
- }
- return false;
-}
-
-void
-QtGroupingProxy::dumpGroups() const
-{
- QString s;
- QDebug debug(&s);
-
- debug << "m_groupMap:\n";
- for( int groupIndex = -1; groupIndex < m_groupMap.keys().count() - 1; groupIndex++ )
- {
- debug << groupIndex << " : " << m_groupMap.value( groupIndex ) << "\n";
- }
-
- debug << "m_groupMaps:\n";
- for( int groupIndex = 0; groupIndex < m_groupMaps.count(); groupIndex++ )
- {
- debug << m_groupMaps[groupIndex] << ": " << m_groupMap.value( groupIndex ) << "\n";
- }
-
- debug << m_groupMap.value( std::numeric_limits<quint32>::max() );
-
- log::debug("{}", s);
-}
+/****************************************************************************************
+ * Copyright (c) 2007-2011 Bart Cerneels <bart.cerneels@kde.org> *
+ * *
+ * This program is free software; you can redistribute it and/or modify it under *
+ * the terms of the GNU General Public License as published by the Free Software *
+ * Foundation; either version 2 of the License, or (at your option) any later *
+ * version. *
+ * *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License along with *
+ * this program. If not, see <http://www.gnu.org/licenses/>. *
+ ****************************************************************************************/
+
+// Modifications 2013-03-27 to 2013-03-29 by Sebastian Herbord
+
+
+#include "qtgroupingproxy.h"
+#include <log.h>
+
+#include <QDebug>
+#include <QIcon>
+#include <QInputDialog>
+
+using namespace MOBase;
+
+/*!
+ \class QtGroupingProxy
+ \brief The QtGroupingProxy class will group source model rows by adding a new top tree-level.
+ The source model can be flat or tree organized, but only the original top level rows are used
+ for determining the grouping.
+ \ingroup model-view
+*/
+
+QtGroupingProxy::QtGroupingProxy(QModelIndex rootNode, int groupedColumn, int groupedRole, unsigned int flags, int aggregateRole)
+ : QAbstractProxyModel()
+ , m_rootNode(rootNode)
+ , m_groupedColumn(0)
+ , m_groupedRole(groupedRole)
+ , m_aggregateRole(aggregateRole)
+ , m_flags(flags)
+{
+ if (groupedColumn != -1) {
+ setGroupedColumn(groupedColumn);
+ }
+}
+
+QtGroupingProxy::~QtGroupingProxy()
+{
+}
+
+void QtGroupingProxy::setSourceModel(QAbstractItemModel* model)
+{
+ if (sourceModel()) {
+ disconnect(sourceModel(), nullptr, this, nullptr);
+ }
+
+ QAbstractProxyModel::setSourceModel(model);
+
+ if (sourceModel()) {
+ // signal proxies
+ connect(sourceModel(), SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ SLOT(modelRowsInserted(const QModelIndex&, int, int)));
+ connect(sourceModel(), SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)),
+ SLOT(modelRowsAboutToBeInserted(const QModelIndex&, int, int)));
+ connect(sourceModel(), SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ SLOT(modelRowsRemoved(const QModelIndex&, int, int)));
+ connect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)),
+ SLOT(modelRowsAboutToBeRemoved(QModelIndex, int, int)));
+ connect(sourceModel(), SIGNAL(layoutChanged()), SLOT(buildTree()));
+ connect(sourceModel(), SIGNAL(dataChanged(QModelIndex, QModelIndex)),
+ SLOT(modelDataChanged(QModelIndex, QModelIndex)));
+ connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(resetModel()));
+
+ buildTree();
+ }
+}
+
+void
+QtGroupingProxy::setGroupedColumn( int groupedColumn )
+{
+ m_groupedColumn = groupedColumn;
+ buildTree();
+}
+
+/** Maps to what groups the source row belongs by returning the data of those groups.
+ *
+ * @returns a list of data for the rows the argument belongs to. In common cases this list will
+ * contain only one entry. An empty list means that the source item will be placed in the root of
+ * this proxyModel. There is no support for hiding source items.
+ *
+ * Group data can be pre-loaded in the return value so it's added to the cache maintained by this
+ * class. This is required if you want to have data that is not present in the source model.
+ */
+QList<RowData>
+QtGroupingProxy::belongsTo( const QModelIndex &idx )
+{
+ QList<RowData> rowDataList;
+
+ //get all the data for this index from the model
+ ItemData itemData = sourceModel()->itemData( idx );
+ if (m_groupedRole != Qt::DisplayRole) {
+ itemData[Qt::DisplayRole] = itemData[m_groupedRole];
+ }
+
+ // invalid value in grouped role -> ungrouped
+ if (!itemData[Qt::DisplayRole].isValid()) {
+ return rowDataList;
+ }
+
+ QMapIterator<int, QVariant> i( itemData );
+ while( i.hasNext() )
+ {
+ i.next();
+ int role = i.key();
+ QVariant variant = i.value();
+
+ if ( variant.type() == QVariant::List )
+ {
+ //a list of variants get's expanded to multiple rows
+ QVariantList list = variant.toList();
+ for( int i = 0; i < list.length(); i++ )
+ {
+ //take an existing row data or create a new one
+ RowData rowData = (rowDataList.count() > i) ? rowDataList.takeAt( i )
+ : RowData();
+
+ //we only gather data for the first column
+ ItemData indexData = rowData.contains( 0 ) ? rowData.take( 0 ) : ItemData();
+ indexData.insert( role, list.value( i ) );
+ rowData.insert( 0, indexData );
+ //for the grouped column the data should not be gathered from the children
+ //this will allow filtering on the content of this column with a
+ //QSortFilterProxyModel
+ rowData.insert( m_groupedColumn, indexData );
+ rowDataList.insert( i, rowData );
+ }
+ break;
+ }
+ else if( !variant.isNull() )
+ {
+ //it's just a normal item. Copy all the data and break this loop.
+ RowData rowData;
+ rowData.insert( 0, itemData );
+ rowDataList << rowData;
+ break;
+ }
+ }
+
+ return rowDataList;
+}
+
+/* m_groupMap layout
+* key : index of the group in m_groupMaps
+* value : a QList of the original rows in sourceModel() for the children of this group
+*
+* key = -1 contains a QList of the non-grouped indexes
+*
+* TODO: sub-groups
+*/
+void
+QtGroupingProxy::buildTree()
+{
+ if( !sourceModel() )
+ return;
+ beginResetModel();
+
+ m_groupMap.clear();
+ //don't clear the data maps since most of it will probably be needed again.
+ m_parentCreateList.clear();
+
+ int max = sourceModel()->rowCount( m_rootNode );
+
+ //WARNING: these have to be added in order because the addToGroups function is optimized for
+ //modelRowsInserted(). Failure to do so will result in wrong data shown in the view at best.
+ for( int row = 0; row < max; row++ )
+ {
+ QModelIndex idx = sourceModel()->index( row, m_groupedColumn, m_rootNode );
+ addSourceRow( idx );
+ }
+ //dumpGroups();
+
+ if (m_flags & FLAG_NOSINGLE) {
+ // awkward: flatten single-item groups as a post-processing steps.
+
+ int currentKey = 0;
+ quint32 quint32max = std::numeric_limits<quint32>::max();
+ std::vector<int> rmgroups;
+
+ QMap<quint32, QList<int> > temp;
+
+ for (auto iter = m_groupMap.begin(); iter != m_groupMap.end(); ++iter) {
+ if ((iter.key() == quint32max) ||
+ (iter->count() < 2)) {
+ temp[quint32max].append(iter.value());
+ if (iter.key() != quint32max) {
+ rmgroups.push_back(iter.key());
+ }
+ } else {
+ temp[currentKey++] = *iter;
+ }
+ }
+ m_groupMap = temp;
+
+ // second loop is necessary because qt containers can't be iterated from end to front
+ // and removing by index from begin to end is ugly
+ std::sort(rmgroups.begin(), rmgroups.end(), [] (int lhs, int rhs) { return rhs < lhs; });
+ for (auto iter = rmgroups.begin(); iter != rmgroups.end(); ++iter) {
+ m_groupMaps.removeAt(*iter);
+ }
+ }
+
+ endResetModel();
+}
+
+QList<int>
+QtGroupingProxy::addSourceRow( const QModelIndex &idx )
+{
+ QList<int> updatedGroups;
+ QList<RowData> groupData = belongsTo( idx );
+
+ //an empty list here means it's supposed to go in root.
+ if( groupData.isEmpty() )
+ {
+ updatedGroups << -1;
+ if( !m_groupMap.keys().contains( std::numeric_limits<quint32>::max() ) )
+ m_groupMap.insert( std::numeric_limits<quint32>::max(), QList<int>() ); //add an empty placeholder
+ }
+
+ //an item can be in multiple groups
+ foreach( RowData data, groupData )
+ {
+ int updatedGroup = -1;
+ if( !data.isEmpty() )
+ {
+ foreach( const RowData &cachedData, m_groupMaps )
+ {
+ //when this matches the index belongs to an existing group
+ if( data[0][Qt::DisplayRole] == cachedData[0][Qt::DisplayRole] )
+ {
+ data = cachedData;
+ break;
+ }
+ }
+
+ updatedGroup = m_groupMaps.indexOf( data );
+ //-1 means not found
+ if( updatedGroup == -1 )
+ {
+ //new groups are added to the end of the existing list
+ m_groupMaps << data;
+ updatedGroup = m_groupMaps.count() - 1;
+ }
+
+ if( !m_groupMap.keys().contains( updatedGroup ) )
+ m_groupMap.insert( updatedGroup, QList<int>() ); //add an empty placeholder
+ }
+
+ if( !updatedGroups.contains( updatedGroup ) )
+ updatedGroups << updatedGroup;
+ }
+
+ //update m_groupMap to the new source-model layout (one row added)
+ QMutableMapIterator<quint32, QList<int> > i( m_groupMap );
+ while( i.hasNext() )
+ {
+ i.next();
+ QList<int> &groupList = i.value();
+ int insertedProxyRow = groupList.count();
+ for( ; insertedProxyRow > 0 ; insertedProxyRow-- )
+ {
+ int &rowValue = groupList[insertedProxyRow-1];
+ if( idx.row() <= rowValue )
+ {
+ //increment the rows that come after the new row since they moved one place up.
+ rowValue++;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if( updatedGroups.contains( i.key() ) )
+ {
+ //the row needs to be added to this group
+ groupList.insert( insertedProxyRow, idx.row() );
+ }
+ }
+
+ return updatedGroups;
+}
+
+/** Each ModelIndex has in it's internalId a position in the parentCreateList.
+ * struct ParentCreate are the instructions to recreate the parent index.
+ * It contains the proxy row number of the parent and the postion in this list of the grandfather.
+ * This function creates the ParentCreate structs and saves them in a list.
+ */
+int
+QtGroupingProxy::indexOfParentCreate( const QModelIndex &parent ) const
+{
+ if( !parent.isValid() )
+ return -1;
+
+ struct ParentCreate pc;
+ for( int i = 0 ; i < m_parentCreateList.size() ; i++ )
+ {
+ pc = m_parentCreateList[i];
+ if( pc.parentCreateIndex == parent.internalId() && pc.row == parent.row() )
+ return i;
+ }
+ //there is no parentCreate yet for this index, so let's create one.
+ pc.parentCreateIndex = parent.internalId();
+ pc.row = parent.row();
+ m_parentCreateList << pc;
+
+ return m_parentCreateList.size() - 1;
+}
+
+QModelIndex
+QtGroupingProxy::index( int row, int column, const QModelIndex &parent ) const
+{
+ if( !hasIndex(row, column, parent) ) {
+ return QModelIndex();
+ }
+
+ if( parent.column() > 0 ) {
+ return QModelIndex();
+ }
+
+ /* We save the instructions to make the parent of the index in a struct.
+ * The place of the struct in the list is stored in the internalId
+ */
+ int parentCreateIndex = indexOfParentCreate( parent );
+
+ return createIndex( row, column, parentCreateIndex );
+}
+
+QModelIndex
+QtGroupingProxy::parent( const QModelIndex &index ) const
+{
+ if( !index.isValid() )
+ return QModelIndex();
+
+ int parentCreateIndex = index.internalId();
+ if( parentCreateIndex == -1 || parentCreateIndex >= m_parentCreateList.count() )
+ return QModelIndex();
+
+ struct ParentCreate pc = m_parentCreateList[parentCreateIndex];
+
+ //only items at column 0 have children
+ return createIndex( pc.row, 0, pc.parentCreateIndex );
+}
+
+int
+QtGroupingProxy::rowCount( const QModelIndex &index ) const
+{
+ if( !index.isValid() )
+ {
+ //the number of top level groups + the number of non-grouped items
+ int rows = m_groupMaps.count() + m_groupMap.value( std::numeric_limits<quint32>::max() ).count();
+ return rows;
+ }
+
+ //TODO:group in group support.
+ if( isGroup( index ) )
+ {
+ qint64 groupIndex = index.row();
+ int rows = m_groupMap.value( groupIndex ).count();
+ return rows;
+ } else {
+ QModelIndex originalIndex = mapToSource( index );
+ int rowCount = sourceModel()->rowCount( originalIndex );
+ return rowCount;
+ }
+}
+
+int
+QtGroupingProxy::columnCount( const QModelIndex &index ) const
+{
+ if( !index.isValid() )
+ return sourceModel()->columnCount( m_rootNode );
+
+ if( index.column() != 0 )
+ return 0;
+
+ return sourceModel()->columnCount( mapToSource( index ) );
+}
+
+
+static bool variantLess(const QVariant &LHS, const QVariant &RHS)
+{
+ if ((LHS.type() == RHS.type()) &&
+ ((LHS.type() == QVariant::Int) || (LHS.type() == QVariant::UInt))) {
+ return LHS.toInt() < RHS.toInt();
+ }
+
+ // this should always work (comparing empty strings in the worst case) but
+ // the results may be wrong
+ return LHS.toString() < RHS.toString();
+}
+
+
+static QVariant variantMax(const QVariantList &variants)
+{
+ QVariant result = variants.first();
+ foreach (const QVariant &iter, variants) {
+ if (variantLess(result, iter)) {
+ result = iter;
+ }
+ }
+ return result;
+}
+
+
+static QVariant variantMin(const QVariantList &variants)
+{
+ QVariant result = variants.first();
+ foreach (const QVariant &iter, variants) {
+ if (variantLess(iter, result)) {
+ result = iter;
+ }
+ }
+ return result;
+}
+
+
+QVariant
+QtGroupingProxy::data( const QModelIndex &index, int role ) const
+{
+ if( !index.isValid() )
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+ if( isGroup( index ) )
+ {
+ if ((role != Qt::DisplayRole) && (role != Qt::EditRole)) {
+ switch (role) {
+ case Qt::ForegroundRole: {
+ return QBrush(Qt::gray);
+ } break;
+ case Qt::FontRole: {
+ QFont font(m_groupMaps[row][column].value(Qt::FontRole).value<QFont>());
+ font.setItalic(true);
+ return font;
+ } break;
+ case Qt::TextAlignmentRole: {
+ return Qt::AlignHCenter;
+ } break;
+ case Qt::UserRole: {
+ return m_groupMaps[row][column].value( Qt::DisplayRole ).toString();
+ } break;
+ case Qt::CheckStateRole: {
+ if (column != 0) return QVariant();
+ int childCount = m_groupMap.value( row ).count();
+ int checked = 0;
+ QModelIndex parentIndex = this->index( row, 0, index.parent() );
+ for( int childRow = 0; childRow < childCount; ++childRow )
+ {
+ QModelIndex childIndex = this->index( childRow, 0, parentIndex );
+ QVariant data = mapToSource( childIndex ).data( Qt::CheckStateRole );
+ if (data.toInt() == 2) ++checked;
+ }
+ if (checked == childCount) return Qt::Checked;
+ else if (checked == 0) return Qt::Unchecked;
+ else return Qt::PartiallyChecked;
+ } break;
+ default: {
+ QModelIndex parentIndex = this->index( row, 0, index.parent() );
+ if (m_groupMap.value( row ).count() > 0) {
+ return this->index(0, column, parentIndex).data(role);
+ } else {
+ return QVariant();
+ }
+ // return m_groupMaps[row][column].value( role );
+ } break;
+ }
+ }
+
+ //use cached or precalculated data
+ if( m_groupMaps[row][column].contains( Qt::DisplayRole ) )
+ {
+ if ((m_flags & FLAG_NOGROUPNAME) != 0) {
+ QModelIndex parentIndex = this->index( row, 0, index.parent() );
+ QModelIndex childIndex = this->index( 0, column, parentIndex );
+ return childIndex.data(role).toString();
+ } else {
+ return m_groupMaps[row][column].value( role ).toString();
+ }
+ }
+
+ //for column 0 we gather data from the grouped column instead
+ if( column == 0 )
+ column = m_groupedColumn;
+
+ //map all data from children to columns of group to allow grouping one level up
+ QVariantList variantsOfChildren;
+ int childCount = m_groupMap.value( row ).count();
+ if( childCount == 0 )
+ return QVariant();
+
+ int function = AGGR_NONE;
+ if (m_aggregateRole >= Qt::UserRole) {
+ QModelIndex parentIndex = this->index( row, 0, index.parent() );
+ QModelIndex childIndex = this->index( 0, column, parentIndex );
+ function = mapToSource(childIndex).data(m_aggregateRole).toInt();
+ }
+
+ //Need a parentIndex with column == 0 because only those have children.
+ QModelIndex parentIndex = this->index( row, 0, index.parent() );
+ for( int childRow = 0; childRow < childCount; childRow++ )
+ {
+ QModelIndex childIndex = this->index( childRow, column, parentIndex );
+ QVariant data = mapToSource( childIndex ).data( role );
+
+ if( data.isValid() && !variantsOfChildren.contains( data ) )
+ variantsOfChildren << data;
+ }
+
+ //saving in cache
+ ItemData roleMap = m_groupMaps[row].value( column );
+ foreach( const QVariant &variant, variantsOfChildren )
+ {
+ if( roleMap[ role ] != variant ) {
+ roleMap.insert( role, variantsOfChildren );
+ }
+ }
+
+ if( variantsOfChildren.count() == 0 )
+ return QVariant();
+
+ //only one unique variant? No need to return a list
+ switch (function) {
+ case AGGR_EMPTY: return QVariant();
+ case AGGR_FIRST: return variantsOfChildren.first();
+ case AGGR_MAX: return variantMax(variantsOfChildren);
+ case AGGR_MIN: return variantMin(variantsOfChildren);
+ default: {
+ if( variantsOfChildren.count() == 1 )
+ return variantsOfChildren.first();
+
+ return variantsOfChildren;
+ } break;
+ }
+ }
+
+ return mapToSource( index ).data( role );
+}
+
+bool
+QtGroupingProxy::setData( const QModelIndex &idx, const QVariant &value, int role )
+{
+ if( !idx.isValid() )
+ return false;
+
+ //no need to set data to exactly the same value
+ if( idx.data( role ) == value )
+ return false;
+
+ if( isGroup( idx ) )
+ {
+ ItemData columnData = m_groupMaps[idx.row()][idx.column()];
+
+ columnData.insert( role, value );
+ //QItemDelegate will always use Qt::EditRole
+ if( role == Qt::EditRole )
+ columnData.insert( Qt::DisplayRole, value );
+
+ //and make sure it's stored in the map
+ m_groupMaps[idx.row()].insert( idx.column(), columnData );
+
+ int columnToChange = idx.column() ? idx.column() : m_groupedColumn;
+ foreach( int originalRow, m_groupMap.value( idx.row() ) )
+ {
+ QModelIndex childIdx = sourceModel()->index( originalRow, columnToChange,
+ m_rootNode );
+ if( childIdx.isValid() )
+ sourceModel()->setData( childIdx, value, role );
+ }
+ //TODO: we might need to reload the data from the children at this point
+
+ emit dataChanged( idx, idx );
+ return true;
+ }
+
+ return sourceModel()->setData( mapToSource( idx ), value, role );
+}
+
+bool
+QtGroupingProxy::isGroup( const QModelIndex &index ) const
+{
+ int parentCreateIndex = index.internalId();
+ if( parentCreateIndex == -1 && index.row() < m_groupMaps.count() )
+ return true;
+ return false;
+}
+
+QModelIndex
+QtGroupingProxy::mapToSource( const QModelIndex &index ) const
+{
+ if( !index.isValid() ) {
+ return m_rootNode;
+ }
+
+ if( isGroup( index ) )
+ {
+ return m_rootNode;
+ }
+
+ QModelIndex proxyParent = index.parent();
+ QModelIndex originalParent = mapToSource( proxyParent );
+
+ int originalRow = index.row();
+ if( originalParent == m_rootNode )
+ {
+ int indexInGroup = index.row();
+ if( !proxyParent.isValid() )
+ indexInGroup -= m_groupMaps.count();
+
+ QList<int> childRows = m_groupMap.value( proxyParent.row() );
+ if( childRows.isEmpty() || indexInGroup >= childRows.count() || indexInGroup < 0 )
+ return QModelIndex();
+
+ originalRow = childRows.at( indexInGroup );
+ }
+ return sourceModel()->index( originalRow, index.column(), originalParent );
+}
+
+QModelIndexList
+QtGroupingProxy::mapToSource( const QModelIndexList& list ) const
+{
+ QModelIndexList originalList;
+ foreach( const QModelIndex &index, list )
+ {
+ QModelIndex originalIndex = mapToSource( index );
+ if( originalIndex.isValid() )
+ originalList << originalIndex;
+ }
+ return originalList;
+}
+
+QModelIndex
+QtGroupingProxy::mapFromSource( const QModelIndex &idx ) const
+{
+ if( !idx.isValid() )
+ return QModelIndex();
+
+ QModelIndex proxyParent;
+ QModelIndex sourceParent = idx.parent();
+
+ int proxyRow = idx.row();
+ int sourceRow = idx.row();
+
+ if( sourceParent.isValid() && ( sourceParent != m_rootNode ) )
+ {
+ //idx is a child of one of the items in the source model
+ proxyParent = mapFromSource( sourceParent );
+ }
+ else
+ {
+ //idx is an item in the top level of the source model (child of the rootnode)
+ int groupRow = -1;
+ QMapIterator<quint32, QList<int> > iterator( m_groupMap );
+ while( iterator.hasNext() )
+ {
+ iterator.next();
+ if( iterator.value().contains( sourceRow ) )
+ {
+ groupRow = iterator.key();
+ break;
+ }
+ }
+
+ if( groupRow != -1 ) //it's in a group, let's find the correct row.
+ {
+ proxyParent = this->index( groupRow, 0, QModelIndex() );
+ proxyRow = m_groupMap.value( groupRow ).indexOf( sourceRow );
+ }
+ else
+ {
+ proxyParent = QModelIndex();
+ // if the proxy item is not in a group it will be below the groups.
+ int groupLength = m_groupMaps.count();
+ int i = m_groupMap.value( std::numeric_limits<quint32>::max() ).indexOf( sourceRow );
+
+ proxyRow = groupLength + i;
+ }
+ }
+
+ return this->index( proxyRow, idx.column(), proxyParent );
+}
+
+Qt::ItemFlags
+QtGroupingProxy::flags( const QModelIndex &idx ) const
+{
+ if( !idx.isValid() )
+ {
+ Qt::ItemFlags rootFlags = sourceModel()->flags( m_rootNode );
+ if( rootFlags.testFlag( Qt::ItemIsDropEnabled ) )
+ return Qt::ItemFlags( Qt::ItemIsDropEnabled );
+
+ return Qt::ItemFlags(0);
+ }
+
+ //only if the grouped column has the editable flag set allow the
+ //actions leading to setData on the source (edit & drop)
+ if( isGroup( idx ) )
+ {
+ // dumpGroups();
+ Qt::ItemFlags defaultFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable );
+ //Qt::ItemFlags defaultFlags(Qt::ItemIsEnabled);
+ bool groupIsEditable = true;
+
+ if (idx.column() == 0) {
+ bool checkable = true;
+ foreach ( int originalRow, m_groupMap.value( idx.row() ) )
+ {
+ QModelIndex originalIdx = sourceModel()->index( originalRow, 0,
+ m_rootNode.parent() );
+ if ( (originalIdx.flags() & Qt::ItemIsUserCheckable) == 0 )
+ {
+ checkable = false;
+ }
+ }
+
+ if ( checkable ) {
+ defaultFlags |= Qt::ItemIsUserCheckable;
+ }
+ }
+
+ //it's possible to have empty groups
+ if( m_groupMap.value( idx.row() ).count() == 0 )
+ {
+ //check the flags of this column with the root node
+ QModelIndex originalRootNode = sourceModel()->index( m_rootNode.row(), m_groupedColumn,
+ m_rootNode.parent() );
+ groupIsEditable = originalRootNode.flags().testFlag( Qt::ItemIsEditable );
+ }
+ else
+ {
+ foreach( int originalRow, m_groupMap.value( idx.row() ) )
+ {
+ QModelIndex originalIdx = sourceModel()->index( originalRow, m_groupedColumn,
+ m_rootNode );
+
+ groupIsEditable = groupIsEditable
+ ? originalIdx.flags().testFlag( Qt::ItemIsEditable )
+ : false;
+ if( !groupIsEditable ) //all children need to have an editable grouped column
+ break;
+ }
+ }
+ if( groupIsEditable )
+ return ( defaultFlags | Qt::ItemIsEditable | Qt::ItemIsDropEnabled );
+ return defaultFlags;
+ }
+
+ QModelIndex originalIdx = mapToSource( idx );
+ Qt::ItemFlags originalItemFlags = sourceModel()->flags( originalIdx );
+
+ //check the source model to see if the grouped column is editable;
+ QModelIndex groupedColumnIndex =
+ sourceModel()->index( originalIdx.row(), m_groupedColumn, originalIdx.parent() );
+ bool groupIsEditable = sourceModel()->flags( groupedColumnIndex ).testFlag( Qt::ItemIsEditable );
+
+ if( groupIsEditable )
+ return originalItemFlags | Qt::ItemIsDragEnabled;
+ return originalItemFlags;
+}
+
+QVariant
+QtGroupingProxy::headerData( int section, Qt::Orientation orientation, int role ) const
+{
+ return sourceModel()->headerData( section, orientation, role );
+}
+
+bool
+QtGroupingProxy::canFetchMore( const QModelIndex &parent ) const
+{
+ if( !parent.isValid() )
+ return false;
+
+ if( isGroup( parent ) )
+ return false;
+
+ return sourceModel()->canFetchMore( mapToSource( parent ) );
+}
+
+void
+QtGroupingProxy::fetchMore ( const QModelIndex & parent )
+{
+ if( !parent.isValid() )
+ return;
+
+ if( isGroup( parent ) )
+ return;
+
+ return sourceModel()->fetchMore( mapToSource( parent ) );
+}
+
+QModelIndex
+QtGroupingProxy::addEmptyGroup( const RowData &data )
+{
+ int newRow = m_groupMaps.count();
+ beginInsertRows( QModelIndex(), newRow, newRow );
+ m_groupMaps << data;
+ endInsertRows();
+ return index( newRow, 0, QModelIndex() );
+}
+
+bool
+QtGroupingProxy::removeGroup( const QModelIndex &idx )
+{
+ beginRemoveRows( idx.parent(), idx.row(), idx.row() );
+ m_groupMap.remove( idx.row() );
+ m_groupMaps.removeAt( idx.row() );
+ m_parentCreateList.removeAt( idx.internalId() );
+ endRemoveRows();
+
+ //TODO: only true if all data could be unset.
+ return true;
+}
+
+bool
+QtGroupingProxy::hasChildren( const QModelIndex &parent ) const
+{
+ if( !parent.isValid() ) {
+ return true;
+ }
+
+ if( isGroup( parent ) ) {
+ return !m_groupMap.value( parent.row() ).isEmpty();
+ }
+
+ return sourceModel()->hasChildren( mapToSource( parent ) );
+}
+
+bool
+QtGroupingProxy::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
+{
+ QModelIndex idx = index(row, column, parent);
+ if (isGroup(idx)) {
+ QList<int> childRows = m_groupMap.value(idx.row());
+ int max = *std::max_element(childRows.begin(), childRows.end());
+
+ QModelIndex newIdx = mapToSource(index(max, column, idx));
+ return sourceModel()->dropMimeData(data, action, max, column, newIdx);
+ } else {
+ if (row == -1) {
+ return sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent));
+ } else {
+ QModelIndex idx = mapToSource(index(row, column, parent));
+ return sourceModel()->dropMimeData(data, action, idx.row(), idx.column(), idx.parent());
+ }
+ }
+}
+
+void
+QtGroupingProxy::modelRowsAboutToBeInserted( const QModelIndex &parent, int start, int end )
+{
+ if( parent != m_rootNode )
+ {
+ //an item will be added to an original index, remap and pass it on
+ QModelIndex proxyParent = mapFromSource( parent );
+ beginInsertRows( proxyParent, start, end );
+ }
+}
+
+void
+QtGroupingProxy::modelRowsInserted( const QModelIndex &parent, int start, int end )
+{
+ if( parent == m_rootNode )
+ {
+ //top level of the model changed, these new rows need to be put in groups
+ for( int modelRow = start; modelRow <= end ; modelRow++ )
+ {
+ addSourceRow( sourceModel()->index( modelRow, m_groupedColumn, m_rootNode ) );
+ }
+ }
+ else
+ {
+ //an item was added to an original index, remap and pass it on
+ QModelIndex proxyParent = mapFromSource( parent );
+
+ QString s;
+ QDebug debug(&s);
+ debug << proxyParent;
+ log::debug("{}", s);
+
+ //beginInsertRows had to be called in modelRowsAboutToBeInserted()
+ endInsertRows();
+ }
+}
+
+void
+QtGroupingProxy::modelRowsAboutToBeRemoved( const QModelIndex &parent, int start, int end )
+{
+ if( parent == m_rootNode )
+ {
+ QMap<quint32, QList<int> >::const_iterator i;
+ //HACK, we are going to call beginRemoveRows() multiple times without
+ // endRemoveRows() if a source index is in multiple groups.
+ // This can be a problem for some views/proxies, but Q*Views can handle it.
+ // TODO: investigate a queue for applying proxy model changes in the correct order
+ for( i = m_groupMap.constBegin(); i != m_groupMap.constEnd(); ++i )
+ {
+ int groupIndex = i.key();
+ const QList<int> &groupList = i.value();
+ QModelIndex proxyParent = index( groupIndex, 0 );
+ foreach( int originalRow, groupList )
+ {
+ if( originalRow >= start && originalRow <= end )
+ {
+ int proxyRow = groupList.indexOf( originalRow );
+ if( groupIndex == -1 ) //adjust for non-grouped (root level) original items
+ proxyRow += m_groupMaps.count();
+ //TODO: optimize for continues original rows in the same group
+ beginRemoveRows( proxyParent, proxyRow, proxyRow );
+ }
+ }
+ }
+ }
+ else
+ {
+ //child item(s) of an original item will be removed, remap and pass it on
+ QModelIndex proxyParent = mapFromSource( parent );
+ beginRemoveRows( proxyParent, start, end );
+ }
+}
+
+void
+QtGroupingProxy::modelRowsRemoved( const QModelIndex &parent, int start, int end )
+{
+ if( parent == m_rootNode )
+ {
+ //TODO: can be optimised by iterating over m_groupMap and checking start <= r < end
+
+ //rather than increasing i we change the stored sourceRows in-place and reuse argument start
+ //X-times (where X = end - start).
+ for( int i = start; i <= end; i++ )
+ {
+ //HACK: we are going to iterate the hash in reverse so calls to endRemoveRows()
+ // are matched up with the beginRemoveRows() in modelRowsAboutToBeRemoved()
+ //NOTE: easier to do reverse with java style iterator
+ QMutableMapIterator<quint32, QList<int> > iter( m_groupMap );
+ iter.toBack();
+ while( iter.hasPrevious() )
+ {
+ iter.previous();
+ int groupIndex = iter.key();
+ //has to be a modifiable reference for remove and replace operations
+ QList<int> &groupList = iter.value();
+ int rowIndex = groupList.indexOf( start );
+ if( rowIndex != -1 )
+ {
+ QModelIndex proxyParent = index( groupIndex, 0 );
+ groupList.removeAt( rowIndex );
+ }
+ //Now decrement all source rows that are after the removed row
+ for( int j = 0; j < groupList.count(); j++ )
+ {
+ int sourceRow = groupList.at( j );
+ if( sourceRow > start )
+ groupList.replace( j, sourceRow-1 );
+ }
+ if( rowIndex != -1)
+ endRemoveRows(); //end remove operation only after group was updated.
+ }
+ }
+
+ return;
+ }
+
+ //beginRemoveRows had to be called in modelRowsAboutToBeRemoved();
+ endRemoveRows();
+}
+
+void
+QtGroupingProxy::resetModel()
+{
+ buildTree();
+}
+
+void
+QtGroupingProxy::modelDataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight )
+{
+ //TODO: need to look in the groupedColumn and see if it changed and changed grouping accordingly
+ QModelIndex proxyTopLeft = mapFromSource( topLeft );
+ if( !proxyTopLeft.isValid() )
+ return;
+
+ if( topLeft == bottomRight )
+ {
+ emit dataChanged( proxyTopLeft, proxyTopLeft );
+ }
+ else
+ {
+ QModelIndex proxyBottomRight = mapFromSource( bottomRight );
+ emit dataChanged( proxyTopLeft, proxyBottomRight );
+ }
+}
+
+bool
+QtGroupingProxy::isAGroupSelected( const QModelIndexList& list ) const
+{
+ foreach( const QModelIndex &index, list )
+ {
+ if( isGroup( index ) )
+ return true;
+ }
+ return false;
+}
+
+void
+QtGroupingProxy::dumpGroups() const
+{
+ QString s;
+ QDebug debug(&s);
+
+ debug << "m_groupMap:\n";
+ for( int groupIndex = -1; groupIndex < m_groupMap.keys().count() - 1; groupIndex++ )
+ {
+ debug << groupIndex << " : " << m_groupMap.value( groupIndex ) << "\n";
+ }
+
+ debug << "m_groupMaps:\n";
+ for( int groupIndex = 0; groupIndex < m_groupMaps.count(); groupIndex++ )
+ {
+ debug << m_groupMaps[groupIndex] << ": " << m_groupMap.value( groupIndex ) << "\n";
+ }
+
+ debug << m_groupMap.value( std::numeric_limits<quint32>::max() );
+
+ log::debug("{}", s);
+}
diff --git a/src/qtgroupingproxy.h b/src/qtgroupingproxy.h
index c6116d8a..80ca4eec 100644
--- a/src/qtgroupingproxy.h
+++ b/src/qtgroupingproxy.h
@@ -1,153 +1,153 @@
-/****************************************************************************************
- * Copyright (c) 2007-2010 Bart Cerneels <bart.cerneels@kde.org> *
- * *
- * This program is free software; you can redistribute it and/or modify it under *
- * the terms of the GNU General Public License as published by the Free Software *
- * Foundation; either version 2 of the License, or (at your option) any later *
- * version. *
- * *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
- * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License along with *
- * this program. If not, see <http://www.gnu.org/licenses/>. *
- ****************************************************************************************/
-
-// Modifications 2013-03-27 to 2013-03-28 by Sebastian Herbord
-
-#ifndef GROUPINGPROXY_H
-#define GROUPINGPROXY_H
-
-#include <QAbstractProxyModel>
-#include <QModelIndex>
-#include <QMultiHash>
-#include <QStringList>
-#include <QIcon>
-#include <QSet>
-
-typedef QMap<int, QVariant> ItemData;
-typedef QMap<int, ItemData> RowData;
-
-class QtGroupingProxy : public QAbstractProxyModel
-{
- Q_OBJECT
-
-public:
-
- static const unsigned int FLAG_NOSINGLE = 1;
- static const unsigned int FLAG_NOGROUPNAME = 2;
-
- enum EAggregateFunction {
- AGGR_NONE, // no aggregation, return child elements as list
- AGGR_EMPTY, // display nothing
- AGGR_FIRST, // return value of the topmost item
- AGGR_MAX, // return maximum value
- AGGR_MIN // return minimum value
- };
-
-public:
- explicit QtGroupingProxy( QModelIndex rootNode = QModelIndex(),
- int groupedColumn = -1, int groupedRole = Qt::DisplayRole,
- unsigned int flags = 0,
- int aggregateRole = Qt::DisplayRole);
- ~QtGroupingProxy();
-
- void setSourceModel(QAbstractItemModel* model) override;
- void setGroupedColumn( int groupedColumn );
-
- /* QAbstractProxyModel methods */
- virtual QModelIndex index( int, int c = 0,
- const QModelIndex& parent = QModelIndex() ) const;
- virtual Qt::ItemFlags flags( const QModelIndex &idx ) const;
- virtual QModelIndex parent( const QModelIndex &idx ) const;
- virtual int rowCount( const QModelIndex &idx = QModelIndex() ) const;
- virtual int columnCount( const QModelIndex &idx ) const;
- virtual QModelIndex mapToSource( const QModelIndex &idx ) const;
- virtual QModelIndexList mapToSource( const QModelIndexList &list ) const;
- virtual QModelIndex mapFromSource( const QModelIndex &idx ) const;
- virtual QVariant data( const QModelIndex &idx, int role ) const;
- virtual bool setData( const QModelIndex &index, const QVariant &value,
- int role = Qt::EditRole );
- virtual QVariant headerData ( int section, Qt::Orientation orientation,
- int role ) const;
- virtual bool canFetchMore( const QModelIndex &parent ) const;
- virtual void fetchMore( const QModelIndex &parent );
- virtual bool hasChildren( const QModelIndex &parent = QModelIndex() ) const;
- virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
-
- /* QtGroupingProxy methods */
- virtual QModelIndex addEmptyGroup( const RowData &data );
- virtual bool removeGroup( const QModelIndex &idx );
-
-protected slots:
- virtual void buildTree();
-
-private slots:
- void modelDataChanged( const QModelIndex &, const QModelIndex & );
- void modelRowsAboutToBeInserted( const QModelIndex &, int ,int );
- void modelRowsInserted( const QModelIndex &, int, int );
- void modelRowsAboutToBeRemoved( const QModelIndex &, int ,int );
- void modelRowsRemoved( const QModelIndex &, int, int );
- void resetModel();
-
-protected:
- /** Maps an item to a group.
- * The return value is a list because an item can put in multiple groups.
- * Inside the list is a 2 dimensional map.
- * Mapped to column-number is another map of role-number to QVariant.
- * This data prepolulates the group-data cache. The rest is gathered on demand
- * from the children of the group.
- */
- virtual QList<RowData> belongsTo( const QModelIndex &idx );
-
- /**
- * calls belongsTo(), checks cached data and adds the index to existing or new groups.
- * @returns the groups this index was added to where -1 means it was added to the root.
- */
- QList<int> addSourceRow( const QModelIndex &idx );
-
- bool isGroup( const QModelIndex &index ) const;
- bool isAGroupSelected( const QModelIndexList &list ) const;
-
- /** Maintains the group -> sourcemodel row mapping
- * The reason a QList<int> is use instead of a QMultiHash is that the values have to be
- * reordered when rows are inserted or removed.
- * TODO:use some auto-incrementing container class (steveire's?) for the list
- */
- QMap<quint32, QList<int> > m_groupMap;
- /** The data cache of the groups.
- * This can be pre-loaded with data in belongsTo()
- */
- QList<RowData> m_groupMaps;
-
- /** "instuctions" how to create an item in the tree.
- * This is used by parent( QModelIndex )
- */
- struct ParentCreate
- {
- int parentCreateIndex;
- int row;
- };
- mutable QList<struct ParentCreate> m_parentCreateList;
- /** @returns index of the "instructions" to recreate the parent. Will create new if it doesn't exist yet.
- */
- int indexOfParentCreate( const QModelIndex &parent ) const;
-
- QModelIndexList m_selectedGroups;
-
- QModelIndex m_rootNode;
- int m_groupedColumn;
-
- /* debug function */
- void dumpGroups() const;
-
-private:
- unsigned int m_flags;
- int m_groupedRole;
-
- int m_aggregateRole;
-
-};
-
-#endif //GROUPINGPROXY_H
+/****************************************************************************************
+ * Copyright (c) 2007-2010 Bart Cerneels <bart.cerneels@kde.org> *
+ * *
+ * This program is free software; you can redistribute it and/or modify it under *
+ * the terms of the GNU General Public License as published by the Free Software *
+ * Foundation; either version 2 of the License, or (at your option) any later *
+ * version. *
+ * *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License along with *
+ * this program. If not, see <http://www.gnu.org/licenses/>. *
+ ****************************************************************************************/
+
+// Modifications 2013-03-27 to 2013-03-28 by Sebastian Herbord
+
+#ifndef GROUPINGPROXY_H
+#define GROUPINGPROXY_H
+
+#include <QAbstractProxyModel>
+#include <QModelIndex>
+#include <QMultiHash>
+#include <QStringList>
+#include <QIcon>
+#include <QSet>
+
+typedef QMap<int, QVariant> ItemData;
+typedef QMap<int, ItemData> RowData;
+
+class QtGroupingProxy : public QAbstractProxyModel
+{
+ Q_OBJECT
+
+public:
+
+ static const unsigned int FLAG_NOSINGLE = 1;
+ static const unsigned int FLAG_NOGROUPNAME = 2;
+
+ enum EAggregateFunction {
+ AGGR_NONE, // no aggregation, return child elements as list
+ AGGR_EMPTY, // display nothing
+ AGGR_FIRST, // return value of the topmost item
+ AGGR_MAX, // return maximum value
+ AGGR_MIN // return minimum value
+ };
+
+public:
+ explicit QtGroupingProxy( QModelIndex rootNode = QModelIndex(),
+ int groupedColumn = -1, int groupedRole = Qt::DisplayRole,
+ unsigned int flags = 0,
+ int aggregateRole = Qt::DisplayRole);
+ ~QtGroupingProxy();
+
+ void setSourceModel(QAbstractItemModel* model) override;
+ void setGroupedColumn( int groupedColumn );
+
+ /* QAbstractProxyModel methods */
+ virtual QModelIndex index( int, int c = 0,
+ const QModelIndex& parent = QModelIndex() ) const;
+ virtual Qt::ItemFlags flags( const QModelIndex &idx ) const;
+ virtual QModelIndex parent( const QModelIndex &idx ) const;
+ virtual int rowCount( const QModelIndex &idx = QModelIndex() ) const;
+ virtual int columnCount( const QModelIndex &idx ) const;
+ virtual QModelIndex mapToSource( const QModelIndex &idx ) const;
+ virtual QModelIndexList mapToSource( const QModelIndexList &list ) const;
+ virtual QModelIndex mapFromSource( const QModelIndex &idx ) const;
+ virtual QVariant data( const QModelIndex &idx, int role ) const;
+ virtual bool setData( const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole );
+ virtual QVariant headerData ( int section, Qt::Orientation orientation,
+ int role ) const;
+ virtual bool canFetchMore( const QModelIndex &parent ) const;
+ virtual void fetchMore( const QModelIndex &parent );
+ virtual bool hasChildren( const QModelIndex &parent = QModelIndex() ) const;
+ virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+
+ /* QtGroupingProxy methods */
+ virtual QModelIndex addEmptyGroup( const RowData &data );
+ virtual bool removeGroup( const QModelIndex &idx );
+
+protected slots:
+ virtual void buildTree();
+
+private slots:
+ void modelDataChanged( const QModelIndex &, const QModelIndex & );
+ void modelRowsAboutToBeInserted( const QModelIndex &, int ,int );
+ void modelRowsInserted( const QModelIndex &, int, int );
+ void modelRowsAboutToBeRemoved( const QModelIndex &, int ,int );
+ void modelRowsRemoved( const QModelIndex &, int, int );
+ void resetModel();
+
+protected:
+ /** Maps an item to a group.
+ * The return value is a list because an item can put in multiple groups.
+ * Inside the list is a 2 dimensional map.
+ * Mapped to column-number is another map of role-number to QVariant.
+ * This data prepolulates the group-data cache. The rest is gathered on demand
+ * from the children of the group.
+ */
+ virtual QList<RowData> belongsTo( const QModelIndex &idx );
+
+ /**
+ * calls belongsTo(), checks cached data and adds the index to existing or new groups.
+ * @returns the groups this index was added to where -1 means it was added to the root.
+ */
+ QList<int> addSourceRow( const QModelIndex &idx );
+
+ bool isGroup( const QModelIndex &index ) const;
+ bool isAGroupSelected( const QModelIndexList &list ) const;
+
+ /** Maintains the group -> sourcemodel row mapping
+ * The reason a QList<int> is use instead of a QMultiHash is that the values have to be
+ * reordered when rows are inserted or removed.
+ * TODO:use some auto-incrementing container class (steveire's?) for the list
+ */
+ QMap<quint32, QList<int> > m_groupMap;
+ /** The data cache of the groups.
+ * This can be pre-loaded with data in belongsTo()
+ */
+ QList<RowData> m_groupMaps;
+
+ /** "instuctions" how to create an item in the tree.
+ * This is used by parent( QModelIndex )
+ */
+ struct ParentCreate
+ {
+ int parentCreateIndex;
+ int row;
+ };
+ mutable QList<struct ParentCreate> m_parentCreateList;
+ /** @returns index of the "instructions" to recreate the parent. Will create new if it doesn't exist yet.
+ */
+ int indexOfParentCreate( const QModelIndex &parent ) const;
+
+ QModelIndexList m_selectedGroups;
+
+ QModelIndex m_rootNode;
+ int m_groupedColumn;
+
+ /* debug function */
+ void dumpGroups() const;
+
+private:
+ unsigned int m_flags;
+ int m_groupedRole;
+
+ int m_aggregateRole;
+
+};
+
+#endif //GROUPINGPROXY_H
diff --git a/src/queryoverwritedialog.cpp b/src/queryoverwritedialog.cpp
index eb67719d..5e42a57c 100644
--- a/src/queryoverwritedialog.cpp
+++ b/src/queryoverwritedialog.cpp
@@ -1,67 +1,67 @@
-/*
-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 "queryoverwritedialog.h"
-#include "ui_queryoverwritedialog.h"
-
-#include <QStyle>
-
-QueryOverwriteDialog::QueryOverwriteDialog(QWidget *parent, Backup b)
- : QDialog(parent), ui(new Ui::QueryOverwriteDialog),
- m_Action(ACT_NONE)
-{
- ui->setupUi(this);
- ui->backupBox->setChecked(b == BACKUP_YES);
- QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion);
- ui->iconLabel->setPixmap(icon.pixmap(128));
-
-}
-
-QueryOverwriteDialog::~QueryOverwriteDialog()
-{
- delete ui;
-}
-
-bool QueryOverwriteDialog::backup() const
-{
- return ui->backupBox->isChecked();
-}
-
-void QueryOverwriteDialog::on_mergeBtn_clicked()
-{
- this->m_Action = ACT_MERGE;
- this->accept();
-}
-
-void QueryOverwriteDialog::on_replaceBtn_clicked()
-{
- this->m_Action = ACT_REPLACE;
- this->accept();
-}
-
-void QueryOverwriteDialog::on_renameBtn_clicked()
-{
- this->m_Action = ACT_RENAME;
- this->accept();
-}
-
-void QueryOverwriteDialog::on_cancelBtn_clicked()
-{
- this->reject();
-}
+/*
+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 "queryoverwritedialog.h"
+#include "ui_queryoverwritedialog.h"
+
+#include <QStyle>
+
+QueryOverwriteDialog::QueryOverwriteDialog(QWidget *parent, Backup b)
+ : QDialog(parent), ui(new Ui::QueryOverwriteDialog),
+ m_Action(ACT_NONE)
+{
+ ui->setupUi(this);
+ ui->backupBox->setChecked(b == BACKUP_YES);
+ QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion);
+ ui->iconLabel->setPixmap(icon.pixmap(128));
+
+}
+
+QueryOverwriteDialog::~QueryOverwriteDialog()
+{
+ delete ui;
+}
+
+bool QueryOverwriteDialog::backup() const
+{
+ return ui->backupBox->isChecked();
+}
+
+void QueryOverwriteDialog::on_mergeBtn_clicked()
+{
+ this->m_Action = ACT_MERGE;
+ this->accept();
+}
+
+void QueryOverwriteDialog::on_replaceBtn_clicked()
+{
+ this->m_Action = ACT_REPLACE;
+ this->accept();
+}
+
+void QueryOverwriteDialog::on_renameBtn_clicked()
+{
+ this->m_Action = ACT_RENAME;
+ this->accept();
+}
+
+void QueryOverwriteDialog::on_cancelBtn_clicked()
+{
+ this->reject();
+}
diff --git a/src/queryoverwritedialog.h b/src/queryoverwritedialog.h
index 3f7b3194..a98f23ef 100644
--- a/src/queryoverwritedialog.h
+++ b/src/queryoverwritedialog.h
@@ -1,61 +1,61 @@
-/*
-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 QUERYOVERWRITEDIALOG_H
-#define QUERYOVERWRITEDIALOG_H
-
-#include <QDialog>
-
-namespace Ui {
-class QueryOverwriteDialog;
-}
-
-class QueryOverwriteDialog : public QDialog
-{
- Q_OBJECT
-public:
- enum Action {
- ACT_NONE,
- ACT_MERGE,
- ACT_REPLACE,
- ACT_RENAME
- };
-
- enum Backup {
- BACKUP_NO,
- BACKUP_YES
- };
-
-public:
- QueryOverwriteDialog(QWidget *parent, Backup b);
- ~QueryOverwriteDialog();
- bool backup() const;
- Action action() const { return m_Action; }
-private slots:
- void on_mergeBtn_clicked();
- void on_replaceBtn_clicked();
- void on_renameBtn_clicked();
- void on_cancelBtn_clicked();
-
-private:
- Ui::QueryOverwriteDialog *ui;
- Action m_Action;
-};
-
-#endif // QUERYOVERWRITEDIALOG_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 QUERYOVERWRITEDIALOG_H
+#define QUERYOVERWRITEDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+class QueryOverwriteDialog;
+}
+
+class QueryOverwriteDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ enum Action {
+ ACT_NONE,
+ ACT_MERGE,
+ ACT_REPLACE,
+ ACT_RENAME
+ };
+
+ enum Backup {
+ BACKUP_NO,
+ BACKUP_YES
+ };
+
+public:
+ QueryOverwriteDialog(QWidget *parent, Backup b);
+ ~QueryOverwriteDialog();
+ bool backup() const;
+ Action action() const { return m_Action; }
+private slots:
+ void on_mergeBtn_clicked();
+ void on_replaceBtn_clicked();
+ void on_renameBtn_clicked();
+ void on_cancelBtn_clicked();
+
+private:
+ Ui::QueryOverwriteDialog *ui;
+ Action m_Action;
+};
+
+#endif // QUERYOVERWRITEDIALOG_H
diff --git a/src/savetextasdialog.cpp b/src/savetextasdialog.cpp
index 8f0095d8..c2026ac4 100644
--- a/src/savetextasdialog.cpp
+++ b/src/savetextasdialog.cpp
@@ -1,50 +1,50 @@
-#include "savetextasdialog.h"
-#include "ui_savetextasdialog.h"
-#include <report.h>
-#include <QClipboard>
-#include <QFileDialog>
-
-
-using MOBase::reportError;
-
-
-SaveTextAsDialog::SaveTextAsDialog(QWidget *parent)
- : QDialog(parent), ui(new Ui::SaveTextAsDialog)
-{
- ui->setupUi(this);
-}
-
-SaveTextAsDialog::~SaveTextAsDialog()
-{
- delete ui;
-}
-
-void SaveTextAsDialog::setText(const QString &text)
-{
- ui->textEdit->setPlainText(text);
-}
-
-void SaveTextAsDialog::on_closeBtn_clicked()
-{
- this->close();
-}
-
-void SaveTextAsDialog::on_clipboardBtn_clicked()
-{
- QClipboard *clipboard = QApplication::clipboard();
- clipboard->setText(ui->textEdit->toPlainText());
-}
-
-void SaveTextAsDialog::on_saveAsBtn_clicked()
-{
- QString fileName = QFileDialog::getSaveFileName(this, tr("Save CSV"), QString(), tr("Text Files") + " (*.txt *.csv)");
- if (!fileName.isEmpty()) {
- QFile file(fileName);
- if (!file.open(QIODevice::WriteOnly)) {
- reportError(tr("failed to open \"%1\" for writing").arg(fileName));
- return;
- }
-
- file.write(ui->textEdit->toPlainText().toUtf8());
- }
-}
+#include "savetextasdialog.h"
+#include "ui_savetextasdialog.h"
+#include <report.h>
+#include <QClipboard>
+#include <QFileDialog>
+
+
+using MOBase::reportError;
+
+
+SaveTextAsDialog::SaveTextAsDialog(QWidget *parent)
+ : QDialog(parent), ui(new Ui::SaveTextAsDialog)
+{
+ ui->setupUi(this);
+}
+
+SaveTextAsDialog::~SaveTextAsDialog()
+{
+ delete ui;
+}
+
+void SaveTextAsDialog::setText(const QString &text)
+{
+ ui->textEdit->setPlainText(text);
+}
+
+void SaveTextAsDialog::on_closeBtn_clicked()
+{
+ this->close();
+}
+
+void SaveTextAsDialog::on_clipboardBtn_clicked()
+{
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText(ui->textEdit->toPlainText());
+}
+
+void SaveTextAsDialog::on_saveAsBtn_clicked()
+{
+ QString fileName = QFileDialog::getSaveFileName(this, tr("Save CSV"), QString(), tr("Text Files") + " (*.txt *.csv)");
+ if (!fileName.isEmpty()) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::WriteOnly)) {
+ reportError(tr("failed to open \"%1\" for writing").arg(fileName));
+ return;
+ }
+
+ file.write(ui->textEdit->toPlainText().toUtf8());
+ }
+}
diff --git a/src/savetextasdialog.h b/src/savetextasdialog.h
index 1a17a2db..d93bd21d 100644
--- a/src/savetextasdialog.h
+++ b/src/savetextasdialog.h
@@ -1,31 +1,31 @@
-#ifndef SAVETEXTASDIALOG_H
-#define SAVETEXTASDIALOG_H
-
-#include <QDialog>
-
-namespace Ui {
-class SaveTextAsDialog;
-}
-
-class SaveTextAsDialog : public QDialog
-{
- Q_OBJECT
-
-public:
- explicit SaveTextAsDialog(QWidget *parent = 0);
- ~SaveTextAsDialog();
-
- void setText(const QString &text);
-
-private slots:
- void on_closeBtn_clicked();
-
- void on_clipboardBtn_clicked();
-
- void on_saveAsBtn_clicked();
-
-private:
- Ui::SaveTextAsDialog *ui;
-};
-
-#endif // SAVETEXTASDIALOG_H
+#ifndef SAVETEXTASDIALOG_H
+#define SAVETEXTASDIALOG_H
+
+#include <QDialog>
+
+namespace Ui {
+class SaveTextAsDialog;
+}
+
+class SaveTextAsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit SaveTextAsDialog(QWidget *parent = 0);
+ ~SaveTextAsDialog();
+
+ void setText(const QString &text);
+
+private slots:
+ void on_closeBtn_clicked();
+
+ void on_clipboardBtn_clicked();
+
+ void on_saveAsBtn_clicked();
+
+private:
+ Ui::SaveTextAsDialog *ui;
+};
+
+#endif // SAVETEXTASDIALOG_H
diff --git a/src/selectiondialog.cpp b/src/selectiondialog.cpp
index 089760f9..c65ffaa9 100644
--- a/src/selectiondialog.cpp
+++ b/src/selectiondialog.cpp
@@ -1,109 +1,109 @@
-/*
-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 "selectiondialog.h"
-#include "ui_selectiondialog.h"
-
-#include <QCommandLinkButton>
-
-SelectionDialog::SelectionDialog(const QString &description, QWidget *parent, const QSize &iconSize)
- : QDialog(parent), ui(new Ui::SelectionDialog), m_Choice(nullptr), m_ValidateByData(false), m_IconSize(iconSize)
-{
- ui->setupUi(this);
-
- ui->descriptionLabel->setText(description);
-}
-
-SelectionDialog::~SelectionDialog()
-{
- delete ui;
-}
-
-void SelectionDialog::addChoice(const QString &buttonText, const QString &description, const QVariant &data)
-{
- QAbstractButton *button = new QCommandLinkButton(buttonText, description, ui->buttonBox);
- if (m_IconSize.isValid()) {
- button->setIconSize(m_IconSize);
- }
- button->setProperty("data", data);
- ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
- if (data.isValid()) m_ValidateByData = true;
-}
-
-void SelectionDialog::addChoice(const QIcon &icon, const QString &buttonText, const QString &description, const QVariant &data)
-{
- QAbstractButton *button = new QCommandLinkButton(buttonText, description, ui->buttonBox);
- if (m_IconSize.isValid()) {
- button->setIconSize(m_IconSize);
- }
- button->setIcon(icon);
- button->setProperty("data", data);
- ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
- if (data.isValid()) m_ValidateByData = true;
-}
-
-int SelectionDialog::numChoices() const
-{
- return ui->buttonBox->findChildren<QCommandLinkButton*>(QString()).count();
-}
-
-QVariant SelectionDialog::getChoiceData()
-{
- return m_Choice->property("data");
-}
-
-
-QString SelectionDialog::getChoiceString()
-{
- if ((m_Choice == nullptr) ||
- (m_ValidateByData && !m_Choice->property("data").isValid())) {
- return QString();
- } else {
- return m_Choice->text();
- }
-}
-
-QString SelectionDialog::getChoiceDescription()
-{
- if (m_Choice == nullptr)
- return QString();
- else
- return m_Choice->accessibleDescription();
-}
-
-void SelectionDialog::disableCancel()
-{
- ui->cancelButton->setEnabled(false);
- ui->cancelButton->setHidden(true);
-}
-
-void SelectionDialog::on_buttonBox_clicked(QAbstractButton *button)
-{
- m_Choice = button;
- if (!m_ValidateByData || m_Choice->property("data").isValid()) {
- this->accept();
- } else {
- this->reject();
- }
-}
-
-void SelectionDialog::on_cancelButton_clicked()
-{
- this->reject();
-}
+/*
+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 "selectiondialog.h"
+#include "ui_selectiondialog.h"
+
+#include <QCommandLinkButton>
+
+SelectionDialog::SelectionDialog(const QString &description, QWidget *parent, const QSize &iconSize)
+ : QDialog(parent), ui(new Ui::SelectionDialog), m_Choice(nullptr), m_ValidateByData(false), m_IconSize(iconSize)
+{
+ ui->setupUi(this);
+
+ ui->descriptionLabel->setText(description);
+}
+
+SelectionDialog::~SelectionDialog()
+{
+ delete ui;
+}
+
+void SelectionDialog::addChoice(const QString &buttonText, const QString &description, const QVariant &data)
+{
+ QAbstractButton *button = new QCommandLinkButton(buttonText, description, ui->buttonBox);
+ if (m_IconSize.isValid()) {
+ button->setIconSize(m_IconSize);
+ }
+ button->setProperty("data", data);
+ ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
+ if (data.isValid()) m_ValidateByData = true;
+}
+
+void SelectionDialog::addChoice(const QIcon &icon, const QString &buttonText, const QString &description, const QVariant &data)
+{
+ QAbstractButton *button = new QCommandLinkButton(buttonText, description, ui->buttonBox);
+ if (m_IconSize.isValid()) {
+ button->setIconSize(m_IconSize);
+ }
+ button->setIcon(icon);
+ button->setProperty("data", data);
+ ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
+ if (data.isValid()) m_ValidateByData = true;
+}
+
+int SelectionDialog::numChoices() const
+{
+ return ui->buttonBox->findChildren<QCommandLinkButton*>(QString()).count();
+}
+
+QVariant SelectionDialog::getChoiceData()
+{
+ return m_Choice->property("data");
+}
+
+
+QString SelectionDialog::getChoiceString()
+{
+ if ((m_Choice == nullptr) ||
+ (m_ValidateByData && !m_Choice->property("data").isValid())) {
+ return QString();
+ } else {
+ return m_Choice->text();
+ }
+}
+
+QString SelectionDialog::getChoiceDescription()
+{
+ if (m_Choice == nullptr)
+ return QString();
+ else
+ return m_Choice->accessibleDescription();
+}
+
+void SelectionDialog::disableCancel()
+{
+ ui->cancelButton->setEnabled(false);
+ ui->cancelButton->setHidden(true);
+}
+
+void SelectionDialog::on_buttonBox_clicked(QAbstractButton *button)
+{
+ m_Choice = button;
+ if (!m_ValidateByData || m_Choice->property("data").isValid()) {
+ this->accept();
+ } else {
+ this->reject();
+ }
+}
+
+void SelectionDialog::on_cancelButton_clicked()
+{
+ this->reject();
+}
diff --git a/src/selectiondialog.h b/src/selectiondialog.h
index 5a70aa5e..472baa19 100644
--- a/src/selectiondialog.h
+++ b/src/selectiondialog.h
@@ -1,74 +1,74 @@
-/*
-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 SELECTIONDIALOG_H
-#define SELECTIONDIALOG_H
-
-#include <QDialog>
-#include <QAbstractButton>
-
-namespace Ui {
-class SelectionDialog;
-}
-
-class SelectionDialog : public QDialog
-{
- Q_OBJECT
-
-public:
-
- explicit SelectionDialog(const QString &description, QWidget *parent = 0, const QSize &iconSize = QSize());
-
- ~SelectionDialog();
-
- /**
- * @brief add a choice to the dialog
- * @param buttonText the text to be displayed on the button
- * @param description the description that shows up under in small letters inside the button
- * @param data data to be stored with the button. Please note that as soon as one choice has data associated with it (non-invalid QVariant)
- * all buttons that contain no data will be treated as "cancel" buttons
- */
- void addChoice(const QString &buttonText, const QString &description, const QVariant &data);
-
- void addChoice(const QIcon &icon, const QString &buttonText, const QString &description, const QVariant &data);
-
- int numChoices() const;
-
- QVariant getChoiceData();
- QString getChoiceString();
- QString getChoiceDescription();
-
- void disableCancel();
-
-private slots:
-
- void on_buttonBox_clicked(QAbstractButton *button);
-
- void on_cancelButton_clicked();
-
-private:
-
- Ui::SelectionDialog *ui;
- QAbstractButton *m_Choice;
- bool m_ValidateByData;
- QSize m_IconSize;
-
-};
-
-#endif // SELECTIONDIALOG_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 SELECTIONDIALOG_H
+#define SELECTIONDIALOG_H
+
+#include <QDialog>
+#include <QAbstractButton>
+
+namespace Ui {
+class SelectionDialog;
+}
+
+class SelectionDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+
+ explicit SelectionDialog(const QString &description, QWidget *parent = 0, const QSize &iconSize = QSize());
+
+ ~SelectionDialog();
+
+ /**
+ * @brief add a choice to the dialog
+ * @param buttonText the text to be displayed on the button
+ * @param description the description that shows up under in small letters inside the button
+ * @param data data to be stored with the button. Please note that as soon as one choice has data associated with it (non-invalid QVariant)
+ * all buttons that contain no data will be treated as "cancel" buttons
+ */
+ void addChoice(const QString &buttonText, const QString &description, const QVariant &data);
+
+ void addChoice(const QIcon &icon, const QString &buttonText, const QString &description, const QVariant &data);
+
+ int numChoices() const;
+
+ QVariant getChoiceData();
+ QString getChoiceString();
+ QString getChoiceDescription();
+
+ void disableCancel();
+
+private slots:
+
+ void on_buttonBox_clicked(QAbstractButton *button);
+
+ void on_cancelButton_clicked();
+
+private:
+
+ Ui::SelectionDialog *ui;
+ QAbstractButton *m_Choice;
+ bool m_ValidateByData;
+ QSize m_IconSize;
+
+};
+
+#endif // SELECTIONDIALOG_H
diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp
index 1b24f3f1..50100faa 100644
--- a/src/selfupdater.cpp
+++ b/src/selfupdater.cpp
@@ -1,361 +1,361 @@
-/*
-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 "selfupdater.h"
-
-#include "utility.h"
-#include "iplugingame.h"
-#include "messagedialog.h"
-#include "downloadmanager.h"
-#include "nexusinterface.h"
-#include "nxmaccessmanager.h"
-#include "settings.h"
-#include "bbcode.h"
-#include "plugincontainer.h"
-#include "organizercore.h"
-#include <versioninfo.h>
-#include <report.h>
-#include "shared/util.h"
-#include "updatedialog.h"
-
-#include <QApplication>
-#include <QCoreApplication>
-#include <QDir>
-#include <QFileInfo>
-#include <QLibrary>
-#include <QMessageBox>
-#include <QNetworkAccessManager>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QProcess>
-#include <QProgressDialog>
-#include <QStringList>
-#include <QTimer>
-#include <QUrl>
-#include <QVariantList>
-#include <QVariantMap>
-#include <QAbstractButton>
-
-#include <Qt>
-#include <QtDebug>
-#include <QtAlgorithms>
-
-#include <boost/bind/bind.hpp>
-
-#include <Windows.h> //for VS_FIXEDFILEINFO, GetLastError
-
-#include <exception>
-#include <map>
-#include <stddef.h> //for size_t
-#include <stdexcept>
-
-using namespace MOBase;
-using namespace MOShared;
-
-SelfUpdater::SelfUpdater(NexusInterface *nexusInterface)
- : m_Parent(nullptr)
- , m_Interface(nexusInterface)
- , m_Reply(nullptr)
- , m_Attempts(3)
-{
- m_MOVersion = createVersionInfo();
-}
-
-
-SelfUpdater::~SelfUpdater()
-{
-}
-
-void SelfUpdater::setUserInterface(QWidget *widget)
-{
- m_Parent = widget;
-}
-
-void SelfUpdater::setPluginContainer(PluginContainer *pluginContainer)
-{
- m_Interface->setPluginContainer(pluginContainer);
-}
-
-void SelfUpdater::testForUpdate(const Settings& settings)
-{
- if (settings.network().offlineMode()) {
- log::debug("not checking for updates, in offline mode");
- return;
- }
-
- if (!settings.checkForUpdates()) {
- log::debug("not checking for updates, disabled");
- return;
- }
-
- // TODO: if prereleases are disabled we could just request the latest release
- // directly
- try {
- m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
- [this](const QJsonArray &releases) {
- if (releases.isEmpty()) {
- // error message already logged
- return;
- }
-
- // We store all releases:
- CandidatesMap mreleases;
- for (const QJsonValue &releaseVal : releases) {
- QJsonObject release = releaseVal.toObject();
- if (!release["draft"].toBool() && (Settings::instance().usePrereleases()
- || !release["prerelease"].toBool())) {
- auto version = VersionInfo(release["tag_name"].toString());
- mreleases[version] = release;
- }
- }
-
- if (!mreleases.empty()) {
- auto lastKey = mreleases.begin()->first;
- if (lastKey > this->m_MOVersion) {
-
- // Fill m_UpdateCandidates with version strictly greater than the
- // current version:
- m_UpdateCandidates.clear();
- for (auto p : mreleases) {
- if (p.first > this->m_MOVersion) {
- m_UpdateCandidates.insert(p);
- }
- }
- log::info("update available: {} -> {}",
- this->m_MOVersion.displayString(3),
- lastKey.displayString(3));
- emit updateAvailable();
- } else if (lastKey < this->m_MOVersion) {
- // this could happen if the user switches from using prereleases to
- // stable builds. Should we downgrade?
- log::debug("This version is newer than the latest released one: {} -> {}",
- this->m_MOVersion.displayString(3),
- lastKey.displayString(3));
- }
- }
- });
- }
- //Catch all is bad by design, should be improved
- catch (...) {
- log::debug("Unable to connect to github.com to check version");
- }
-}
-
-void SelfUpdater::startUpdate()
-{
- // the button can't be pressed if there isn't an update candidate
- Q_ASSERT(!m_UpdateCandidates.empty());
-
- auto latestRelease = m_UpdateCandidates.begin()->second;
-
- UpdateDialog dialog(m_Parent);
- dialog.setVersions(
- MOShared::createVersionInfo().displayString(3),
- latestRelease["tag_name"].toString()
- );
-
- // We concatenate release details. We only include pre-release if those are
- // the latest release:
- QString details;
- bool includePreRelease = true;
- for (auto& p : m_UpdateCandidates) {
- auto& release = p.second;
-
- // Ignore details for pre-release after a release has been found:
- if (release["prerelease"].toBool() && !includePreRelease) {
- continue;
- }
-
- // Stop including pre-release as soon as we find a non-prerelease:
- if (!release["prerelease"].toBool()) {
- includePreRelease = false;
- }
-
- details += "\n## " + release["name"].toString() + "\n---\n";
- details += release["body"].toString();
- }
-
- // Need to call setDetailedText to create the QTextEdit and then be able to retrieve it:
- dialog.setChangeLogs(details);
-
- int res = dialog.exec();
-
- if (dialog.result() == QDialog::Accepted) {
- bool found = false;
- for (const QJsonValue &assetVal : latestRelease["assets"].toArray()) {
- QJsonObject asset = assetVal.toObject();
- if (asset["content_type"].toString() == "application/x-msdownload") {
- openOutputFile(asset["name"].toString());
- download(asset["browser_download_url"].toString());
- found = true;
- break;
- }
- }
- if (!found) {
- QMessageBox::warning(
- m_Parent, tr("Download failed"),
- tr("Failed to find correct download, please try again later."));
- }
- }
-}
-
-
-void SelfUpdater::showProgress()
-{
- if (m_Progress == nullptr) {
- m_Progress = new QProgressDialog(m_Parent, Qt::Dialog);
- connect(m_Progress, SIGNAL(canceled()), this, SLOT(downloadCancel()));
- }
- m_Progress->setModal(true);
- m_Progress->show();
- m_Progress->setValue(0);
- m_Progress->setWindowTitle(tr("Update"));
- m_Progress->setLabelText(tr("Download in progress"));
-}
-
-void SelfUpdater::closeProgress()
-{
- if (m_Progress != nullptr) {
- m_Progress->hide();
- m_Progress->deleteLater();
- m_Progress = nullptr;
- }
-}
-
-void SelfUpdater::openOutputFile(const QString &fileName)
-{
- QString outputPath = QDir::fromNativeSeparators(qApp->property("dataPath").toString()) + "/" + fileName;
- log::debug("downloading to {}", outputPath);
- m_UpdateFile.setFileName(outputPath);
- m_UpdateFile.open(QIODevice::WriteOnly);
-}
-
-void SelfUpdater::download(const QString &downloadLink)
-{
- QNetworkAccessManager *accessManager = m_Interface->getAccessManager();
- QUrl dlUrl(downloadLink);
- QNetworkRequest request(dlUrl);
- m_Canceled = false;
- m_Reply = accessManager->get(request);
- showProgress();
-
- connect(m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64)));
- connect(m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
- connect(m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
-}
-
-
-void SelfUpdater::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
-{
- if (m_Reply != nullptr) {
- if (m_Canceled) {
- m_Reply->abort();
- } else {
- if (bytesTotal != 0) {
- if (m_Progress != nullptr) {
- m_Progress->setValue((bytesReceived * 100) / bytesTotal);
- }
- }
- }
- }
-}
-
-
-void SelfUpdater::downloadReadyRead()
-{
- if (m_Reply != nullptr) {
- m_UpdateFile.write(m_Reply->readAll());
- }
-}
-
-
-void SelfUpdater::downloadFinished()
-{
- int error = QNetworkReply::NoError;
-
- if (m_Reply != nullptr) {
- if (m_Reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 302) {
- QUrl url = m_Reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
- m_UpdateFile.reset();
- download(url.toString());
- return;
- }
- m_UpdateFile.write(m_Reply->readAll());
-
- error = m_Reply->error();
-
- if (m_Reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive)) {
- m_Canceled = true;
- }
-
- closeProgress();
-
- m_Reply->close();
- m_Reply->deleteLater();
- m_Reply = nullptr;
- }
-
- m_UpdateFile.close();
-
- if ((m_UpdateFile.size() == 0) ||
- (error != QNetworkReply::NoError) ||
- m_Canceled) {
- if (!m_Canceled) {
- reportError(tr("Download failed: %1").arg(error));
- }
- m_UpdateFile.remove();
- return;
- }
-
- log::debug("download: {}", m_UpdateFile.fileName());
-
- try {
- installUpdate();
- } catch (const std::exception &e) {
- reportError(tr("Failed to install update: %1").arg(e.what()));
- }
-}
-
-
-void SelfUpdater::downloadCancel()
-{
- m_Canceled = true;
-}
-
-
-void SelfUpdater::installUpdate()
-{
- const QString parameters = "/DIR=\"" + qApp->applicationDirPath() + "\" ";
- const auto r = shell::Execute(m_UpdateFile.fileName(), parameters);
-
- if (r.success()) {
- QCoreApplication::quit();
- } else {
- reportError(tr("Failed to start %1: %2")
- .arg(m_UpdateFile.fileName())
- .arg(r.toString()));
- }
-
- m_UpdateFile.remove();
-}
-
-void SelfUpdater::report7ZipError(QString const &errorMessage)
-{
- QMessageBox::critical(m_Parent, tr("Error"), errorMessage);
-}
+/*
+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 "selfupdater.h"
+
+#include "utility.h"
+#include "iplugingame.h"
+#include "messagedialog.h"
+#include "downloadmanager.h"
+#include "nexusinterface.h"
+#include "nxmaccessmanager.h"
+#include "settings.h"
+#include "bbcode.h"
+#include "plugincontainer.h"
+#include "organizercore.h"
+#include <versioninfo.h>
+#include <report.h>
+#include "shared/util.h"
+#include "updatedialog.h"
+
+#include <QApplication>
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QLibrary>
+#include <QMessageBox>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QProcess>
+#include <QProgressDialog>
+#include <QStringList>
+#include <QTimer>
+#include <QUrl>
+#include <QVariantList>
+#include <QVariantMap>
+#include <QAbstractButton>
+
+#include <Qt>
+#include <QtDebug>
+#include <QtAlgorithms>
+
+#include <boost/bind/bind.hpp>
+
+#include <Windows.h> //for VS_FIXEDFILEINFO, GetLastError
+
+#include <exception>
+#include <map>
+#include <stddef.h> //for size_t
+#include <stdexcept>
+
+using namespace MOBase;
+using namespace MOShared;
+
+SelfUpdater::SelfUpdater(NexusInterface *nexusInterface)
+ : m_Parent(nullptr)
+ , m_Interface(nexusInterface)
+ , m_Reply(nullptr)
+ , m_Attempts(3)
+{
+ m_MOVersion = createVersionInfo();
+}
+
+
+SelfUpdater::~SelfUpdater()
+{
+}
+
+void SelfUpdater::setUserInterface(QWidget *widget)
+{
+ m_Parent = widget;
+}
+
+void SelfUpdater::setPluginContainer(PluginContainer *pluginContainer)
+{
+ m_Interface->setPluginContainer(pluginContainer);
+}
+
+void SelfUpdater::testForUpdate(const Settings& settings)
+{
+ if (settings.network().offlineMode()) {
+ log::debug("not checking for updates, in offline mode");
+ return;
+ }
+
+ if (!settings.checkForUpdates()) {
+ log::debug("not checking for updates, disabled");
+ return;
+ }
+
+ // TODO: if prereleases are disabled we could just request the latest release
+ // directly
+ try {
+ m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
+ [this](const QJsonArray &releases) {
+ if (releases.isEmpty()) {
+ // error message already logged
+ return;
+ }
+
+ // We store all releases:
+ CandidatesMap mreleases;
+ for (const QJsonValue &releaseVal : releases) {
+ QJsonObject release = releaseVal.toObject();
+ if (!release["draft"].toBool() && (Settings::instance().usePrereleases()
+ || !release["prerelease"].toBool())) {
+ auto version = VersionInfo(release["tag_name"].toString());
+ mreleases[version] = release;
+ }
+ }
+
+ if (!mreleases.empty()) {
+ auto lastKey = mreleases.begin()->first;
+ if (lastKey > this->m_MOVersion) {
+
+ // Fill m_UpdateCandidates with version strictly greater than the
+ // current version:
+ m_UpdateCandidates.clear();
+ for (auto p : mreleases) {
+ if (p.first > this->m_MOVersion) {
+ m_UpdateCandidates.insert(p);
+ }
+ }
+ log::info("update available: {} -> {}",
+ this->m_MOVersion.displayString(3),
+ lastKey.displayString(3));
+ emit updateAvailable();
+ } else if (lastKey < this->m_MOVersion) {
+ // this could happen if the user switches from using prereleases to
+ // stable builds. Should we downgrade?
+ log::debug("This version is newer than the latest released one: {} -> {}",
+ this->m_MOVersion.displayString(3),
+ lastKey.displayString(3));
+ }
+ }
+ });
+ }
+ //Catch all is bad by design, should be improved
+ catch (...) {
+ log::debug("Unable to connect to github.com to check version");
+ }
+}
+
+void SelfUpdater::startUpdate()
+{
+ // the button can't be pressed if there isn't an update candidate
+ Q_ASSERT(!m_UpdateCandidates.empty());
+
+ auto latestRelease = m_UpdateCandidates.begin()->second;
+
+ UpdateDialog dialog(m_Parent);
+ dialog.setVersions(
+ MOShared::createVersionInfo().displayString(3),
+ latestRelease["tag_name"].toString()
+ );
+
+ // We concatenate release details. We only include pre-release if those are
+ // the latest release:
+ QString details;
+ bool includePreRelease = true;
+ for (auto& p : m_UpdateCandidates) {
+ auto& release = p.second;
+
+ // Ignore details for pre-release after a release has been found:
+ if (release["prerelease"].toBool() && !includePreRelease) {
+ continue;
+ }
+
+ // Stop including pre-release as soon as we find a non-prerelease:
+ if (!release["prerelease"].toBool()) {
+ includePreRelease = false;
+ }
+
+ details += "\n## " + release["name"].toString() + "\n---\n";
+ details += release["body"].toString();
+ }
+
+ // Need to call setDetailedText to create the QTextEdit and then be able to retrieve it:
+ dialog.setChangeLogs(details);
+
+ int res = dialog.exec();
+
+ if (dialog.result() == QDialog::Accepted) {
+ bool found = false;
+ for (const QJsonValue &assetVal : latestRelease["assets"].toArray()) {
+ QJsonObject asset = assetVal.toObject();
+ if (asset["content_type"].toString() == "application/x-msdownload") {
+ openOutputFile(asset["name"].toString());
+ download(asset["browser_download_url"].toString());
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ QMessageBox::warning(
+ m_Parent, tr("Download failed"),
+ tr("Failed to find correct download, please try again later."));
+ }
+ }
+}
+
+
+void SelfUpdater::showProgress()
+{
+ if (m_Progress == nullptr) {
+ m_Progress = new QProgressDialog(m_Parent, Qt::Dialog);
+ connect(m_Progress, SIGNAL(canceled()), this, SLOT(downloadCancel()));
+ }
+ m_Progress->setModal(true);
+ m_Progress->show();
+ m_Progress->setValue(0);
+ m_Progress->setWindowTitle(tr("Update"));
+ m_Progress->setLabelText(tr("Download in progress"));
+}
+
+void SelfUpdater::closeProgress()
+{
+ if (m_Progress != nullptr) {
+ m_Progress->hide();
+ m_Progress->deleteLater();
+ m_Progress = nullptr;
+ }
+}
+
+void SelfUpdater::openOutputFile(const QString &fileName)
+{
+ QString outputPath = QDir::fromNativeSeparators(qApp->property("dataPath").toString()) + "/" + fileName;
+ log::debug("downloading to {}", outputPath);
+ m_UpdateFile.setFileName(outputPath);
+ m_UpdateFile.open(QIODevice::WriteOnly);
+}
+
+void SelfUpdater::download(const QString &downloadLink)
+{
+ QNetworkAccessManager *accessManager = m_Interface->getAccessManager();
+ QUrl dlUrl(downloadLink);
+ QNetworkRequest request(dlUrl);
+ m_Canceled = false;
+ m_Reply = accessManager->get(request);
+ showProgress();
+
+ connect(m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64)));
+ connect(m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
+ connect(m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
+}
+
+
+void SelfUpdater::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ if (m_Reply != nullptr) {
+ if (m_Canceled) {
+ m_Reply->abort();
+ } else {
+ if (bytesTotal != 0) {
+ if (m_Progress != nullptr) {
+ m_Progress->setValue((bytesReceived * 100) / bytesTotal);
+ }
+ }
+ }
+ }
+}
+
+
+void SelfUpdater::downloadReadyRead()
+{
+ if (m_Reply != nullptr) {
+ m_UpdateFile.write(m_Reply->readAll());
+ }
+}
+
+
+void SelfUpdater::downloadFinished()
+{
+ int error = QNetworkReply::NoError;
+
+ if (m_Reply != nullptr) {
+ if (m_Reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 302) {
+ QUrl url = m_Reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ m_UpdateFile.reset();
+ download(url.toString());
+ return;
+ }
+ m_UpdateFile.write(m_Reply->readAll());
+
+ error = m_Reply->error();
+
+ if (m_Reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive)) {
+ m_Canceled = true;
+ }
+
+ closeProgress();
+
+ m_Reply->close();
+ m_Reply->deleteLater();
+ m_Reply = nullptr;
+ }
+
+ m_UpdateFile.close();
+
+ if ((m_UpdateFile.size() == 0) ||
+ (error != QNetworkReply::NoError) ||
+ m_Canceled) {
+ if (!m_Canceled) {
+ reportError(tr("Download failed: %1").arg(error));
+ }
+ m_UpdateFile.remove();
+ return;
+ }
+
+ log::debug("download: {}", m_UpdateFile.fileName());
+
+ try {
+ installUpdate();
+ } catch (const std::exception &e) {
+ reportError(tr("Failed to install update: %1").arg(e.what()));
+ }
+}
+
+
+void SelfUpdater::downloadCancel()
+{
+ m_Canceled = true;
+}
+
+
+void SelfUpdater::installUpdate()
+{
+ const QString parameters = "/DIR=\"" + qApp->applicationDirPath() + "\" ";
+ const auto r = shell::Execute(m_UpdateFile.fileName(), parameters);
+
+ if (r.success()) {
+ QCoreApplication::quit();
+ } else {
+ reportError(tr("Failed to start %1: %2")
+ .arg(m_UpdateFile.fileName())
+ .arg(r.toString()));
+ }
+
+ m_UpdateFile.remove();
+}
+
+void SelfUpdater::report7ZipError(QString const &errorMessage)
+{
+ QMessageBox::critical(m_Parent, tr("Error"), errorMessage);
+}
diff --git a/src/selfupdater.h b/src/selfupdater.h
index f7637292..baf830d1 100644
--- a/src/selfupdater.h
+++ b/src/selfupdater.h
@@ -1,153 +1,153 @@
-/*
-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 SELFUPDATER_H
-#define SELFUPDATER_H
-
-#include <map>
-
-#include <versioninfo.h>
-#include <github.h>
-
-class Archive;
-class NexusInterface;
-class PluginContainer;
-namespace MOBase { class IPluginGame; }
-
-#include <QFile>
-#include <QObject>
-#include <QString>
-#include <QVariant>
-#include <QtGlobal> //for qint64
-
-class QNetworkReply;
-class QProgressDialog;
-class Settings;
-
-/**
- * @brief manages updates for Mod Organizer itself
- * This class is used to update the Mod Organizer
- * The process looks like this:
- * 1. call testForUpdate() to determine is available
- * 2. if the updateAvailable() signal is received, allow the user to start the update
- * 3. if the user start the update, call startUpdate()
- * 4. startUpdate() will first query a list of files, try to determine if there is an
- * incremental update. If not, the user will have to confirm the download of a full download.
- * Once the correct file is selected, it is downloaded.
- * 5. before the downloaded file is extracted, existing files that are going to be replaced are
- * moved to "update_backup" on because files that are currently open can't be replaced.
- * 6. the update is extracted and then deleted
- * 7. finally, a restart is requested via signal.
- * 8. at restart, Mod Organizer will remove the update_backup directory since none of the files
- * should now be open
- *
- * @todo use NexusBridge
- **/
-class SelfUpdater : public QObject
-{
-
- Q_OBJECT
-
-public:
-
- /**
- * @brief constructor
- *
- * @param nexusInterface interface to query information from nexus
- * @param parent parent widget
- * @todo passing the nexus interface is unneccessary
- **/
- explicit SelfUpdater(NexusInterface *nexusInterface);
-
- virtual ~SelfUpdater();
-
- void setUserInterface(QWidget *widget);
-
- void setPluginContainer(PluginContainer *pluginContainer);
-
- /**
- * @brief request information about the current version
- **/
- void testForUpdate(const Settings& settings);
-
- /**
- * @brief start the update process
- * @note this should not be called if there is no update available
- **/
- void startUpdate();
-
- /**
- * @return current version of Mod Organizer
- **/
- MOBase::VersionInfo getVersion() const { return m_MOVersion; }
-
-signals:
-
- /**
- * @brief emitted if a restart of the client is necessary to complete the update
- **/
- void restart();
-
- /**
- * @brief emitted if an update is available
- **/
- void updateAvailable();
-
- /**
- * @brief emitted if a message of the day was received
- **/
- void motdAvailable(const QString &motd);
-
-private:
-
- void openOutputFile(const QString &fileName);
- void download(const QString &downloadLink);
- void installUpdate();
- void report7ZipError(const QString &errorMessage);
- void showProgress();
- void closeProgress();
-
-private slots:
-
- void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
- void downloadReadyRead();
- void downloadFinished();
- void downloadCancel();
-
-private:
-
- QWidget *m_Parent;
- MOBase::VersionInfo m_MOVersion;
- NexusInterface *m_Interface;
- QFile m_UpdateFile;
- QNetworkReply *m_Reply;
- QProgressDialog *m_Progress { nullptr };
- bool m_Canceled;
- int m_Attempts;
-
- GitHub m_GitHub;
-
- // Map from version to release, in decreasing order (first element is the latest release):
- using CandidatesMap = std::map<MOBase::VersionInfo, QJsonObject, std::greater<>>;
- CandidatesMap m_UpdateCandidates;
-
-};
-
-
-#endif // SELFUPDATER_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 SELFUPDATER_H
+#define SELFUPDATER_H
+
+#include <map>
+
+#include <versioninfo.h>
+#include <github.h>
+
+class Archive;
+class NexusInterface;
+class PluginContainer;
+namespace MOBase { class IPluginGame; }
+
+#include <QFile>
+#include <QObject>
+#include <QString>
+#include <QVariant>
+#include <QtGlobal> //for qint64
+
+class QNetworkReply;
+class QProgressDialog;
+class Settings;
+
+/**
+ * @brief manages updates for Mod Organizer itself
+ * This class is used to update the Mod Organizer
+ * The process looks like this:
+ * 1. call testForUpdate() to determine is available
+ * 2. if the updateAvailable() signal is received, allow the user to start the update
+ * 3. if the user start the update, call startUpdate()
+ * 4. startUpdate() will first query a list of files, try to determine if there is an
+ * incremental update. If not, the user will have to confirm the download of a full download.
+ * Once the correct file is selected, it is downloaded.
+ * 5. before the downloaded file is extracted, existing files that are going to be replaced are
+ * moved to "update_backup" on because files that are currently open can't be replaced.
+ * 6. the update is extracted and then deleted
+ * 7. finally, a restart is requested via signal.
+ * 8. at restart, Mod Organizer will remove the update_backup directory since none of the files
+ * should now be open
+ *
+ * @todo use NexusBridge
+ **/
+class SelfUpdater : public QObject
+{
+
+ Q_OBJECT
+
+public:
+
+ /**
+ * @brief constructor
+ *
+ * @param nexusInterface interface to query information from nexus
+ * @param parent parent widget
+ * @todo passing the nexus interface is unneccessary
+ **/
+ explicit SelfUpdater(NexusInterface *nexusInterface);
+
+ virtual ~SelfUpdater();
+
+ void setUserInterface(QWidget *widget);
+
+ void setPluginContainer(PluginContainer *pluginContainer);
+
+ /**
+ * @brief request information about the current version
+ **/
+ void testForUpdate(const Settings& settings);
+
+ /**
+ * @brief start the update process
+ * @note this should not be called if there is no update available
+ **/
+ void startUpdate();
+
+ /**
+ * @return current version of Mod Organizer
+ **/
+ MOBase::VersionInfo getVersion() const { return m_MOVersion; }
+
+signals:
+
+ /**
+ * @brief emitted if a restart of the client is necessary to complete the update
+ **/
+ void restart();
+
+ /**
+ * @brief emitted if an update is available
+ **/
+ void updateAvailable();
+
+ /**
+ * @brief emitted if a message of the day was received
+ **/
+ void motdAvailable(const QString &motd);
+
+private:
+
+ void openOutputFile(const QString &fileName);
+ void download(const QString &downloadLink);
+ void installUpdate();
+ void report7ZipError(const QString &errorMessage);
+ void showProgress();
+ void closeProgress();
+
+private slots:
+
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void downloadReadyRead();
+ void downloadFinished();
+ void downloadCancel();
+
+private:
+
+ QWidget *m_Parent;
+ MOBase::VersionInfo m_MOVersion;
+ NexusInterface *m_Interface;
+ QFile m_UpdateFile;
+ QNetworkReply *m_Reply;
+ QProgressDialog *m_Progress { nullptr };
+ bool m_Canceled;
+ int m_Attempts;
+
+ GitHub m_GitHub;
+
+ // Map from version to release, in decreasing order (first element is the latest release):
+ using CandidatesMap = std::map<MOBase::VersionInfo, QJsonObject, std::greater<>>;
+ CandidatesMap m_UpdateCandidates;
+
+};
+
+
+#endif // SELFUPDATER_H
diff --git a/src/serverinfo.cpp b/src/serverinfo.cpp
index aece61da..8d8c98fe 100644
--- a/src/serverinfo.cpp
+++ b/src/serverinfo.cpp
@@ -1,178 +1,178 @@
-#include "serverinfo.h"
-#include "log.h"
-
-using namespace MOBase;
-
-const std::size_t MaxDownloadCount = 5;
-
-
-ServerInfo::ServerInfo()
- : ServerInfo({}, false, {}, 0, {})
-{
-}
-
-ServerInfo::ServerInfo(
- QString name, bool premium, QDate last, int preferred,
- SpeedList lastDownloads) :
- m_name(std::move(name)), m_premium(premium), m_lastSeen(std::move(last)),
- m_preferred(preferred), m_lastDownloads(std::move(lastDownloads))
-{
- if (m_lastDownloads.size() > MaxDownloadCount) {
- m_lastDownloads.resize(MaxDownloadCount);
- }
-}
-
-const QString& ServerInfo::name() const
-{
- return m_name;
-}
-
-bool ServerInfo::isPremium() const
-{
- return m_premium;
-}
-
-void ServerInfo::setPremium(bool b)
-{
- m_premium = b;
-}
-
-const QDate& ServerInfo::lastSeen() const
-{
- return m_lastSeen;
-}
-
-void ServerInfo::updateLastSeen()
-{
- m_lastSeen = QDate::currentDate();
-}
-
-int ServerInfo::preferred() const
-{
- return m_preferred;
-}
-
-void ServerInfo::setPreferred(int i)
-{
- m_preferred = i;
-}
-
-const ServerInfo::SpeedList& ServerInfo::lastDownloads() const
-{
- return m_lastDownloads;
-}
-
-int ServerInfo::averageSpeed() const
-{
- int count = 0;
- int total = 0;
-
- for (const auto& s : m_lastDownloads) {
- if (s > 0) {
- ++count;
- total += s;
- }
- }
-
- if (count > 0) {
- return static_cast<double>(total) / count;
- }
-
- return 0;
-}
-
-void ServerInfo::addDownload(int bytesPerSecond)
-{
- if (bytesPerSecond <= 0) {
- log::error(
- "trying to add download with {} B/s to server '{}'; ignoring",
- bytesPerSecond, m_name);
-
- return;
- }
-
- if (m_lastDownloads.size() == MaxDownloadCount) {
- std::rotate(
- m_lastDownloads.begin(),
- m_lastDownloads.begin() + 1,
- m_lastDownloads.end());
-
- m_lastDownloads.back() = bytesPerSecond;
- } else {
- m_lastDownloads.push_back(bytesPerSecond);
- }
-
- log::debug("added download at {} B/s to server '{}'", bytesPerSecond, m_name);
-}
-
-
-void ServerList::add(ServerInfo s)
-{
- m_servers.push_back(std::move(s));
-
- std::sort(m_servers.begin(), m_servers.end(), [](auto&& a, auto&& b){
- return (a.preferred() < b.preferred());
- });
-}
-
-ServerList::iterator ServerList::begin()
-{
- return m_servers.begin();
-}
-
-ServerList::const_iterator ServerList::begin() const
-{
- return m_servers.begin();
-}
-
-ServerList::iterator ServerList::end()
-{
- return m_servers.end();
-}
-
-ServerList::const_iterator ServerList::end() const
-{
- return m_servers.end();
-}
-
-std::size_t ServerList::size() const
-{
- return m_servers.size();
-}
-
-bool ServerList::empty() const
-{
- return m_servers.empty();
-}
-
-ServerList::container ServerList::getPreferred() const
-{
- container v;
-
- for (const auto& server : m_servers) {
- if (server.preferred() > 0) {
- v.push_back(server);
- }
- }
-
- return v;
-}
-
-void ServerList::cleanup()
-{
- QDate now = QDate::currentDate();
-
- for (auto itor=m_servers.begin(); itor!=m_servers.end(); ) {
- const QDate lastSeen = itor->lastSeen();
-
- if (lastSeen.daysTo(now) > 30) {
- log::debug(
- "removing server {} since it hasn't been available for downloads "
- "in over a month", itor->name());
-
- itor = m_servers.erase(itor);
- } else {
- ++itor;
- }
- }
-}
+#include "serverinfo.h"
+#include "log.h"
+
+using namespace MOBase;
+
+const std::size_t MaxDownloadCount = 5;
+
+
+ServerInfo::ServerInfo()
+ : ServerInfo({}, false, {}, 0, {})
+{
+}
+
+ServerInfo::ServerInfo(
+ QString name, bool premium, QDate last, int preferred,
+ SpeedList lastDownloads) :
+ m_name(std::move(name)), m_premium(premium), m_lastSeen(std::move(last)),
+ m_preferred(preferred), m_lastDownloads(std::move(lastDownloads))
+{
+ if (m_lastDownloads.size() > MaxDownloadCount) {
+ m_lastDownloads.resize(MaxDownloadCount);
+ }
+}
+
+const QString& ServerInfo::name() const
+{
+ return m_name;
+}
+
+bool ServerInfo::isPremium() const
+{
+ return m_premium;
+}
+
+void ServerInfo::setPremium(bool b)
+{
+ m_premium = b;
+}
+
+const QDate& ServerInfo::lastSeen() const
+{
+ return m_lastSeen;
+}
+
+void ServerInfo::updateLastSeen()
+{
+ m_lastSeen = QDate::currentDate();
+}
+
+int ServerInfo::preferred() const
+{
+ return m_preferred;
+}
+
+void ServerInfo::setPreferred(int i)
+{
+ m_preferred = i;
+}
+
+const ServerInfo::SpeedList& ServerInfo::lastDownloads() const
+{
+ return m_lastDownloads;
+}
+
+int ServerInfo::averageSpeed() const
+{
+ int count = 0;
+ int total = 0;
+
+ for (const auto& s : m_lastDownloads) {
+ if (s > 0) {
+ ++count;
+ total += s;
+ }
+ }
+
+ if (count > 0) {
+ return static_cast<double>(total) / count;
+ }
+
+ return 0;
+}
+
+void ServerInfo::addDownload(int bytesPerSecond)
+{
+ if (bytesPerSecond <= 0) {
+ log::error(
+ "trying to add download with {} B/s to server '{}'; ignoring",
+ bytesPerSecond, m_name);
+
+ return;
+ }
+
+ if (m_lastDownloads.size() == MaxDownloadCount) {
+ std::rotate(
+ m_lastDownloads.begin(),
+ m_lastDownloads.begin() + 1,
+ m_lastDownloads.end());
+
+ m_lastDownloads.back() = bytesPerSecond;
+ } else {
+ m_lastDownloads.push_back(bytesPerSecond);
+ }
+
+ log::debug("added download at {} B/s to server '{}'", bytesPerSecond, m_name);
+}
+
+
+void ServerList::add(ServerInfo s)
+{
+ m_servers.push_back(std::move(s));
+
+ std::sort(m_servers.begin(), m_servers.end(), [](auto&& a, auto&& b){
+ return (a.preferred() < b.preferred());
+ });
+}
+
+ServerList::iterator ServerList::begin()
+{
+ return m_servers.begin();
+}
+
+ServerList::const_iterator ServerList::begin() const
+{
+ return m_servers.begin();
+}
+
+ServerList::iterator ServerList::end()
+{
+ return m_servers.end();
+}
+
+ServerList::const_iterator ServerList::end() const
+{
+ return m_servers.end();
+}
+
+std::size_t ServerList::size() const
+{
+ return m_servers.size();
+}
+
+bool ServerList::empty() const
+{
+ return m_servers.empty();
+}
+
+ServerList::container ServerList::getPreferred() const
+{
+ container v;
+
+ for (const auto& server : m_servers) {
+ if (server.preferred() > 0) {
+ v.push_back(server);
+ }
+ }
+
+ return v;
+}
+
+void ServerList::cleanup()
+{
+ QDate now = QDate::currentDate();
+
+ for (auto itor=m_servers.begin(); itor!=m_servers.end(); ) {
+ const QDate lastSeen = itor->lastSeen();
+
+ if (lastSeen.daysTo(now) > 30) {
+ log::debug(
+ "removing server {} since it hasn't been available for downloads "
+ "in over a month", itor->name());
+
+ itor = m_servers.erase(itor);
+ } else {
+ ++itor;
+ }
+ }
+}
diff --git a/src/serverinfo.h b/src/serverinfo.h
index af8f77c8..0bec89cc 100644
--- a/src/serverinfo.h
+++ b/src/serverinfo.h
@@ -1,70 +1,70 @@
-#ifndef SERVERINFO_H
-#define SERVERINFO_H
-
-#include <QString>
-#include <QDate>
-#include <QMetaType>
-
-class ServerInfo
-{
-public:
- using SpeedList = std::vector<int>;
-
- ServerInfo();
- ServerInfo(
- QString name, bool premium, QDate lastSeen, int preferred,
- SpeedList lastDownloads);
-
- const QString& name() const;
-
- bool isPremium() const;
- void setPremium(bool b);
-
- const QDate& lastSeen() const;
- void updateLastSeen();
-
- int preferred() const;
- void setPreferred(int i);
-
- const SpeedList& lastDownloads() const;
- int averageSpeed() const;
- void addDownload(int bytesPerSecond);
-
-private:
- QString m_name;
- bool m_premium;
- QDate m_lastSeen;
- int m_preferred;
- SpeedList m_lastDownloads;
-};
-
-Q_DECLARE_METATYPE(ServerInfo)
-
-
-class ServerList
-{
-public:
- using container = std::vector<ServerInfo>;
- using iterator = container::iterator;
- using const_iterator = container::const_iterator;
-
- void add(ServerInfo s);
-
- iterator begin();
- const_iterator begin() const;
- iterator end();
- const_iterator end() const;
- std::size_t size() const;
- bool empty() const;
-
- container getPreferred() const;
-
- // removes servers that haven't been seen in a while
- //
- void cleanup();
-
-private:
- container m_servers;
-};
-
-#endif // SERVERINFO_H
+#ifndef SERVERINFO_H
+#define SERVERINFO_H
+
+#include <QString>
+#include <QDate>
+#include <QMetaType>
+
+class ServerInfo
+{
+public:
+ using SpeedList = std::vector<int>;
+
+ ServerInfo();
+ ServerInfo(
+ QString name, bool premium, QDate lastSeen, int preferred,
+ SpeedList lastDownloads);
+
+ const QString& name() const;
+
+ bool isPremium() const;
+ void setPremium(bool b);
+
+ const QDate& lastSeen() const;
+ void updateLastSeen();
+
+ int preferred() const;
+ void setPreferred(int i);
+
+ const SpeedList& lastDownloads() const;
+ int averageSpeed() const;
+ void addDownload(int bytesPerSecond);
+
+private:
+ QString m_name;
+ bool m_premium;
+ QDate m_lastSeen;
+ int m_preferred;
+ SpeedList m_lastDownloads;
+};
+
+Q_DECLARE_METATYPE(ServerInfo)
+
+
+class ServerList
+{
+public:
+ using container = std::vector<ServerInfo>;
+ using iterator = container::iterator;
+ using const_iterator = container::const_iterator;
+
+ void add(ServerInfo s);
+
+ iterator begin();
+ const_iterator begin() const;
+ iterator end();
+ const_iterator end() const;
+ std::size_t size() const;
+ bool empty() const;
+
+ container getPreferred() const;
+
+ // removes servers that haven't been seen in a while
+ //
+ void cleanup();
+
+private:
+ container m_servers;
+};
+
+#endif // SERVERINFO_H
diff --git a/src/shared/appconfig.cpp b/src/shared/appconfig.cpp
index 49edcbcc..d2fc386c 100644
--- a/src/shared/appconfig.cpp
+++ b/src/shared/appconfig.cpp
@@ -1,33 +1,33 @@
-/*
-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 "appconfig.h"
-
-namespace AppConfig {
-
-#define PARWSTRING wstring
-#define APPPARAM(partype, parid, value) partype parid () { return value; }
-#include "appconfig.inc"
-
-namespace MOShared {
-#undef PARWSTRING
-#undef APPPARAM
-
-}
-} // namespace MOShared
+/*
+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 "appconfig.h"
+
+namespace AppConfig {
+
+#define PARWSTRING wstring
+#define APPPARAM(partype, parid, value) partype parid () { return value; }
+#include "appconfig.inc"
+
+namespace MOShared {
+#undef PARWSTRING
+#undef APPPARAM
+
+}
+} // namespace MOShared
diff --git a/src/shared/appconfig.h b/src/shared/appconfig.h
index b32043e5..cca5b0f5 100644
--- a/src/shared/appconfig.h
+++ b/src/shared/appconfig.h
@@ -1,40 +1,40 @@
-/*
-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 APPCONFIG_H
-#define APPCONFIG_H
-
-#include <string>
-
-namespace AppConfig {
-
-
-#define PARWSTRING wstring
-#define APPPARAM(partype, parid, value) partype parid ();
-#include "appconfig.inc"
-
-namespace MOShared {
-#undef PARWSTRING
-#undef APPPARAM
-
-}
-
-} // namespace MOShared
-
-#endif // APPCONFIG_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 APPCONFIG_H
+#define APPCONFIG_H
+
+#include <string>
+
+namespace AppConfig {
+
+
+#define PARWSTRING wstring
+#define APPPARAM(partype, parid, value) partype parid ();
+#include "appconfig.inc"
+
+namespace MOShared {
+#undef PARWSTRING
+#undef APPPARAM
+
+}
+
+} // namespace MOShared
+
+#endif // APPCONFIG_H
diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp
index badb5636..48ff7aa3 100644
--- a/src/shared/directoryentry.cpp
+++ b/src/shared/directoryentry.cpp
@@ -1,978 +1,978 @@
-/*
-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 "directoryentry.h"
-#include "originconnection.h"
-#include "filesorigin.h"
-#include "fileentry.h"
-#include "../envfs.h"
-#include "util.h"
-#include "windows_error.h"
-#include <log.h>
-#include <utility.h>
-
-namespace MOShared
-{
-
-using namespace MOBase;
-const int MAXPATH_UNICODE = 32767;
-
-template <class F>
-void elapsedImpl(std::chrono::nanoseconds& out, F&& f)
-{
- if constexpr (DirectoryStats::EnableInstrumentation) {
- const auto start = std::chrono::high_resolution_clock::now();
- f();
- const auto end = std::chrono::high_resolution_clock::now();
- out += (end - start);
- } else {
- f();
- }
-}
-
-// elapsed() is not optimized out when EnableInstrumentation is false even
-// though it's equivalent that this macro
-#define elapsed(OUT, F) (F)();
-//#define elapsed(OUT, F) elapsedImpl(OUT, F);
-
-static bool SupportOptimizedFind()
-{
- // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2, win 7 and newer
-
- OSVERSIONINFOEX versionInfo;
- versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
- versionInfo.dwMajorVersion = 6;
- versionInfo.dwMinorVersion = 1;
-
- ULONGLONG mask = ::VerSetConditionMask(
- ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL),
- VER_MINORVERSION, VER_GREATER_EQUAL);
-
- return (::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, mask) == TRUE);
-}
-
-bool DirCompareByName::operator()(
- const DirectoryEntry* lhs, const DirectoryEntry* rhs) const
-{
- return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
-}
-
-
-DirectoryEntry::DirectoryEntry(
- std::wstring name, DirectoryEntry* parent, int originID) :
- m_OriginConnection(new OriginConnection),
- m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(true)
-{
- m_FileRegister.reset(new FileRegister(m_OriginConnection));
- m_Origins.insert(originID);
-}
-
-DirectoryEntry::DirectoryEntry(
- std::wstring name, DirectoryEntry* parent, int originID,
- boost::shared_ptr<FileRegister> fileRegister,
- boost::shared_ptr<OriginConnection> originConnection) :
- m_FileRegister(fileRegister), m_OriginConnection(originConnection),
- m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(false)
-{
- m_Origins.insert(originID);
-}
-
-DirectoryEntry::~DirectoryEntry()
-{
- clear();
-}
-
-void DirectoryEntry::clear()
-{
- for (auto itor=m_SubDirectories.rbegin(); itor!=m_SubDirectories.rend(); ++itor) {
- delete *itor;
- }
-
- m_Files.clear();
- m_FilesLookup.clear();
- m_SubDirectories.clear();
- m_SubDirectoriesLookup.clear();
-}
-
-void DirectoryEntry::addFromOrigin(
- const std::wstring &originName, const std::wstring &directory, int priority,
- DirectoryStats& stats)
-{
- env::DirectoryWalker walker;
- addFromOrigin(walker, originName, directory, priority, stats);
-}
-
-void DirectoryEntry::addFromOrigin(
- env::DirectoryWalker& walker, const std::wstring &originName,
- const std::wstring &directory, int priority, DirectoryStats& stats)
-{
- FilesOrigin &origin = createOrigin(originName, directory, priority, stats);
-
- if (!directory.empty()) {
- addFiles(walker, origin, directory, stats);
- }
-
- m_Populated = true;
-}
-
-void DirectoryEntry::addFromList(
- const std::wstring &originName, const std::wstring &directory,
- env::Directory& root, int priority, DirectoryStats& stats)
-{
- stats = {};
-
- FilesOrigin &origin = createOrigin(originName, directory, priority, stats);
- addDir(origin, root, stats);
-}
-
-void DirectoryEntry::addDir(
- FilesOrigin& origin, env::Directory& d, DirectoryStats& stats)
-{
- elapsed(stats.dirTimes, [&]{
- for (auto& sd : d.dirs) {
- auto* sdirEntry = getSubDirectory(sd, true, stats, origin.getID());
- sdirEntry->addDir(origin, sd, stats);
- }
- });
-
- elapsed(stats.fileTimes, [&]{
- for (auto& f : d.files) {
- insert(f, origin, L"", -1, stats);
- }
- });
-
- m_Populated = true;
-}
-
-void DirectoryEntry::addFromAllBSAs(
- const std::wstring& originName, const std::wstring& directory,
- int priority, const std::vector<std::wstring>& archives,
- const std::set<std::wstring>& enabledArchives,
- const std::vector<std::wstring>& loadOrder,
- DirectoryStats& stats)
-{
- for (const auto& archive : archives) {
- const std::filesystem::path archivePath(archive);
- const auto filename = archivePath.filename().native();
-
- if (!enabledArchives.contains(filename)) {
- continue;
- }
-
- const auto filenameLc = ToLowerCopy(filename);
-
- int order = -1;
-
- for (auto plugin : loadOrder)
- {
- const auto pluginNameLc =
- ToLowerCopy(std::filesystem::path(plugin).stem().native());
-
- if (filenameLc.starts_with(pluginNameLc + L" - ") ||
- filenameLc.starts_with(pluginNameLc + L".")) {
- auto itor = std::find(loadOrder.begin(), loadOrder.end(), plugin);
- if (itor != loadOrder.end()) {
- order = std::distance(loadOrder.begin(), itor);
- }
- }
- }
-
- addFromBSA(
- originName, directory, archivePath.native(),
- priority, order, stats);
- }
-}
-
-void DirectoryEntry::addFromBSA(
- const std::wstring& originName, const std::wstring& directory,
- const std::wstring& archivePath, int priority, int order, DirectoryStats& stats)
-{
- FilesOrigin& origin = createOrigin(originName, directory, priority, stats);
- const auto archiveName = std::filesystem::path(archivePath).filename().native();
-
- if (containsArchive(archiveName)) {
- return;
- }
-
- BSA::Archive archive;
- BSA::EErrorCode res = BSA::ERROR_NONE;
-
- try
- {
- // read() can return an error, but it can also throw if the file is not a
- // valid bsa
- res = archive.read(ToString(archivePath, false).c_str(), false);
- }
- catch(std::exception& e)
- {
- log::error("invalid bsa '{}', error {}", archivePath, e.what());
- return;
- }
-
- if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
- log::error("invalid bsa '{}', error {}", archivePath, res);
- return;
- }
-
- std::error_code ec;
- const auto lwt = std::filesystem::last_write_time(archivePath, ec);
- FILETIME ft = {};
-
- if (ec) {
- log::warn(
- "failed to get last modified date for '{}', {}",
- archivePath, ec.message());
- } else {
- ft = ToFILETIME(lwt);
- }
-
- addFiles(origin, archive.getRoot(), ft, archiveName, order, stats);
-
- m_Populated = true;
-}
-
-void DirectoryEntry::propagateOrigin(int origin)
-{
- {
- std::scoped_lock lock(m_OriginsMutex);
- m_Origins.insert(origin);
- }
-
- if (m_Parent != nullptr) {
- m_Parent->propagateOrigin(origin);
- }
-}
-
-bool DirectoryEntry::originExists(const std::wstring &name) const
-{
- return m_OriginConnection->exists(name);
-}
-
-FilesOrigin &DirectoryEntry::getOriginByID(int ID) const
-{
- return m_OriginConnection->getByID(ID);
-}
-
-FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const
-{
- return m_OriginConnection->getByName(name);
-}
-
-const FilesOrigin* DirectoryEntry::findOriginByID(int ID) const
-{
- return m_OriginConnection->findByID(ID);
-}
-
-int DirectoryEntry::anyOrigin() const
-{
- bool ignore;
-
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- FileEntryPtr entry = m_FileRegister->getFile(iter->second);
- if ((entry.get() != nullptr) && !entry->isFromArchive()) {
- return entry->getOrigin(ignore);
- }
- }
-
- // if we got here, no file directly within this directory is a valid indicator for a mod, thus
- // we continue looking in subdirectories
- for (DirectoryEntry* entry : m_SubDirectories) {
- int res = entry->anyOrigin();
- if (res != InvalidOriginID){
- return res;
- }
- }
-
- return *(m_Origins.begin());
-}
-
-std::vector<FileEntryPtr> DirectoryEntry::getFiles() const
-{
- std::vector<FileEntryPtr> result;
-
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- result.push_back(m_FileRegister->getFile(iter->second));
- }
-
- return result;
-}
-
-DirectoryEntry* DirectoryEntry::findSubDirectory(
- const std::wstring &name, bool alreadyLowerCase) const
-{
- SubDirectoriesLookup::const_iterator itor;
-
- if (alreadyLowerCase) {
- itor = m_SubDirectoriesLookup.find(name);
- } else {
- itor = m_SubDirectoriesLookup.find(ToLowerCopy(name));
- }
-
- if (itor == m_SubDirectoriesLookup.end()) {
- return nullptr;
- }
-
- return itor->second;
-}
-
-DirectoryEntry* DirectoryEntry::findSubDirectoryRecursive(const std::wstring &path)
-{
- DirectoryStats dummy;
- return getSubDirectoryRecursive(path, false, dummy, InvalidOriginID);
-}
-
-const FileEntryPtr DirectoryEntry::findFile(
- const std::wstring &name, bool alreadyLowerCase) const
-{
- FilesLookup::const_iterator iter;
-
- if (alreadyLowerCase) {
- iter = m_FilesLookup.find(DirectoryEntryFileKey(name));
- } else {
- iter = m_FilesLookup.find(DirectoryEntryFileKey(ToLowerCopy(name)));
- }
-
- if (iter != m_FilesLookup.end()) {
- return m_FileRegister->getFile(iter->second);
- } else {
- return FileEntryPtr();
- }
-}
-
-const FileEntryPtr DirectoryEntry::findFile(const DirectoryEntryFileKey& key) const
-{
- auto iter = m_FilesLookup.find(key);
-
- if (iter != m_FilesLookup.end()) {
- return m_FileRegister->getFile(iter->second);
- } else {
- return FileEntryPtr();
- }
-}
-
-bool DirectoryEntry::hasFile(const std::wstring& name) const
-{
- return m_Files.contains(ToLowerCopy(name));
-}
-
-bool DirectoryEntry::containsArchive(std::wstring archiveName)
-{
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- FileEntryPtr entry = m_FileRegister->getFile(iter->second);
- if (entry->isFromArchive(archiveName)) {
- return true;
- }
- }
-
- return false;
-}
-
-const FileEntryPtr DirectoryEntry::searchFile(
- const std::wstring &path, const DirectoryEntry** directory) const
-{
- if (directory != nullptr) {
- *directory = nullptr;
- }
-
- if ((path.length() == 0) || (path == L"*")) {
- // no file name -> the path ended on a (back-)slash
- if (directory != nullptr) {
- *directory = this;
- }
-
- return FileEntryPtr();
- }
-
- const size_t len = path.find_first_of(L"\\/");
-
- if (len == std::string::npos) {
- // no more path components
- auto iter = m_Files.find(ToLowerCopy(path));
-
- if (iter != m_Files.end()) {
- return m_FileRegister->getFile(iter->second);
- } else if (directory != nullptr) {
- DirectoryEntry* temp = findSubDirectory(path);
- if (temp != nullptr) {
- *directory = temp;
- }
- }
- } else {
- // file is in a subdirectory, recurse into the matching subdirectory
- std::wstring pathComponent = path.substr(0, len);
- DirectoryEntry* temp = findSubDirectory(pathComponent);
-
- if (temp != nullptr) {
- if (len >= path.size()) {
- log::error(QObject::tr("unexpected end of path").toStdString());
- return FileEntryPtr();
- }
-
- return temp->searchFile(path.substr(len + 1), directory);
- }
- }
-
- return FileEntryPtr();
-}
-
-void DirectoryEntry::removeFile(FileIndex index)
-{
- removeFileFromList(index);
-}
-
-bool DirectoryEntry::removeFile(const std::wstring &filePath, int* origin)
-{
- size_t pos = filePath.find_first_of(L"\\/");
-
- if (pos == std::string::npos) {
- return this->remove(filePath, origin);
- }
-
- std::wstring dirName = filePath.substr(0, pos);
- std::wstring rest = filePath.substr(pos + 1);
-
- DirectoryStats dummy;
- DirectoryEntry* entry = getSubDirectoryRecursive(dirName, false, dummy);
-
- if (entry != nullptr) {
- return entry->removeFile(rest, origin);
- } else {
- return false;
- }
-}
-
-void DirectoryEntry::removeDir(const std::wstring &path)
-{
- size_t pos = path.find_first_of(L"\\/");
-
- if (pos == std::string::npos) {
- for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) {
- DirectoryEntry* entry = *iter;
-
- if (CaseInsensitiveEqual(entry->getName(), path)) {
- entry->removeDirRecursive();
- removeDirectoryFromList(iter);
- delete entry;
- break;
- }
- }
- } else {
- std::wstring dirName = path.substr(0, pos);
- std::wstring rest = path.substr(pos + 1);
-
- DirectoryStats dummy;
- DirectoryEntry* entry = getSubDirectoryRecursive(dirName, false, dummy);
-
- if (entry != nullptr) {
- entry->removeDir(rest);
- }
- }
-}
-
-bool DirectoryEntry::remove(const std::wstring &fileName, int* origin)
-{
- const auto lcFileName = ToLowerCopy(fileName);
-
- auto iter = m_Files.find(lcFileName);
- bool b = false;
-
- if (iter != m_Files.end()) {
- if (origin != nullptr) {
- FileEntryPtr entry = m_FileRegister->getFile(iter->second);
- if (entry.get() != nullptr) {
- bool ignore;
- *origin = entry->getOrigin(ignore);
- }
- }
-
- b = m_FileRegister->removeFile(iter->second);
- }
-
- return b;
-}
-
-bool DirectoryEntry::hasContentsFromOrigin(int originID) const
-{
- return m_Origins.find(originID) != m_Origins.end();
-}
-
-FilesOrigin &DirectoryEntry::createOrigin(
- const std::wstring &originName, const std::wstring &directory, int priority,
- DirectoryStats& stats)
-{
- auto r = m_OriginConnection->getOrCreate(
- originName, directory, priority,
- m_FileRegister, m_OriginConnection, stats);
-
- if (r.second) {
- ++stats.originCreate;
- } else {
- ++stats.originExists;
- }
-
- return r.first;
-}
-
-void DirectoryEntry::removeFiles(const std::set<FileIndex> &indices)
-{
- removeFilesFromList(indices);
-}
-
-FileEntryPtr DirectoryEntry::insert(
- std::wstring_view fileName, FilesOrigin &origin, FILETIME fileTime,
- std::wstring_view archive, int order, DirectoryStats& stats)
-{
- std::wstring fileNameLower = ToLowerCopy(fileName);
- FileEntryPtr fe;
-
- DirectoryEntryFileKey key(std::move(fileNameLower));
-
- {
- std::unique_lock lock(m_FilesMutex);
-
- FilesLookup::iterator itor;
-
- elapsed(stats.filesLookupTimes, [&]{
- itor = m_FilesLookup.find(key);
- });
-
- if (itor != m_FilesLookup.end()) {
- lock.unlock();
- ++stats.fileExists;
- fe = m_FileRegister->getFile(itor->second);
- } else {
- ++stats.fileCreate;
- fe = m_FileRegister->createFile(
- std::wstring(fileName.begin(), fileName.end()), this, stats);
-
- elapsed(stats.addFileTimes, [&] {
- addFileToList(std::move(key.value), fe->getIndex());
- });
-
- // fileNameLower has moved from this point
- }
- }
-
- elapsed(stats.addOriginToFileTimes, [&]{
- fe->addOrigin(origin.getID(), fileTime, archive, order);
- });
-
- elapsed(stats.addFileToOriginTimes, [&]{
- origin.addFile(fe->getIndex());
- });
-
- return fe;
-}
-
-FileEntryPtr DirectoryEntry::insert(
- env::File& file, FilesOrigin &origin, std::wstring_view archive, int order,
- DirectoryStats& stats)
-{
- FileEntryPtr fe;
-
- {
- std::unique_lock lock(m_FilesMutex);
-
- FilesMap::iterator itor;
-
- elapsed(stats.filesLookupTimes, [&]{
- itor = m_Files.find(file.lcname);
- });
-
- if (itor != m_Files.end()) {
- lock.unlock();
- ++stats.fileExists;
- fe = m_FileRegister->getFile(itor->second);
- } else {
- ++stats.fileCreate;
- fe = m_FileRegister->createFile(std::move(file.name), this, stats);
- // file.name has been moved from this point
-
- elapsed(stats.addFileTimes, [&]{
- addFileToList(std::move(file.lcname), fe->getIndex());
- });
-
- // file.lcname has been moved from this point
- }
- }
-
- elapsed(stats.addOriginToFileTimes, [&]{
- fe->addOrigin(origin.getID(), file.lastModified, archive, order);
- });
-
- elapsed(stats.addFileToOriginTimes, [&]{
- origin.addFile(fe->getIndex());
- });
-
- return fe;
-}
-
-struct DirectoryEntry::Context
-{
- FilesOrigin& origin;
- DirectoryStats& stats;
- std::stack<DirectoryEntry*> current;
-};
-
-void DirectoryEntry::addFiles(
- env::DirectoryWalker& walker, FilesOrigin &origin,
- const std::wstring& path, DirectoryStats& stats)
-{
- Context cx = {origin, stats};
- cx.current.push(this);
-
- walker.forEachEntry(path, &cx,
- [](void* pcx, std::wstring_view path)
- {
- onDirectoryStart((Context*)pcx, path);
- },
-
- [](void* pcx, std::wstring_view path)
- {
- onDirectoryEnd((Context*)pcx, path);
- },
-
- [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t)
- {
- onFile((Context*)pcx, path, ft);
- }
- );
-}
-
-void DirectoryEntry::onDirectoryStart(Context* cx, std::wstring_view path)
-{
- elapsed(cx->stats.dirTimes, [&] {
- auto* sd = cx->current.top()->getSubDirectory(
- path, true, cx->stats, cx->origin.getID());
-
- cx->current.push(sd);
- });
-}
-
-void DirectoryEntry::onDirectoryEnd(Context* cx, std::wstring_view path)
-{
- elapsed(cx->stats.dirTimes, [&] {
- cx->current.pop();
- });
-}
-
-void DirectoryEntry::onFile(Context* cx, std::wstring_view path, FILETIME ft)
-{
- elapsed(cx->stats.fileTimes, [&]{
- cx->current.top()->insert(path, cx->origin, ft, L"", -1, cx->stats);
- });
-}
-
-void DirectoryEntry::addFiles(
- FilesOrigin& origin, const BSA::Folder::Ptr archiveFolder, FILETIME fileTime,
- const std::wstring& archiveName, int order, DirectoryStats& stats)
-{
- // add files
- const auto fileCount = archiveFolder->getNumFiles();
- for (unsigned int i=0; i<fileCount; ++i) {
- const BSA::File::Ptr file = archiveFolder->getFile(i);
-
- auto f = insert(
- ToWString(file->getName(), true), origin, fileTime,
- archiveName, order, stats);
-
- if (f) {
- if (file->getUncompressedFileSize() > 0) {
- f->setFileSize(file->getFileSize(), file->getUncompressedFileSize());
- } else {
- f->setFileSize(file->getFileSize(), FileEntry::NoFileSize);
- }
- }
- }
-
- // recurse into subdirectories
- const auto dirCount = archiveFolder->getNumSubFolders();
- for (unsigned int i=0; i<dirCount; ++i) {
- const BSA::Folder::Ptr folder = archiveFolder->getSubFolder(i);
-
- DirectoryEntry* folderEntry = getSubDirectoryRecursive(
- ToWString(folder->getName(), true), true, stats, origin.getID());
-
- folderEntry->addFiles(origin, folder, fileTime, archiveName, order, stats);
- }
-}
-
-DirectoryEntry* DirectoryEntry::getSubDirectory(
- std::wstring_view name, bool create, DirectoryStats& stats, int originID)
-{
- std::wstring nameLc = ToLowerCopy(name);
-
- std::scoped_lock lock(m_SubDirMutex);
-
- SubDirectoriesLookup::iterator itor;
- elapsed(stats.subdirLookupTimes, [&] {
- itor = m_SubDirectoriesLookup.find(nameLc);
- });
-
- if (itor != m_SubDirectoriesLookup.end()) {
- ++stats.subdirExists;
- return itor->second;
- }
-
- if (create) {
- ++stats.subdirCreate;
-
- auto* entry = new DirectoryEntry(
- std::wstring(name.begin(), name.end()), this, originID,
- m_FileRegister, m_OriginConnection);
-
- elapsed(stats.addDirectoryTimes, [&] {
- addDirectoryToList(entry, std::move(nameLc));
- // nameLc is moved from this point
- });
-
- return entry;
- } else {
- return nullptr;
- }
-}
-
-DirectoryEntry* DirectoryEntry::getSubDirectory(
- env::Directory& dir, bool create, DirectoryStats& stats, int originID)
-{
- SubDirectoriesLookup::iterator itor;
-
- std::scoped_lock lock(m_SubDirMutex);
-
- elapsed(stats.subdirLookupTimes, [&] {
- itor = m_SubDirectoriesLookup.find(dir.lcname);
- });
-
- if (itor != m_SubDirectoriesLookup.end()) {
- ++stats.subdirExists;
- return itor->second;
- }
-
- if (create) {
- ++stats.subdirCreate;
-
- auto* entry = new DirectoryEntry(
- std::move(dir.name), this, originID,
- m_FileRegister, m_OriginConnection);
- // dir.name is moved from this point
-
- elapsed(stats.addDirectoryTimes, [&]{
- addDirectoryToList(entry, std::move(dir.lcname));
- });
-
- // dir.lcname is moved from this point
-
- return entry;
- } else {
- return nullptr;
- }
-}
-
-DirectoryEntry* DirectoryEntry::getSubDirectoryRecursive(
- const std::wstring& path, bool create, DirectoryStats& stats, int originID)
-{
- if (path.length() == 0) {
- // path ended with a backslash?
- return this;
- }
-
- const size_t pos = path.find_first_of(L"\\/");
-
- if (pos == std::wstring::npos) {
- return getSubDirectory(path, create, stats);
- } else {
- DirectoryEntry* nextChild = getSubDirectory(
- path.substr(0, pos), create, stats, originID);
-
- if (nextChild == nullptr) {
- return nullptr;
- } else {
- return nextChild->getSubDirectoryRecursive(
- path.substr(pos + 1), create, stats, originID);
- }
- }
-}
-
-void DirectoryEntry::removeDirRecursive()
-{
- while (!m_Files.empty()) {
- m_FileRegister->removeFile(m_Files.begin()->second);
- }
-
- m_FilesLookup.clear();
-
- for (DirectoryEntry* entry : m_SubDirectories) {
- entry->removeDirRecursive();
- delete entry;
- }
-
- m_SubDirectories.clear();
- m_SubDirectoriesLookup.clear();
-}
-
-void DirectoryEntry::addDirectoryToList(DirectoryEntry* e, std::wstring nameLc)
-{
- m_SubDirectories.insert(e);
- m_SubDirectoriesLookup.emplace(std::move(nameLc), e);
-}
-
-void DirectoryEntry::removeDirectoryFromList(SubDirectories::iterator itor)
-{
- const auto* entry = *itor;
-
- {
- auto itor2 = std::find_if(
- m_SubDirectoriesLookup.begin(), m_SubDirectoriesLookup.end(),
- [&](auto&& d) { return (d.second == entry); });
-
- if (itor2 == m_SubDirectoriesLookup.end()) {
- log::error("entry {} not in sub directories map", entry->getName());
- } else {
- m_SubDirectoriesLookup.erase(itor2);
- }
- }
-
- m_SubDirectories.erase(itor);
-}
-
-void DirectoryEntry::removeFileFromList(FileIndex index)
-{
- auto removeFrom = [&](auto& list) {
- auto iter = std::find_if(
- list.begin(), list.end(),
- [&index](auto&& pair) { return (pair.second == index); }
- );
-
- if (iter == list.end()) {
- auto f = m_FileRegister->getFile(index);
-
- if (f) {
- log::error(
- "can't remove file '{}', not in directory entry '{}'",
- f->getName(), getName());
- } else {
- log::error(
- "can't remove file with index {}, not in directory entry '{}' and "
- "not in register",
- index, getName());
- }
- } else {
- list.erase(iter);
- }
- };
-
- removeFrom(m_FilesLookup);
- removeFrom(m_Files);
-}
-
-void DirectoryEntry::removeFilesFromList(const std::set<FileIndex>& indices)
-{
- for (auto iter = m_Files.begin(); iter != m_Files.end();) {
- if (indices.find(iter->second) != indices.end()) {
- iter = m_Files.erase(iter);
- } else {
- ++iter;
- }
- }
-
- for (auto iter = m_FilesLookup.begin(); iter != m_FilesLookup.end();) {
- if (indices.find(iter->second) != indices.end()) {
- iter = m_FilesLookup.erase(iter);
- } else {
- ++iter;
- }
- }
-}
-
-void DirectoryEntry::addFileToList(std::wstring fileNameLower, FileIndex index)
-{
- m_FilesLookup.emplace(fileNameLower, index);
- m_Files.emplace(std::move(fileNameLower), index);
- // fileNameLower has been moved from this point
-}
-
-struct DumpFailed : public std::runtime_error
-{
- using runtime_error::runtime_error;
-};
-
-void DirectoryEntry::dump(const std::wstring& file) const
-{
- try
- {
- std::FILE* f = nullptr;
- auto e = _wfopen_s(&f, file.c_str(), L"wb");
-
- if (e != 0 || !f) {
- throw DumpFailed(fmt::format(
- "failed to open, {} ({})", std::strerror(e), e));
- }
-
- Guard g([&]{ std::fclose(f); });
-
- dump(f, L"Data");
- }
- catch(DumpFailed& e)
- {
- log::error(
- "failed to write list to '{}': {}",
- QString::fromStdWString(file).toStdString(), e.what());
- }
-}
-
-void DirectoryEntry::dump(std::FILE* f, const std::wstring& parentPath) const
-{
- {
- std::scoped_lock lock(m_FilesMutex);
-
- for (auto&& index : m_Files) {
- const auto file = m_FileRegister->getFile(index.second);
- if (!file) {
- continue;
- }
-
- if (file->isFromArchive()) {
- // TODO: don't list files from archives. maybe make this an option?
- continue;
- }
-
- const auto& o = m_OriginConnection->getByID(file->getOrigin());
- const auto path = parentPath + L"\\" + file->getName();
- const auto line = path + L"\t(" + o.getName() + L")\r\n";
-
- const auto lineu8 = MOShared::ToString(line, true);
-
- if (std::fwrite(lineu8.data(), lineu8.size(), 1, f) != 1) {
- const auto e = errno;
- throw DumpFailed(fmt::format(
- "failed to write, {} ({})", std::strerror(e), e));
- }
- }
- }
-
- {
- std::scoped_lock lock(m_SubDirMutex);
- for (auto&& d : m_SubDirectories) {
- const auto path = parentPath + L"\\" + d->m_Name;
- d->dump(f, path);
- }
- }
-}
-
-} // namespace MOShared
+/*
+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 "directoryentry.h"
+#include "originconnection.h"
+#include "filesorigin.h"
+#include "fileentry.h"
+#include "../envfs.h"
+#include "util.h"
+#include "windows_error.h"
+#include <log.h>
+#include <utility.h>
+
+namespace MOShared
+{
+
+using namespace MOBase;
+const int MAXPATH_UNICODE = 32767;
+
+template <class F>
+void elapsedImpl(std::chrono::nanoseconds& out, F&& f)
+{
+ if constexpr (DirectoryStats::EnableInstrumentation) {
+ const auto start = std::chrono::high_resolution_clock::now();
+ f();
+ const auto end = std::chrono::high_resolution_clock::now();
+ out += (end - start);
+ } else {
+ f();
+ }
+}
+
+// elapsed() is not optimized out when EnableInstrumentation is false even
+// though it's equivalent that this macro
+#define elapsed(OUT, F) (F)();
+//#define elapsed(OUT, F) elapsedImpl(OUT, F);
+
+static bool SupportOptimizedFind()
+{
+ // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2, win 7 and newer
+
+ OSVERSIONINFOEX versionInfo;
+ versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
+ versionInfo.dwMajorVersion = 6;
+ versionInfo.dwMinorVersion = 1;
+
+ ULONGLONG mask = ::VerSetConditionMask(
+ ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL),
+ VER_MINORVERSION, VER_GREATER_EQUAL);
+
+ return (::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, mask) == TRUE);
+}
+
+bool DirCompareByName::operator()(
+ const DirectoryEntry* lhs, const DirectoryEntry* rhs) const
+{
+ return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
+}
+
+
+DirectoryEntry::DirectoryEntry(
+ std::wstring name, DirectoryEntry* parent, int originID) :
+ m_OriginConnection(new OriginConnection),
+ m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(true)
+{
+ m_FileRegister.reset(new FileRegister(m_OriginConnection));
+ m_Origins.insert(originID);
+}
+
+DirectoryEntry::DirectoryEntry(
+ std::wstring name, DirectoryEntry* parent, int originID,
+ boost::shared_ptr<FileRegister> fileRegister,
+ boost::shared_ptr<OriginConnection> originConnection) :
+ m_FileRegister(fileRegister), m_OriginConnection(originConnection),
+ m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(false)
+{
+ m_Origins.insert(originID);
+}
+
+DirectoryEntry::~DirectoryEntry()
+{
+ clear();
+}
+
+void DirectoryEntry::clear()
+{
+ for (auto itor=m_SubDirectories.rbegin(); itor!=m_SubDirectories.rend(); ++itor) {
+ delete *itor;
+ }
+
+ m_Files.clear();
+ m_FilesLookup.clear();
+ m_SubDirectories.clear();
+ m_SubDirectoriesLookup.clear();
+}
+
+void DirectoryEntry::addFromOrigin(
+ const std::wstring &originName, const std::wstring &directory, int priority,
+ DirectoryStats& stats)
+{
+ env::DirectoryWalker walker;
+ addFromOrigin(walker, originName, directory, priority, stats);
+}
+
+void DirectoryEntry::addFromOrigin(
+ env::DirectoryWalker& walker, const std::wstring &originName,
+ const std::wstring &directory, int priority, DirectoryStats& stats)
+{
+ FilesOrigin &origin = createOrigin(originName, directory, priority, stats);
+
+ if (!directory.empty()) {
+ addFiles(walker, origin, directory, stats);
+ }
+
+ m_Populated = true;
+}
+
+void DirectoryEntry::addFromList(
+ const std::wstring &originName, const std::wstring &directory,
+ env::Directory& root, int priority, DirectoryStats& stats)
+{
+ stats = {};
+
+ FilesOrigin &origin = createOrigin(originName, directory, priority, stats);
+ addDir(origin, root, stats);
+}
+
+void DirectoryEntry::addDir(
+ FilesOrigin& origin, env::Directory& d, DirectoryStats& stats)
+{
+ elapsed(stats.dirTimes, [&]{
+ for (auto& sd : d.dirs) {
+ auto* sdirEntry = getSubDirectory(sd, true, stats, origin.getID());
+ sdirEntry->addDir(origin, sd, stats);
+ }
+ });
+
+ elapsed(stats.fileTimes, [&]{
+ for (auto& f : d.files) {
+ insert(f, origin, L"", -1, stats);
+ }
+ });
+
+ m_Populated = true;
+}
+
+void DirectoryEntry::addFromAllBSAs(
+ const std::wstring& originName, const std::wstring& directory,
+ int priority, const std::vector<std::wstring>& archives,
+ const std::set<std::wstring>& enabledArchives,
+ const std::vector<std::wstring>& loadOrder,
+ DirectoryStats& stats)
+{
+ for (const auto& archive : archives) {
+ const std::filesystem::path archivePath(archive);
+ const auto filename = archivePath.filename().native();
+
+ if (!enabledArchives.contains(filename)) {
+ continue;
+ }
+
+ const auto filenameLc = ToLowerCopy(filename);
+
+ int order = -1;
+
+ for (auto plugin : loadOrder)
+ {
+ const auto pluginNameLc =
+ ToLowerCopy(std::filesystem::path(plugin).stem().native());
+
+ if (filenameLc.starts_with(pluginNameLc + L" - ") ||
+ filenameLc.starts_with(pluginNameLc + L".")) {
+ auto itor = std::find(loadOrder.begin(), loadOrder.end(), plugin);
+ if (itor != loadOrder.end()) {
+ order = std::distance(loadOrder.begin(), itor);
+ }
+ }
+ }
+
+ addFromBSA(
+ originName, directory, archivePath.native(),
+ priority, order, stats);
+ }
+}
+
+void DirectoryEntry::addFromBSA(
+ const std::wstring& originName, const std::wstring& directory,
+ const std::wstring& archivePath, int priority, int order, DirectoryStats& stats)
+{
+ FilesOrigin& origin = createOrigin(originName, directory, priority, stats);
+ const auto archiveName = std::filesystem::path(archivePath).filename().native();
+
+ if (containsArchive(archiveName)) {
+ return;
+ }
+
+ BSA::Archive archive;
+ BSA::EErrorCode res = BSA::ERROR_NONE;
+
+ try
+ {
+ // read() can return an error, but it can also throw if the file is not a
+ // valid bsa
+ res = archive.read(ToString(archivePath, false).c_str(), false);
+ }
+ catch(std::exception& e)
+ {
+ log::error("invalid bsa '{}', error {}", archivePath, e.what());
+ return;
+ }
+
+ if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
+ log::error("invalid bsa '{}', error {}", archivePath, res);
+ return;
+ }
+
+ std::error_code ec;
+ const auto lwt = std::filesystem::last_write_time(archivePath, ec);
+ FILETIME ft = {};
+
+ if (ec) {
+ log::warn(
+ "failed to get last modified date for '{}', {}",
+ archivePath, ec.message());
+ } else {
+ ft = ToFILETIME(lwt);
+ }
+
+ addFiles(origin, archive.getRoot(), ft, archiveName, order, stats);
+
+ m_Populated = true;
+}
+
+void DirectoryEntry::propagateOrigin(int origin)
+{
+ {
+ std::scoped_lock lock(m_OriginsMutex);
+ m_Origins.insert(origin);
+ }
+
+ if (m_Parent != nullptr) {
+ m_Parent->propagateOrigin(origin);
+ }
+}
+
+bool DirectoryEntry::originExists(const std::wstring &name) const
+{
+ return m_OriginConnection->exists(name);
+}
+
+FilesOrigin &DirectoryEntry::getOriginByID(int ID) const
+{
+ return m_OriginConnection->getByID(ID);
+}
+
+FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const
+{
+ return m_OriginConnection->getByName(name);
+}
+
+const FilesOrigin* DirectoryEntry::findOriginByID(int ID) const
+{
+ return m_OriginConnection->findByID(ID);
+}
+
+int DirectoryEntry::anyOrigin() const
+{
+ bool ignore;
+
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ FileEntryPtr entry = m_FileRegister->getFile(iter->second);
+ if ((entry.get() != nullptr) && !entry->isFromArchive()) {
+ return entry->getOrigin(ignore);
+ }
+ }
+
+ // if we got here, no file directly within this directory is a valid indicator for a mod, thus
+ // we continue looking in subdirectories
+ for (DirectoryEntry* entry : m_SubDirectories) {
+ int res = entry->anyOrigin();
+ if (res != InvalidOriginID){
+ return res;
+ }
+ }
+
+ return *(m_Origins.begin());
+}
+
+std::vector<FileEntryPtr> DirectoryEntry::getFiles() const
+{
+ std::vector<FileEntryPtr> result;
+
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ result.push_back(m_FileRegister->getFile(iter->second));
+ }
+
+ return result;
+}
+
+DirectoryEntry* DirectoryEntry::findSubDirectory(
+ const std::wstring &name, bool alreadyLowerCase) const
+{
+ SubDirectoriesLookup::const_iterator itor;
+
+ if (alreadyLowerCase) {
+ itor = m_SubDirectoriesLookup.find(name);
+ } else {
+ itor = m_SubDirectoriesLookup.find(ToLowerCopy(name));
+ }
+
+ if (itor == m_SubDirectoriesLookup.end()) {
+ return nullptr;
+ }
+
+ return itor->second;
+}
+
+DirectoryEntry* DirectoryEntry::findSubDirectoryRecursive(const std::wstring &path)
+{
+ DirectoryStats dummy;
+ return getSubDirectoryRecursive(path, false, dummy, InvalidOriginID);
+}
+
+const FileEntryPtr DirectoryEntry::findFile(
+ const std::wstring &name, bool alreadyLowerCase) const
+{
+ FilesLookup::const_iterator iter;
+
+ if (alreadyLowerCase) {
+ iter = m_FilesLookup.find(DirectoryEntryFileKey(name));
+ } else {
+ iter = m_FilesLookup.find(DirectoryEntryFileKey(ToLowerCopy(name)));
+ }
+
+ if (iter != m_FilesLookup.end()) {
+ return m_FileRegister->getFile(iter->second);
+ } else {
+ return FileEntryPtr();
+ }
+}
+
+const FileEntryPtr DirectoryEntry::findFile(const DirectoryEntryFileKey& key) const
+{
+ auto iter = m_FilesLookup.find(key);
+
+ if (iter != m_FilesLookup.end()) {
+ return m_FileRegister->getFile(iter->second);
+ } else {
+ return FileEntryPtr();
+ }
+}
+
+bool DirectoryEntry::hasFile(const std::wstring& name) const
+{
+ return m_Files.contains(ToLowerCopy(name));
+}
+
+bool DirectoryEntry::containsArchive(std::wstring archiveName)
+{
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ FileEntryPtr entry = m_FileRegister->getFile(iter->second);
+ if (entry->isFromArchive(archiveName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+const FileEntryPtr DirectoryEntry::searchFile(
+ const std::wstring &path, const DirectoryEntry** directory) const
+{
+ if (directory != nullptr) {
+ *directory = nullptr;
+ }
+
+ if ((path.length() == 0) || (path == L"*")) {
+ // no file name -> the path ended on a (back-)slash
+ if (directory != nullptr) {
+ *directory = this;
+ }
+
+ return FileEntryPtr();
+ }
+
+ const size_t len = path.find_first_of(L"\\/");
+
+ if (len == std::string::npos) {
+ // no more path components
+ auto iter = m_Files.find(ToLowerCopy(path));
+
+ if (iter != m_Files.end()) {
+ return m_FileRegister->getFile(iter->second);
+ } else if (directory != nullptr) {
+ DirectoryEntry* temp = findSubDirectory(path);
+ if (temp != nullptr) {
+ *directory = temp;
+ }
+ }
+ } else {
+ // file is in a subdirectory, recurse into the matching subdirectory
+ std::wstring pathComponent = path.substr(0, len);
+ DirectoryEntry* temp = findSubDirectory(pathComponent);
+
+ if (temp != nullptr) {
+ if (len >= path.size()) {
+ log::error(QObject::tr("unexpected end of path").toStdString());
+ return FileEntryPtr();
+ }
+
+ return temp->searchFile(path.substr(len + 1), directory);
+ }
+ }
+
+ return FileEntryPtr();
+}
+
+void DirectoryEntry::removeFile(FileIndex index)
+{
+ removeFileFromList(index);
+}
+
+bool DirectoryEntry::removeFile(const std::wstring &filePath, int* origin)
+{
+ size_t pos = filePath.find_first_of(L"\\/");
+
+ if (pos == std::string::npos) {
+ return this->remove(filePath, origin);
+ }
+
+ std::wstring dirName = filePath.substr(0, pos);
+ std::wstring rest = filePath.substr(pos + 1);
+
+ DirectoryStats dummy;
+ DirectoryEntry* entry = getSubDirectoryRecursive(dirName, false, dummy);
+
+ if (entry != nullptr) {
+ return entry->removeFile(rest, origin);
+ } else {
+ return false;
+ }
+}
+
+void DirectoryEntry::removeDir(const std::wstring &path)
+{
+ size_t pos = path.find_first_of(L"\\/");
+
+ if (pos == std::string::npos) {
+ for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) {
+ DirectoryEntry* entry = *iter;
+
+ if (CaseInsensitiveEqual(entry->getName(), path)) {
+ entry->removeDirRecursive();
+ removeDirectoryFromList(iter);
+ delete entry;
+ break;
+ }
+ }
+ } else {
+ std::wstring dirName = path.substr(0, pos);
+ std::wstring rest = path.substr(pos + 1);
+
+ DirectoryStats dummy;
+ DirectoryEntry* entry = getSubDirectoryRecursive(dirName, false, dummy);
+
+ if (entry != nullptr) {
+ entry->removeDir(rest);
+ }
+ }
+}
+
+bool DirectoryEntry::remove(const std::wstring &fileName, int* origin)
+{
+ const auto lcFileName = ToLowerCopy(fileName);
+
+ auto iter = m_Files.find(lcFileName);
+ bool b = false;
+
+ if (iter != m_Files.end()) {
+ if (origin != nullptr) {
+ FileEntryPtr entry = m_FileRegister->getFile(iter->second);
+ if (entry.get() != nullptr) {
+ bool ignore;
+ *origin = entry->getOrigin(ignore);
+ }
+ }
+
+ b = m_FileRegister->removeFile(iter->second);
+ }
+
+ return b;
+}
+
+bool DirectoryEntry::hasContentsFromOrigin(int originID) const
+{
+ return m_Origins.find(originID) != m_Origins.end();
+}
+
+FilesOrigin &DirectoryEntry::createOrigin(
+ const std::wstring &originName, const std::wstring &directory, int priority,
+ DirectoryStats& stats)
+{
+ auto r = m_OriginConnection->getOrCreate(
+ originName, directory, priority,
+ m_FileRegister, m_OriginConnection, stats);
+
+ if (r.second) {
+ ++stats.originCreate;
+ } else {
+ ++stats.originExists;
+ }
+
+ return r.first;
+}
+
+void DirectoryEntry::removeFiles(const std::set<FileIndex> &indices)
+{
+ removeFilesFromList(indices);
+}
+
+FileEntryPtr DirectoryEntry::insert(
+ std::wstring_view fileName, FilesOrigin &origin, FILETIME fileTime,
+ std::wstring_view archive, int order, DirectoryStats& stats)
+{
+ std::wstring fileNameLower = ToLowerCopy(fileName);
+ FileEntryPtr fe;
+
+ DirectoryEntryFileKey key(std::move(fileNameLower));
+
+ {
+ std::unique_lock lock(m_FilesMutex);
+
+ FilesLookup::iterator itor;
+
+ elapsed(stats.filesLookupTimes, [&]{
+ itor = m_FilesLookup.find(key);
+ });
+
+ if (itor != m_FilesLookup.end()) {
+ lock.unlock();
+ ++stats.fileExists;
+ fe = m_FileRegister->getFile(itor->second);
+ } else {
+ ++stats.fileCreate;
+ fe = m_FileRegister->createFile(
+ std::wstring(fileName.begin(), fileName.end()), this, stats);
+
+ elapsed(stats.addFileTimes, [&] {
+ addFileToList(std::move(key.value), fe->getIndex());
+ });
+
+ // fileNameLower has moved from this point
+ }
+ }
+
+ elapsed(stats.addOriginToFileTimes, [&]{
+ fe->addOrigin(origin.getID(), fileTime, archive, order);
+ });
+
+ elapsed(stats.addFileToOriginTimes, [&]{
+ origin.addFile(fe->getIndex());
+ });
+
+ return fe;
+}
+
+FileEntryPtr DirectoryEntry::insert(
+ env::File& file, FilesOrigin &origin, std::wstring_view archive, int order,
+ DirectoryStats& stats)
+{
+ FileEntryPtr fe;
+
+ {
+ std::unique_lock lock(m_FilesMutex);
+
+ FilesMap::iterator itor;
+
+ elapsed(stats.filesLookupTimes, [&]{
+ itor = m_Files.find(file.lcname);
+ });
+
+ if (itor != m_Files.end()) {
+ lock.unlock();
+ ++stats.fileExists;
+ fe = m_FileRegister->getFile(itor->second);
+ } else {
+ ++stats.fileCreate;
+ fe = m_FileRegister->createFile(std::move(file.name), this, stats);
+ // file.name has been moved from this point
+
+ elapsed(stats.addFileTimes, [&]{
+ addFileToList(std::move(file.lcname), fe->getIndex());
+ });
+
+ // file.lcname has been moved from this point
+ }
+ }
+
+ elapsed(stats.addOriginToFileTimes, [&]{
+ fe->addOrigin(origin.getID(), file.lastModified, archive, order);
+ });
+
+ elapsed(stats.addFileToOriginTimes, [&]{
+ origin.addFile(fe->getIndex());
+ });
+
+ return fe;
+}
+
+struct DirectoryEntry::Context
+{
+ FilesOrigin& origin;
+ DirectoryStats& stats;
+ std::stack<DirectoryEntry*> current;
+};
+
+void DirectoryEntry::addFiles(
+ env::DirectoryWalker& walker, FilesOrigin &origin,
+ const std::wstring& path, DirectoryStats& stats)
+{
+ Context cx = {origin, stats};
+ cx.current.push(this);
+
+ walker.forEachEntry(path, &cx,
+ [](void* pcx, std::wstring_view path)
+ {
+ onDirectoryStart((Context*)pcx, path);
+ },
+
+ [](void* pcx, std::wstring_view path)
+ {
+ onDirectoryEnd((Context*)pcx, path);
+ },
+
+ [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t)
+ {
+ onFile((Context*)pcx, path, ft);
+ }
+ );
+}
+
+void DirectoryEntry::onDirectoryStart(Context* cx, std::wstring_view path)
+{
+ elapsed(cx->stats.dirTimes, [&] {
+ auto* sd = cx->current.top()->getSubDirectory(
+ path, true, cx->stats, cx->origin.getID());
+
+ cx->current.push(sd);
+ });
+}
+
+void DirectoryEntry::onDirectoryEnd(Context* cx, std::wstring_view path)
+{
+ elapsed(cx->stats.dirTimes, [&] {
+ cx->current.pop();
+ });
+}
+
+void DirectoryEntry::onFile(Context* cx, std::wstring_view path, FILETIME ft)
+{
+ elapsed(cx->stats.fileTimes, [&]{
+ cx->current.top()->insert(path, cx->origin, ft, L"", -1, cx->stats);
+ });
+}
+
+void DirectoryEntry::addFiles(
+ FilesOrigin& origin, const BSA::Folder::Ptr archiveFolder, FILETIME fileTime,
+ const std::wstring& archiveName, int order, DirectoryStats& stats)
+{
+ // add files
+ const auto fileCount = archiveFolder->getNumFiles();
+ for (unsigned int i=0; i<fileCount; ++i) {
+ const BSA::File::Ptr file = archiveFolder->getFile(i);
+
+ auto f = insert(
+ ToWString(file->getName(), true), origin, fileTime,
+ archiveName, order, stats);
+
+ if (f) {
+ if (file->getUncompressedFileSize() > 0) {
+ f->setFileSize(file->getFileSize(), file->getUncompressedFileSize());
+ } else {
+ f->setFileSize(file->getFileSize(), FileEntry::NoFileSize);
+ }
+ }
+ }
+
+ // recurse into subdirectories
+ const auto dirCount = archiveFolder->getNumSubFolders();
+ for (unsigned int i=0; i<dirCount; ++i) {
+ const BSA::Folder::Ptr folder = archiveFolder->getSubFolder(i);
+
+ DirectoryEntry* folderEntry = getSubDirectoryRecursive(
+ ToWString(folder->getName(), true), true, stats, origin.getID());
+
+ folderEntry->addFiles(origin, folder, fileTime, archiveName, order, stats);
+ }
+}
+
+DirectoryEntry* DirectoryEntry::getSubDirectory(
+ std::wstring_view name, bool create, DirectoryStats& stats, int originID)
+{
+ std::wstring nameLc = ToLowerCopy(name);
+
+ std::scoped_lock lock(m_SubDirMutex);
+
+ SubDirectoriesLookup::iterator itor;
+ elapsed(stats.subdirLookupTimes, [&] {
+ itor = m_SubDirectoriesLookup.find(nameLc);
+ });
+
+ if (itor != m_SubDirectoriesLookup.end()) {
+ ++stats.subdirExists;
+ return itor->second;
+ }
+
+ if (create) {
+ ++stats.subdirCreate;
+
+ auto* entry = new DirectoryEntry(
+ std::wstring(name.begin(), name.end()), this, originID,
+ m_FileRegister, m_OriginConnection);
+
+ elapsed(stats.addDirectoryTimes, [&] {
+ addDirectoryToList(entry, std::move(nameLc));
+ // nameLc is moved from this point
+ });
+
+ return entry;
+ } else {
+ return nullptr;
+ }
+}
+
+DirectoryEntry* DirectoryEntry::getSubDirectory(
+ env::Directory& dir, bool create, DirectoryStats& stats, int originID)
+{
+ SubDirectoriesLookup::iterator itor;
+
+ std::scoped_lock lock(m_SubDirMutex);
+
+ elapsed(stats.subdirLookupTimes, [&] {
+ itor = m_SubDirectoriesLookup.find(dir.lcname);
+ });
+
+ if (itor != m_SubDirectoriesLookup.end()) {
+ ++stats.subdirExists;
+ return itor->second;
+ }
+
+ if (create) {
+ ++stats.subdirCreate;
+
+ auto* entry = new DirectoryEntry(
+ std::move(dir.name), this, originID,
+ m_FileRegister, m_OriginConnection);
+ // dir.name is moved from this point
+
+ elapsed(stats.addDirectoryTimes, [&]{
+ addDirectoryToList(entry, std::move(dir.lcname));
+ });
+
+ // dir.lcname is moved from this point
+
+ return entry;
+ } else {
+ return nullptr;
+ }
+}
+
+DirectoryEntry* DirectoryEntry::getSubDirectoryRecursive(
+ const std::wstring& path, bool create, DirectoryStats& stats, int originID)
+{
+ if (path.length() == 0) {
+ // path ended with a backslash?
+ return this;
+ }
+
+ const size_t pos = path.find_first_of(L"\\/");
+
+ if (pos == std::wstring::npos) {
+ return getSubDirectory(path, create, stats);
+ } else {
+ DirectoryEntry* nextChild = getSubDirectory(
+ path.substr(0, pos), create, stats, originID);
+
+ if (nextChild == nullptr) {
+ return nullptr;
+ } else {
+ return nextChild->getSubDirectoryRecursive(
+ path.substr(pos + 1), create, stats, originID);
+ }
+ }
+}
+
+void DirectoryEntry::removeDirRecursive()
+{
+ while (!m_Files.empty()) {
+ m_FileRegister->removeFile(m_Files.begin()->second);
+ }
+
+ m_FilesLookup.clear();
+
+ for (DirectoryEntry* entry : m_SubDirectories) {
+ entry->removeDirRecursive();
+ delete entry;
+ }
+
+ m_SubDirectories.clear();
+ m_SubDirectoriesLookup.clear();
+}
+
+void DirectoryEntry::addDirectoryToList(DirectoryEntry* e, std::wstring nameLc)
+{
+ m_SubDirectories.insert(e);
+ m_SubDirectoriesLookup.emplace(std::move(nameLc), e);
+}
+
+void DirectoryEntry::removeDirectoryFromList(SubDirectories::iterator itor)
+{
+ const auto* entry = *itor;
+
+ {
+ auto itor2 = std::find_if(
+ m_SubDirectoriesLookup.begin(), m_SubDirectoriesLookup.end(),
+ [&](auto&& d) { return (d.second == entry); });
+
+ if (itor2 == m_SubDirectoriesLookup.end()) {
+ log::error("entry {} not in sub directories map", entry->getName());
+ } else {
+ m_SubDirectoriesLookup.erase(itor2);
+ }
+ }
+
+ m_SubDirectories.erase(itor);
+}
+
+void DirectoryEntry::removeFileFromList(FileIndex index)
+{
+ auto removeFrom = [&](auto& list) {
+ auto iter = std::find_if(
+ list.begin(), list.end(),
+ [&index](auto&& pair) { return (pair.second == index); }
+ );
+
+ if (iter == list.end()) {
+ auto f = m_FileRegister->getFile(index);
+
+ if (f) {
+ log::error(
+ "can't remove file '{}', not in directory entry '{}'",
+ f->getName(), getName());
+ } else {
+ log::error(
+ "can't remove file with index {}, not in directory entry '{}' and "
+ "not in register",
+ index, getName());
+ }
+ } else {
+ list.erase(iter);
+ }
+ };
+
+ removeFrom(m_FilesLookup);
+ removeFrom(m_Files);
+}
+
+void DirectoryEntry::removeFilesFromList(const std::set<FileIndex>& indices)
+{
+ for (auto iter = m_Files.begin(); iter != m_Files.end();) {
+ if (indices.find(iter->second) != indices.end()) {
+ iter = m_Files.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+
+ for (auto iter = m_FilesLookup.begin(); iter != m_FilesLookup.end();) {
+ if (indices.find(iter->second) != indices.end()) {
+ iter = m_FilesLookup.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+}
+
+void DirectoryEntry::addFileToList(std::wstring fileNameLower, FileIndex index)
+{
+ m_FilesLookup.emplace(fileNameLower, index);
+ m_Files.emplace(std::move(fileNameLower), index);
+ // fileNameLower has been moved from this point
+}
+
+struct DumpFailed : public std::runtime_error
+{
+ using runtime_error::runtime_error;
+};
+
+void DirectoryEntry::dump(const std::wstring& file) const
+{
+ try
+ {
+ std::FILE* f = nullptr;
+ auto e = _wfopen_s(&f, file.c_str(), L"wb");
+
+ if (e != 0 || !f) {
+ throw DumpFailed(fmt::format(
+ "failed to open, {} ({})", std::strerror(e), e));
+ }
+
+ Guard g([&]{ std::fclose(f); });
+
+ dump(f, L"Data");
+ }
+ catch(DumpFailed& e)
+ {
+ log::error(
+ "failed to write list to '{}': {}",
+ QString::fromStdWString(file).toStdString(), e.what());
+ }
+}
+
+void DirectoryEntry::dump(std::FILE* f, const std::wstring& parentPath) const
+{
+ {
+ std::scoped_lock lock(m_FilesMutex);
+
+ for (auto&& index : m_Files) {
+ const auto file = m_FileRegister->getFile(index.second);
+ if (!file) {
+ continue;
+ }
+
+ if (file->isFromArchive()) {
+ // TODO: don't list files from archives. maybe make this an option?
+ continue;
+ }
+
+ const auto& o = m_OriginConnection->getByID(file->getOrigin());
+ const auto path = parentPath + L"\\" + file->getName();
+ const auto line = path + L"\t(" + o.getName() + L")\r\n";
+
+ const auto lineu8 = MOShared::ToString(line, true);
+
+ if (std::fwrite(lineu8.data(), lineu8.size(), 1, f) != 1) {
+ const auto e = errno;
+ throw DumpFailed(fmt::format(
+ "failed to write, {} ({})", std::strerror(e), e));
+ }
+ }
+ }
+
+ {
+ std::scoped_lock lock(m_SubDirMutex);
+ for (auto&& d : m_SubDirectories) {
+ const auto path = parentPath + L"\\" + d->m_Name;
+ d->dump(f, path);
+ }
+ }
+}
+
+} // namespace MOShared
diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h
index 0ee3c919..f7ab2b90 100644
--- a/src/shared/directoryentry.h
+++ b/src/shared/directoryentry.h
@@ -1,321 +1,321 @@
-/*
-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 MO_REGISTER_DIRECTORYENTRY_INCLUDED
-#define MO_REGISTER_DIRECTORYENTRY_INCLUDED
-
-#include "fileregister.h"
-#include <bsatk.h>
-
-namespace env
-{
- class DirectoryWalker;
- struct Directory;
- struct File;
-}
-
-namespace std
-{
- template <>
- struct hash<MOShared::DirectoryEntryFileKey>
- {
- using argument_type = MOShared::DirectoryEntryFileKey;
- using result_type = std::size_t;
-
- inline result_type operator()(const argument_type& key) const;
- };
-}
-
-
-namespace MOShared
-{
-
-struct DirCompareByName
-{
- bool operator()(const DirectoryEntry* a, const DirectoryEntry* b) const;
-};
-
-
-class DirectoryEntry
-{
-public:
- using SubDirectories = std::set<DirectoryEntry*, DirCompareByName>;
-
- DirectoryEntry(
- std::wstring name, DirectoryEntry* parent, OriginID originID);
-
- DirectoryEntry(
- std::wstring name, DirectoryEntry* parent, OriginID originID,
- boost::shared_ptr<FileRegister> fileRegister,
- boost::shared_ptr<OriginConnection> originConnection);
-
- ~DirectoryEntry();
-
- // noncopyable
- DirectoryEntry(const DirectoryEntry&) = delete;
- DirectoryEntry& operator=(const DirectoryEntry&) = delete;
-
- void clear();
-
- bool isPopulated() const
- {
- return m_Populated;
- }
-
- bool isTopLevel() const
- {
- return m_TopLevel;
- }
-
- bool isEmpty() const
- {
- return m_Files.empty() && m_SubDirectories.empty();
- }
-
- bool hasFiles() const
- {
- return !m_Files.empty();
- }
-
- const DirectoryEntry* getParent() const
- {
- return m_Parent;
- }
-
- // add files to this directory (and subdirectories) from the specified origin.
- // That origin may exist or not
- void addFromOrigin(
- const std::wstring& originName,
- const std::wstring& directory, int priority, DirectoryStats& stats);
-
- void addFromOrigin(
- env::DirectoryWalker& walker, const std::wstring& originName,
- const std::wstring& directory, int priority, DirectoryStats& stats);
-
- void addFromAllBSAs(
- const std::wstring& originName, const std::wstring& directory,
- int priority, const std::vector<std::wstring>& archives,
- const std::set<std::wstring>& enabledArchives,
- const std::vector<std::wstring>& loadOrder,
- DirectoryStats& stats);
-
- void addFromBSA(
- const std::wstring& originName, const std::wstring& directory,
- const std::wstring& archivePath, int priority, int order,
- DirectoryStats& stats);
-
- void addFromList(
- const std::wstring& originName, const std::wstring& directory,
- env::Directory& root, int priority, DirectoryStats& stats);
-
- void propagateOrigin(OriginID origin);
-
- const std::wstring& getName() const
- {
- return m_Name;
- }
-
- boost::shared_ptr<FileRegister> getFileRegister()
- {
- return m_FileRegister;
- }
-
- bool originExists(const std::wstring& name) const;
- FilesOrigin& getOriginByID(OriginID ID) const;
- FilesOrigin& getOriginByName(const std::wstring& name) const;
- const FilesOrigin* findOriginByID(OriginID ID) const;
-
- OriginID anyOrigin() const;
-
- std::vector<FileEntryPtr> getFiles() const;
-
- const SubDirectories& getSubDirectories() const
- {
- return m_SubDirectories;
- }
-
- template <class F>
- void forEachDirectory(F&& f) const
- {
- for (auto&& d : m_SubDirectories) {
- if (!f(*d)) {
- break;
- }
- }
- }
-
- template <class F>
- void forEachFile(F&& f) const
- {
- for (auto&& p : m_Files) {
- if (auto file=m_FileRegister->getFile(p.second)) {
- if (!f(*file)) {
- break;
- }
- }
- }
- }
-
- template <class F>
- void forEachFileIndex(F&& f) const
- {
- for (auto&& p : m_Files) {
- if (!f(p.second)) {
- break;
- }
- }
- }
-
- FileEntryPtr getFileByIndex(FileIndex index) const
- {
- return m_FileRegister->getFile(index);
- }
-
- DirectoryEntry* findSubDirectory(
- const std::wstring& name, bool alreadyLowerCase=false) const;
-
- DirectoryEntry* findSubDirectoryRecursive(const std::wstring& path);
-
- /** retrieve a file in this directory by name.
- * @param name name of the file
- * @return fileentry object for the file or nullptr if no file matches
- */
- const FileEntryPtr findFile(const std::wstring& name, bool alreadyLowerCase=false) const;
- const FileEntryPtr findFile(const DirectoryEntryFileKey& key) const;
-
- bool hasFile(const std::wstring& name) const;
- bool containsArchive(std::wstring archiveName);
-
- // search through this directory and all subdirectories for a file by the
- // specified name (relative path).
- //
- // if directory is not nullptr, the referenced variable will be set to the
- // path containing the file
- //
- const FileEntryPtr searchFile(
- const std::wstring& path, const DirectoryEntry** directory=nullptr) const;
-
- void removeFile(FileIndex index);
-
- // remove the specified file from the tree. This can be a path leading to a
- // file in a subdirectory
- bool removeFile(const std::wstring& filePath, OriginID* origin = nullptr);
-
- /**
- * @brief remove the specified directory
- * @param path directory to remove
- */
- void removeDir(const std::wstring& path);
-
- bool remove(const std::wstring& fileName, OriginID* origin);
-
- bool hasContentsFromOrigin(OriginID originID) const;
-
- FilesOrigin& createOrigin(
- const std::wstring& originName,
- const std::wstring& directory, int priority, DirectoryStats& stats);
-
- void removeFiles(const std::set<FileIndex>& indices);
-
- void dump(const std::wstring& file) const;
-
-private:
- using FilesMap = std::map<std::wstring, FileIndex>;
- using FilesLookup = std::unordered_map<DirectoryEntryFileKey, FileIndex>;
- using SubDirectoriesLookup = std::unordered_map<std::wstring, DirectoryEntry*>;
-
- boost::shared_ptr<FileRegister> m_FileRegister;
- boost::shared_ptr<OriginConnection> m_OriginConnection;
-
- std::wstring m_Name;
- FilesMap m_Files;
- FilesLookup m_FilesLookup;
- SubDirectories m_SubDirectories;
- SubDirectoriesLookup m_SubDirectoriesLookup;
-
- DirectoryEntry* m_Parent;
- std::set<OriginID> m_Origins;
- bool m_Populated;
- bool m_TopLevel;
- mutable std::mutex m_SubDirMutex;
- mutable std::mutex m_FilesMutex;
- mutable std::mutex m_OriginsMutex;
-
-
- FileEntryPtr insert(
- std::wstring_view fileName, FilesOrigin& origin, FILETIME fileTime,
- std::wstring_view archive, int order, DirectoryStats& stats);
-
- FileEntryPtr insert(
- env::File& file, FilesOrigin& origin,
- std::wstring_view archive, int order, DirectoryStats& stats);
-
- void addFiles(
- env::DirectoryWalker& walker, FilesOrigin& origin,
- const std::wstring& path, DirectoryStats& stats);
-
- void addFiles(
- FilesOrigin& origin, BSA::Folder::Ptr archiveFolder, FILETIME fileTime,
- const std::wstring& archiveName, int order, DirectoryStats& stats);
-
- void addDir(FilesOrigin& origin, env::Directory& d, DirectoryStats& stats);
-
- DirectoryEntry* getSubDirectory(
- std::wstring_view name, bool create, DirectoryStats& stats,
- OriginID originID = InvalidOriginID);
-
- DirectoryEntry* getSubDirectory(
- env::Directory& dir, bool create, DirectoryStats& stats,
- OriginID originID = InvalidOriginID);
-
- DirectoryEntry* getSubDirectoryRecursive(
- const std::wstring& path, bool create, DirectoryStats& stats,
- OriginID originID = InvalidOriginID);
-
- void removeDirRecursive();
-
- void addDirectoryToList(DirectoryEntry* e, std::wstring nameLc);
- void removeDirectoryFromList(SubDirectories::iterator itor);
-
- void addFileToList(std::wstring fileNameLower, FileIndex index);
- void removeFileFromList(FileIndex index);
- void removeFilesFromList(const std::set<FileIndex>& indices);
-
- struct Context;
- static void onDirectoryStart(Context* cx, std::wstring_view path);
- static void onDirectoryEnd(Context* cx, std::wstring_view path);
- static void onFile(Context* cx, std::wstring_view path, FILETIME ft);
-
- void dump(std::FILE* f, const std::wstring& parentPath) const;
-};
-
-} // namespace MOShared
-
-
-namespace std
-{
- hash<MOShared::DirectoryEntryFileKey>::result_type
- hash<MOShared::DirectoryEntryFileKey>::operator()(
- const argument_type& key) const
- {
- return key.hash;
- }
-}
-
-#endif // MO_REGISTER_DIRECTORYENTRY_INCLUDED
+/*
+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 MO_REGISTER_DIRECTORYENTRY_INCLUDED
+#define MO_REGISTER_DIRECTORYENTRY_INCLUDED
+
+#include "fileregister.h"
+#include <bsatk.h>
+
+namespace env
+{
+ class DirectoryWalker;
+ struct Directory;
+ struct File;
+}
+
+namespace std
+{
+ template <>
+ struct hash<MOShared::DirectoryEntryFileKey>
+ {
+ using argument_type = MOShared::DirectoryEntryFileKey;
+ using result_type = std::size_t;
+
+ inline result_type operator()(const argument_type& key) const;
+ };
+}
+
+
+namespace MOShared
+{
+
+struct DirCompareByName
+{
+ bool operator()(const DirectoryEntry* a, const DirectoryEntry* b) const;
+};
+
+
+class DirectoryEntry
+{
+public:
+ using SubDirectories = std::set<DirectoryEntry*, DirCompareByName>;
+
+ DirectoryEntry(
+ std::wstring name, DirectoryEntry* parent, OriginID originID);
+
+ DirectoryEntry(
+ std::wstring name, DirectoryEntry* parent, OriginID originID,
+ boost::shared_ptr<FileRegister> fileRegister,
+ boost::shared_ptr<OriginConnection> originConnection);
+
+ ~DirectoryEntry();
+
+ // noncopyable
+ DirectoryEntry(const DirectoryEntry&) = delete;
+ DirectoryEntry& operator=(const DirectoryEntry&) = delete;
+
+ void clear();
+
+ bool isPopulated() const
+ {
+ return m_Populated;
+ }
+
+ bool isTopLevel() const
+ {
+ return m_TopLevel;
+ }
+
+ bool isEmpty() const
+ {
+ return m_Files.empty() && m_SubDirectories.empty();
+ }
+
+ bool hasFiles() const
+ {
+ return !m_Files.empty();
+ }
+
+ const DirectoryEntry* getParent() const
+ {
+ return m_Parent;
+ }
+
+ // add files to this directory (and subdirectories) from the specified origin.
+ // That origin may exist or not
+ void addFromOrigin(
+ const std::wstring& originName,
+ const std::wstring& directory, int priority, DirectoryStats& stats);
+
+ void addFromOrigin(
+ env::DirectoryWalker& walker, const std::wstring& originName,
+ const std::wstring& directory, int priority, DirectoryStats& stats);
+
+ void addFromAllBSAs(
+ const std::wstring& originName, const std::wstring& directory,
+ int priority, const std::vector<std::wstring>& archives,
+ const std::set<std::wstring>& enabledArchives,
+ const std::vector<std::wstring>& loadOrder,
+ DirectoryStats& stats);
+
+ void addFromBSA(
+ const std::wstring& originName, const std::wstring& directory,
+ const std::wstring& archivePath, int priority, int order,
+ DirectoryStats& stats);
+
+ void addFromList(
+ const std::wstring& originName, const std::wstring& directory,
+ env::Directory& root, int priority, DirectoryStats& stats);
+
+ void propagateOrigin(OriginID origin);
+
+ const std::wstring& getName() const
+ {
+ return m_Name;
+ }
+
+ boost::shared_ptr<FileRegister> getFileRegister()
+ {
+ return m_FileRegister;
+ }
+
+ bool originExists(const std::wstring& name) const;
+ FilesOrigin& getOriginByID(OriginID ID) const;
+ FilesOrigin& getOriginByName(const std::wstring& name) const;
+ const FilesOrigin* findOriginByID(OriginID ID) const;
+
+ OriginID anyOrigin() const;
+
+ std::vector<FileEntryPtr> getFiles() const;
+
+ const SubDirectories& getSubDirectories() const
+ {
+ return m_SubDirectories;
+ }
+
+ template <class F>
+ void forEachDirectory(F&& f) const
+ {
+ for (auto&& d : m_SubDirectories) {
+ if (!f(*d)) {
+ break;
+ }
+ }
+ }
+
+ template <class F>
+ void forEachFile(F&& f) const
+ {
+ for (auto&& p : m_Files) {
+ if (auto file=m_FileRegister->getFile(p.second)) {
+ if (!f(*file)) {
+ break;
+ }
+ }
+ }
+ }
+
+ template <class F>
+ void forEachFileIndex(F&& f) const
+ {
+ for (auto&& p : m_Files) {
+ if (!f(p.second)) {
+ break;
+ }
+ }
+ }
+
+ FileEntryPtr getFileByIndex(FileIndex index) const
+ {
+ return m_FileRegister->getFile(index);
+ }
+
+ DirectoryEntry* findSubDirectory(
+ const std::wstring& name, bool alreadyLowerCase=false) const;
+
+ DirectoryEntry* findSubDirectoryRecursive(const std::wstring& path);
+
+ /** retrieve a file in this directory by name.
+ * @param name name of the file
+ * @return fileentry object for the file or nullptr if no file matches
+ */
+ const FileEntryPtr findFile(const std::wstring& name, bool alreadyLowerCase=false) const;
+ const FileEntryPtr findFile(const DirectoryEntryFileKey& key) const;
+
+ bool hasFile(const std::wstring& name) const;
+ bool containsArchive(std::wstring archiveName);
+
+ // search through this directory and all subdirectories for a file by the
+ // specified name (relative path).
+ //
+ // if directory is not nullptr, the referenced variable will be set to the
+ // path containing the file
+ //
+ const FileEntryPtr searchFile(
+ const std::wstring& path, const DirectoryEntry** directory=nullptr) const;
+
+ void removeFile(FileIndex index);
+
+ // remove the specified file from the tree. This can be a path leading to a
+ // file in a subdirectory
+ bool removeFile(const std::wstring& filePath, OriginID* origin = nullptr);
+
+ /**
+ * @brief remove the specified directory
+ * @param path directory to remove
+ */
+ void removeDir(const std::wstring& path);
+
+ bool remove(const std::wstring& fileName, OriginID* origin);
+
+ bool hasContentsFromOrigin(OriginID originID) const;
+
+ FilesOrigin& createOrigin(
+ const std::wstring& originName,
+ const std::wstring& directory, int priority, DirectoryStats& stats);
+
+ void removeFiles(const std::set<FileIndex>& indices);
+
+ void dump(const std::wstring& file) const;
+
+private:
+ using FilesMap = std::map<std::wstring, FileIndex>;
+ using FilesLookup = std::unordered_map<DirectoryEntryFileKey, FileIndex>;
+ using SubDirectoriesLookup = std::unordered_map<std::wstring, DirectoryEntry*>;
+
+ boost::shared_ptr<FileRegister> m_FileRegister;
+ boost::shared_ptr<OriginConnection> m_OriginConnection;
+
+ std::wstring m_Name;
+ FilesMap m_Files;
+ FilesLookup m_FilesLookup;
+ SubDirectories m_SubDirectories;
+ SubDirectoriesLookup m_SubDirectoriesLookup;
+
+ DirectoryEntry* m_Parent;
+ std::set<OriginID> m_Origins;
+ bool m_Populated;
+ bool m_TopLevel;
+ mutable std::mutex m_SubDirMutex;
+ mutable std::mutex m_FilesMutex;
+ mutable std::mutex m_OriginsMutex;
+
+
+ FileEntryPtr insert(
+ std::wstring_view fileName, FilesOrigin& origin, FILETIME fileTime,
+ std::wstring_view archive, int order, DirectoryStats& stats);
+
+ FileEntryPtr insert(
+ env::File& file, FilesOrigin& origin,
+ std::wstring_view archive, int order, DirectoryStats& stats);
+
+ void addFiles(
+ env::DirectoryWalker& walker, FilesOrigin& origin,
+ const std::wstring& path, DirectoryStats& stats);
+
+ void addFiles(
+ FilesOrigin& origin, BSA::Folder::Ptr archiveFolder, FILETIME fileTime,
+ const std::wstring& archiveName, int order, DirectoryStats& stats);
+
+ void addDir(FilesOrigin& origin, env::Directory& d, DirectoryStats& stats);
+
+ DirectoryEntry* getSubDirectory(
+ std::wstring_view name, bool create, DirectoryStats& stats,
+ OriginID originID = InvalidOriginID);
+
+ DirectoryEntry* getSubDirectory(
+ env::Directory& dir, bool create, DirectoryStats& stats,
+ OriginID originID = InvalidOriginID);
+
+ DirectoryEntry* getSubDirectoryRecursive(
+ const std::wstring& path, bool create, DirectoryStats& stats,
+ OriginID originID = InvalidOriginID);
+
+ void removeDirRecursive();
+
+ void addDirectoryToList(DirectoryEntry* e, std::wstring nameLc);
+ void removeDirectoryFromList(SubDirectories::iterator itor);
+
+ void addFileToList(std::wstring fileNameLower, FileIndex index);
+ void removeFileFromList(FileIndex index);
+ void removeFilesFromList(const std::set<FileIndex>& indices);
+
+ struct Context;
+ static void onDirectoryStart(Context* cx, std::wstring_view path);
+ static void onDirectoryEnd(Context* cx, std::wstring_view path);
+ static void onFile(Context* cx, std::wstring_view path, FILETIME ft);
+
+ void dump(std::FILE* f, const std::wstring& parentPath) const;
+};
+
+} // namespace MOShared
+
+
+namespace std
+{
+ hash<MOShared::DirectoryEntryFileKey>::result_type
+ hash<MOShared::DirectoryEntryFileKey>::operator()(
+ const argument_type& key) const
+ {
+ return key.hash;
+ }
+}
+
+#endif // MO_REGISTER_DIRECTORYENTRY_INCLUDED
diff --git a/src/shared/util.cpp b/src/shared/util.cpp
index 54c6b841..e2faaec5 100644
--- a/src/shared/util.cpp
+++ b/src/shared/util.cpp
@@ -1,444 +1,444 @@
-/*
-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 "util.h"
-#include "windows_error.h"
-#include "../mainwindow.h"
-#include "../env.h"
-#include <log.h>
-#include <usvfs.h>
-#include <usvfs_version.h>
-
-using namespace MOBase;
-
-namespace MOShared
-{
-
-bool FileExists(const std::string &filename)
-{
- DWORD dwAttrib = ::GetFileAttributesA(filename.c_str());
-
- return (dwAttrib != INVALID_FILE_ATTRIBUTES);
-}
-
-bool FileExists(const std::wstring &filename)
-{
- DWORD dwAttrib = ::GetFileAttributesW(filename.c_str());
-
- return (dwAttrib != INVALID_FILE_ATTRIBUTES);
-}
-
-bool FileExists(const std::wstring &searchPath, const std::wstring &filename)
-{
- std::wstringstream stream;
- stream << searchPath << "\\" << filename;
- return FileExists(stream.str());
-}
-
-std::string ToString(const std::wstring &source, bool utf8)
-{
- std::string result;
- if (source.length() > 0) {
- UINT codepage = CP_UTF8;
- if (!utf8) {
- codepage = AreFileApisANSI() ? GetACP() : GetOEMCP();
- }
- int sizeRequired = ::WideCharToMultiByte(codepage, 0, &source[0], -1, nullptr, 0, nullptr, nullptr);
- if (sizeRequired == 0) {
- throw windows_error("failed to convert string to multibyte");
- }
- // the size returned by WideCharToMultiByte contains zero termination IF -1 is specified for the length.
- // we don't want that \0 in the string because then the length field would be wrong. Because madness
- result.resize(sizeRequired - 1, '\0');
- ::WideCharToMultiByte(codepage, 0, &source[0], (int)source.size(), &result[0], sizeRequired, nullptr, nullptr);
- }
-
- return result;
-}
-
-std::wstring ToWString(const std::string &source, bool utf8)
-{
- std::wstring result;
- if (source.length() > 0) {
- UINT codepage = CP_UTF8;
- if (!utf8) {
- codepage = AreFileApisANSI() ? GetACP() : GetOEMCP();
- }
- int sizeRequired
- = ::MultiByteToWideChar(codepage, 0, source.c_str(),
- static_cast<int>(source.length()), nullptr, 0);
- if (sizeRequired == 0) {
- throw windows_error("failed to convert string to wide character");
- }
- result.resize(sizeRequired, L'\0');
- ::MultiByteToWideChar(codepage, 0, source.c_str(),
- static_cast<int>(source.length()), &result[0],
- sizeRequired);
- }
-
- return result;
-}
-
-static std::locale loc("");
-static auto locToLowerW = [] (wchar_t in) -> wchar_t {
- return std::tolower(in, loc);
-};
-
-static auto locToLower = [] (char in) -> char {
- return std::tolower(in, loc);
-};
-
-std::string& ToLowerInPlace(std::string& text)
-{
- CharLowerBuffA(const_cast<CHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
- return text;
-}
-
-std::string ToLowerCopy(const std::string& text)
-{
- std::string result(text);
- CharLowerBuffA(const_cast<CHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
- return result;
-}
-
-std::wstring& ToLowerInPlace(std::wstring& text)
-{
- CharLowerBuffW(const_cast<WCHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
- return text;
-}
-
-std::wstring ToLowerCopy(const std::wstring& text)
-{
- std::wstring result(text);
- CharLowerBuffW(const_cast<WCHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
- return result;
-}
-
-std::wstring ToLowerCopy(std::wstring_view text)
-{
- std::wstring result(text.begin(), text.end());
- ToLowerInPlace(result);
- return result;
-}
-
-bool CaseInsenstiveComparePred(wchar_t lhs, wchar_t rhs)
-{
- return std::tolower(lhs, loc) == std::tolower(rhs, loc);
-}
-
-bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs)
-{
- return (lhs.length() == rhs.length())
- && std::equal(lhs.begin(), lhs.end(),
- rhs.begin(),
- [] (wchar_t lhs, wchar_t rhs) -> bool {
- return std::tolower(lhs, loc) == std::tolower(rhs, loc);
- });
-}
-
-VS_FIXEDFILEINFO GetFileVersion(const std::wstring &fileName)
-{
- DWORD handle = 0UL;
- DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
- if (size == 0) {
- throw windows_error("failed to determine file version info size");
- }
-
- boost::scoped_array<char> buffer(new char[size]);
- try {
- handle = 0UL;
- if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) {
- throw windows_error("failed to determine file version info");
- }
-
- void *versionInfoPtr = nullptr;
- UINT versionInfoLength = 0;
- if (!::VerQueryValue(buffer.get(), L"\\", &versionInfoPtr, &versionInfoLength)) {
- throw windows_error("failed to determine file version");
- }
-
- VS_FIXEDFILEINFO result = *(VS_FIXEDFILEINFO*)versionInfoPtr;
- return result;
- } catch (...) {
- throw;
- }
-}
-
-std::wstring GetFileVersionString(const std::wstring &fileName)
-{
- DWORD handle = 0UL;
- DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
- if (size == 0) {
- throw windows_error("failed to determine file version info size");
- }
-
- boost::scoped_array<char> buffer(new char[size]);
- try {
- handle = 0UL;
- if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) {
- throw windows_error("failed to determine file version info");
- }
-
- LPVOID strBuffer = nullptr;
- UINT strLength = 0;
- if (!::VerQueryValue(buffer.get(), L"\\StringFileInfo\\040904B0\\ProductVersion", &strBuffer, &strLength)) {
- throw windows_error("failed to determine file version");
- }
-
- return std::wstring((LPCTSTR)strBuffer);
- }
- catch (...) {
- throw;
- }
-}
-
-VersionInfo createVersionInfo()
-{
- VS_FIXEDFILEINFO version = GetFileVersion(env::thisProcessPath().native());
-
- if (version.dwFileFlags & VS_FF_PRERELEASE)
- {
- // Pre-release builds need annotating
- QString versionString = QString::fromStdWString(
- GetFileVersionString(env::thisProcessPath().native()));
-
- // The pre-release flag can be set without the string specifying what type of pre-release
- bool noLetters = true;
- for (QChar character : versionString)
- {
- if (character.isLetter())
- {
- noLetters = false;
- break;
- }
- }
-
- if (noLetters)
- {
- // Default to pre-alpha when release type is unspecified
- return VersionInfo(version.dwFileVersionMS >> 16,
- version.dwFileVersionMS & 0xFFFF,
- version.dwFileVersionLS >> 16,
- version.dwFileVersionLS & 0xFFFF,
- VersionInfo::RELEASE_PREALPHA);
- }
- else
- {
- // Trust the string to make sense
- return VersionInfo(versionString);
- }
- }
- else
- {
- // Non-pre-release builds just need their version numbers reading
- return VersionInfo(version.dwFileVersionMS >> 16,
- version.dwFileVersionMS & 0xFFFF,
- version.dwFileVersionLS >> 16,
- version.dwFileVersionLS & 0xFFFF);
- }
-}
-
-QString getUsvfsDLLVersion()
-{
- // once 2.2.2 is released, this can be changed to call USVFSVersionString()
- // directly; until then, using GetProcAddress() allows for mixing up devbuilds
- // and usvfs dlls
-
- using USVFSVersionStringType = const char* WINAPI ();
-
- QString s;
-
- const auto m = ::LoadLibraryW(L"usvfs_x64.dll");
-
- if (m) {
- auto* f = reinterpret_cast<USVFSVersionStringType*>(
- ::GetProcAddress(m, "USVFSVersionString"));
-
- if (f) {
- s = f();
- }
-
- ::FreeLibrary(m);
- }
-
- if (s.isEmpty()) {
- s = "?";
- }
-
- return s;
-}
-
-QString getUsvfsVersionString()
-{
- const QString dll = getUsvfsDLLVersion();
- const QString header = USVFS_VERSION_STRING;
-
- QString usvfsVersion;
-
- if (dll == header) {
- return dll;
- } else {
- return "dll is " + dll + ", compiled against " + header;
- }
-}
-
-void SetThisThreadName(const QString& s)
-{
- using SetThreadDescriptionType = HRESULT (
- HANDLE hThread,
- PCWSTR lpThreadDescription
- );
-
- static SetThreadDescriptionType* SetThreadDescription = [] {
- SetThreadDescriptionType* p = nullptr;
-
- env::LibraryPtr kernel32(LoadLibraryW(L"kernel32.dll"));
- if (!kernel32) {
- return p;
- }
-
- p = reinterpret_cast<SetThreadDescriptionType*>(
- GetProcAddress(kernel32.get(), "SetThreadDescription"));
-
- return p;
- }();
-
- if (SetThreadDescription) {
- SetThreadDescription(GetCurrentThread(), s.toStdWString().c_str());
- }
-}
-
-
-char shortcutChar(const QAction* a)
-{
- const auto text = a->text();
- char shortcut = 0;
-
- for (int i=0; i<text.size(); ++i) {
- const auto c = text[i];
- if (c == '&') {
- if (i >= (text.size() - 1)) {
- log::error("ampersand at the end");
- return 0;
- }
-
- return text[i + 1].toLatin1();
- }
- }
-
- log::error("action {} has no shortcut", text);
- return 0;
-}
-
-void checkDuplicateShortcuts(const QMenu& m)
-{
- const auto actions = m.actions();
-
- for (int i=0; i<actions.size(); ++i) {
- const auto* action1 = actions[i];
- if (action1->isSeparator()) {
- continue;
- }
-
- const char shortcut1 = shortcutChar(action1);
- if (shortcut1 == 0) {
- continue;
- }
-
- for (int j=i+1; j<actions.size(); ++j) {
- const auto* action2 = actions[j];
- if (action2->isSeparator()) {
- continue;
- }
-
- const char shortcut2 = shortcutChar(action2);
-
- if (shortcut1 == shortcut2) {
- log::error(
- "duplicate shortcut {} for {} and {}",
- shortcut1, action1->text(), action2->text());
-
- break;
- }
- }
- }
-}
-
-} // namespace MOShared
-
-
-static bool g_exiting = false;
-static bool g_canClose = false;
-
-MainWindow* findMainWindow()
-{
- for (auto* tl : qApp->topLevelWidgets()) {
- if (auto* mw=dynamic_cast<MainWindow*>(tl)) {
- return mw;
- }
- }
-
- return nullptr;
-}
-
-bool ExitModOrganizer(ExitFlags e)
-{
- if (g_exiting) {
- return true;
- }
-
- g_exiting = true;
- Guard g([&]{ g_exiting = false; });
-
- if (!e.testFlag(Exit::Force)) {
- if (auto* mw=findMainWindow()) {
- if (!mw->canExit()) {
- return false;
- }
- }
- }
-
- g_canClose = true;
-
- const int code = (e.testFlag(Exit::Restart) ? RestartExitCode : 0);
- qApp->exit(code);
-
- return true;
-}
-
-bool ModOrganizerCanCloseNow()
-{
- return g_canClose;
-}
-
-bool ModOrganizerExiting()
-{
- return g_exiting;
-}
-
-void ResetExitFlag()
-{
- g_exiting = false;
-}
-
-
-bool isNxmLink(const QString& link)
-{
- return link.startsWith("nxm://", Qt::CaseInsensitive);
-}
+/*
+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 "util.h"
+#include "windows_error.h"
+#include "../mainwindow.h"
+#include "../env.h"
+#include <log.h>
+#include <usvfs.h>
+#include <usvfs_version.h>
+
+using namespace MOBase;
+
+namespace MOShared
+{
+
+bool FileExists(const std::string &filename)
+{
+ DWORD dwAttrib = ::GetFileAttributesA(filename.c_str());
+
+ return (dwAttrib != INVALID_FILE_ATTRIBUTES);
+}
+
+bool FileExists(const std::wstring &filename)
+{
+ DWORD dwAttrib = ::GetFileAttributesW(filename.c_str());
+
+ return (dwAttrib != INVALID_FILE_ATTRIBUTES);
+}
+
+bool FileExists(const std::wstring &searchPath, const std::wstring &filename)
+{
+ std::wstringstream stream;
+ stream << searchPath << "\\" << filename;
+ return FileExists(stream.str());
+}
+
+std::string ToString(const std::wstring &source, bool utf8)
+{
+ std::string result;
+ if (source.length() > 0) {
+ UINT codepage = CP_UTF8;
+ if (!utf8) {
+ codepage = AreFileApisANSI() ? GetACP() : GetOEMCP();
+ }
+ int sizeRequired = ::WideCharToMultiByte(codepage, 0, &source[0], -1, nullptr, 0, nullptr, nullptr);
+ if (sizeRequired == 0) {
+ throw windows_error("failed to convert string to multibyte");
+ }
+ // the size returned by WideCharToMultiByte contains zero termination IF -1 is specified for the length.
+ // we don't want that \0 in the string because then the length field would be wrong. Because madness
+ result.resize(sizeRequired - 1, '\0');
+ ::WideCharToMultiByte(codepage, 0, &source[0], (int)source.size(), &result[0], sizeRequired, nullptr, nullptr);
+ }
+
+ return result;
+}
+
+std::wstring ToWString(const std::string &source, bool utf8)
+{
+ std::wstring result;
+ if (source.length() > 0) {
+ UINT codepage = CP_UTF8;
+ if (!utf8) {
+ codepage = AreFileApisANSI() ? GetACP() : GetOEMCP();
+ }
+ int sizeRequired
+ = ::MultiByteToWideChar(codepage, 0, source.c_str(),
+ static_cast<int>(source.length()), nullptr, 0);
+ if (sizeRequired == 0) {
+ throw windows_error("failed to convert string to wide character");
+ }
+ result.resize(sizeRequired, L'\0');
+ ::MultiByteToWideChar(codepage, 0, source.c_str(),
+ static_cast<int>(source.length()), &result[0],
+ sizeRequired);
+ }
+
+ return result;
+}
+
+static std::locale loc("");
+static auto locToLowerW = [] (wchar_t in) -> wchar_t {
+ return std::tolower(in, loc);
+};
+
+static auto locToLower = [] (char in) -> char {
+ return std::tolower(in, loc);
+};
+
+std::string& ToLowerInPlace(std::string& text)
+{
+ CharLowerBuffA(const_cast<CHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
+ return text;
+}
+
+std::string ToLowerCopy(const std::string& text)
+{
+ std::string result(text);
+ CharLowerBuffA(const_cast<CHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
+ return result;
+}
+
+std::wstring& ToLowerInPlace(std::wstring& text)
+{
+ CharLowerBuffW(const_cast<WCHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
+ return text;
+}
+
+std::wstring ToLowerCopy(const std::wstring& text)
+{
+ std::wstring result(text);
+ CharLowerBuffW(const_cast<WCHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
+ return result;
+}
+
+std::wstring ToLowerCopy(std::wstring_view text)
+{
+ std::wstring result(text.begin(), text.end());
+ ToLowerInPlace(result);
+ return result;
+}
+
+bool CaseInsenstiveComparePred(wchar_t lhs, wchar_t rhs)
+{
+ return std::tolower(lhs, loc) == std::tolower(rhs, loc);
+}
+
+bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs)
+{
+ return (lhs.length() == rhs.length())
+ && std::equal(lhs.begin(), lhs.end(),
+ rhs.begin(),
+ [] (wchar_t lhs, wchar_t rhs) -> bool {
+ return std::tolower(lhs, loc) == std::tolower(rhs, loc);
+ });
+}
+
+VS_FIXEDFILEINFO GetFileVersion(const std::wstring &fileName)
+{
+ DWORD handle = 0UL;
+ DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
+ if (size == 0) {
+ throw windows_error("failed to determine file version info size");
+ }
+
+ boost::scoped_array<char> buffer(new char[size]);
+ try {
+ handle = 0UL;
+ if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) {
+ throw windows_error("failed to determine file version info");
+ }
+
+ void *versionInfoPtr = nullptr;
+ UINT versionInfoLength = 0;
+ if (!::VerQueryValue(buffer.get(), L"\\", &versionInfoPtr, &versionInfoLength)) {
+ throw windows_error("failed to determine file version");
+ }
+
+ VS_FIXEDFILEINFO result = *(VS_FIXEDFILEINFO*)versionInfoPtr;
+ return result;
+ } catch (...) {
+ throw;
+ }
+}
+
+std::wstring GetFileVersionString(const std::wstring &fileName)
+{
+ DWORD handle = 0UL;
+ DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
+ if (size == 0) {
+ throw windows_error("failed to determine file version info size");
+ }
+
+ boost::scoped_array<char> buffer(new char[size]);
+ try {
+ handle = 0UL;
+ if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) {
+ throw windows_error("failed to determine file version info");
+ }
+
+ LPVOID strBuffer = nullptr;
+ UINT strLength = 0;
+ if (!::VerQueryValue(buffer.get(), L"\\StringFileInfo\\040904B0\\ProductVersion", &strBuffer, &strLength)) {
+ throw windows_error("failed to determine file version");
+ }
+
+ return std::wstring((LPCTSTR)strBuffer);
+ }
+ catch (...) {
+ throw;
+ }
+}
+
+VersionInfo createVersionInfo()
+{
+ VS_FIXEDFILEINFO version = GetFileVersion(env::thisProcessPath().native());
+
+ if (version.dwFileFlags & VS_FF_PRERELEASE)
+ {
+ // Pre-release builds need annotating
+ QString versionString = QString::fromStdWString(
+ GetFileVersionString(env::thisProcessPath().native()));
+
+ // The pre-release flag can be set without the string specifying what type of pre-release
+ bool noLetters = true;
+ for (QChar character : versionString)
+ {
+ if (character.isLetter())
+ {
+ noLetters = false;
+ break;
+ }
+ }
+
+ if (noLetters)
+ {
+ // Default to pre-alpha when release type is unspecified
+ return VersionInfo(version.dwFileVersionMS >> 16,
+ version.dwFileVersionMS & 0xFFFF,
+ version.dwFileVersionLS >> 16,
+ version.dwFileVersionLS & 0xFFFF,
+ VersionInfo::RELEASE_PREALPHA);
+ }
+ else
+ {
+ // Trust the string to make sense
+ return VersionInfo(versionString);
+ }
+ }
+ else
+ {
+ // Non-pre-release builds just need their version numbers reading
+ return VersionInfo(version.dwFileVersionMS >> 16,
+ version.dwFileVersionMS & 0xFFFF,
+ version.dwFileVersionLS >> 16,
+ version.dwFileVersionLS & 0xFFFF);
+ }
+}
+
+QString getUsvfsDLLVersion()
+{
+ // once 2.2.2 is released, this can be changed to call USVFSVersionString()
+ // directly; until then, using GetProcAddress() allows for mixing up devbuilds
+ // and usvfs dlls
+
+ using USVFSVersionStringType = const char* WINAPI ();
+
+ QString s;
+
+ const auto m = ::LoadLibraryW(L"usvfs_x64.dll");
+
+ if (m) {
+ auto* f = reinterpret_cast<USVFSVersionStringType*>(
+ ::GetProcAddress(m, "USVFSVersionString"));
+
+ if (f) {
+ s = f();
+ }
+
+ ::FreeLibrary(m);
+ }
+
+ if (s.isEmpty()) {
+ s = "?";
+ }
+
+ return s;
+}
+
+QString getUsvfsVersionString()
+{
+ const QString dll = getUsvfsDLLVersion();
+ const QString header = USVFS_VERSION_STRING;
+
+ QString usvfsVersion;
+
+ if (dll == header) {
+ return dll;
+ } else {
+ return "dll is " + dll + ", compiled against " + header;
+ }
+}
+
+void SetThisThreadName(const QString& s)
+{
+ using SetThreadDescriptionType = HRESULT (
+ HANDLE hThread,
+ PCWSTR lpThreadDescription
+ );
+
+ static SetThreadDescriptionType* SetThreadDescription = [] {
+ SetThreadDescriptionType* p = nullptr;
+
+ env::LibraryPtr kernel32(LoadLibraryW(L"kernel32.dll"));
+ if (!kernel32) {
+ return p;
+ }
+
+ p = reinterpret_cast<SetThreadDescriptionType*>(
+ GetProcAddress(kernel32.get(), "SetThreadDescription"));
+
+ return p;
+ }();
+
+ if (SetThreadDescription) {
+ SetThreadDescription(GetCurrentThread(), s.toStdWString().c_str());
+ }
+}
+
+
+char shortcutChar(const QAction* a)
+{
+ const auto text = a->text();
+ char shortcut = 0;
+
+ for (int i=0; i<text.size(); ++i) {
+ const auto c = text[i];
+ if (c == '&') {
+ if (i >= (text.size() - 1)) {
+ log::error("ampersand at the end");
+ return 0;
+ }
+
+ return text[i + 1].toLatin1();
+ }
+ }
+
+ log::error("action {} has no shortcut", text);
+ return 0;
+}
+
+void checkDuplicateShortcuts(const QMenu& m)
+{
+ const auto actions = m.actions();
+
+ for (int i=0; i<actions.size(); ++i) {
+ const auto* action1 = actions[i];
+ if (action1->isSeparator()) {
+ continue;
+ }
+
+ const char shortcut1 = shortcutChar(action1);
+ if (shortcut1 == 0) {
+ continue;
+ }
+
+ for (int j=i+1; j<actions.size(); ++j) {
+ const auto* action2 = actions[j];
+ if (action2->isSeparator()) {
+ continue;
+ }
+
+ const char shortcut2 = shortcutChar(action2);
+
+ if (shortcut1 == shortcut2) {
+ log::error(
+ "duplicate shortcut {} for {} and {}",
+ shortcut1, action1->text(), action2->text());
+
+ break;
+ }
+ }
+ }
+}
+
+} // namespace MOShared
+
+
+static bool g_exiting = false;
+static bool g_canClose = false;
+
+MainWindow* findMainWindow()
+{
+ for (auto* tl : qApp->topLevelWidgets()) {
+ if (auto* mw=dynamic_cast<MainWindow*>(tl)) {
+ return mw;
+ }
+ }
+
+ return nullptr;
+}
+
+bool ExitModOrganizer(ExitFlags e)
+{
+ if (g_exiting) {
+ return true;
+ }
+
+ g_exiting = true;
+ Guard g([&]{ g_exiting = false; });
+
+ if (!e.testFlag(Exit::Force)) {
+ if (auto* mw=findMainWindow()) {
+ if (!mw->canExit()) {
+ return false;
+ }
+ }
+ }
+
+ g_canClose = true;
+
+ const int code = (e.testFlag(Exit::Restart) ? RestartExitCode : 0);
+ qApp->exit(code);
+
+ return true;
+}
+
+bool ModOrganizerCanCloseNow()
+{
+ return g_canClose;
+}
+
+bool ModOrganizerExiting()
+{
+ return g_exiting;
+}
+
+void ResetExitFlag()
+{
+ g_exiting = false;
+}
+
+
+bool isNxmLink(const QString& link)
+{
+ return link.startsWith("nxm://", Qt::CaseInsensitive);
+}
diff --git a/src/shared/util.h b/src/shared/util.h
index d8fdb3a3..87ec5bf2 100644
--- a/src/shared/util.h
+++ b/src/shared/util.h
@@ -1,89 +1,89 @@
-/*
-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 UTIL_H
-#define UTIL_H
-
-#include <log.h>
-#include <string>
-#include <filesystem>
-#include <versioninfo.h>
-
-class Executable;
-
-namespace MOShared {
-
-/// Test if a file (or directory) by the specified name exists
-bool FileExists(const std::string &filename);
-bool FileExists(const std::wstring &filename);
-
-bool FileExists(const std::wstring &searchPath, const std::wstring &filename);
-
-std::string ToString(const std::wstring &source, bool utf8);
-std::wstring ToWString(const std::string &source, bool utf8);
-
-std::string& ToLowerInPlace(std::string& text);
-std::string ToLowerCopy(const std::string& text);
-
-std::wstring& ToLowerInPlace(std::wstring& text);
-std::wstring ToLowerCopy(const std::wstring& text);
-std::wstring ToLowerCopy(std::wstring_view text);
-
-bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs);
-
-MOBase::VersionInfo createVersionInfo();
-QString getUsvfsVersionString();
-
-void SetThisThreadName(const QString& s);
-void checkDuplicateShortcuts(const QMenu& m);
-
-inline FILETIME ToFILETIME(std::filesystem::file_time_type t)
-{
- FILETIME ft;
- static_assert(sizeof(t) == sizeof(ft));
-
- std::memcpy(&ft, &t, sizeof(FILETIME));
- return ft;
-}
-
-} // namespace MOShared
-
-
-enum class Exit
-{
- None = 0x00,
- Normal = 0x01,
- Restart = 0x02,
- Force = 0x04
-};
-
-const int RestartExitCode = INT_MAX;
-const int ReselectExitCode = INT_MAX - 1;
-
-using ExitFlags = QFlags<Exit>;
-Q_DECLARE_OPERATORS_FOR_FLAGS(ExitFlags);
-
-bool ExitModOrganizer(ExitFlags e=Exit::Normal);
-bool ModOrganizerExiting();
-bool ModOrganizerCanCloseNow();
-void ResetExitFlag();
-
-bool isNxmLink(const QString& link);
-
-#endif // UTIL_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 UTIL_H
+#define UTIL_H
+
+#include <log.h>
+#include <string>
+#include <filesystem>
+#include <versioninfo.h>
+
+class Executable;
+
+namespace MOShared {
+
+/// Test if a file (or directory) by the specified name exists
+bool FileExists(const std::string &filename);
+bool FileExists(const std::wstring &filename);
+
+bool FileExists(const std::wstring &searchPath, const std::wstring &filename);
+
+std::string ToString(const std::wstring &source, bool utf8);
+std::wstring ToWString(const std::string &source, bool utf8);
+
+std::string& ToLowerInPlace(std::string& text);
+std::string ToLowerCopy(const std::string& text);
+
+std::wstring& ToLowerInPlace(std::wstring& text);
+std::wstring ToLowerCopy(const std::wstring& text);
+std::wstring ToLowerCopy(std::wstring_view text);
+
+bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs);
+
+MOBase::VersionInfo createVersionInfo();
+QString getUsvfsVersionString();
+
+void SetThisThreadName(const QString& s);
+void checkDuplicateShortcuts(const QMenu& m);
+
+inline FILETIME ToFILETIME(std::filesystem::file_time_type t)
+{
+ FILETIME ft;
+ static_assert(sizeof(t) == sizeof(ft));
+
+ std::memcpy(&ft, &t, sizeof(FILETIME));
+ return ft;
+}
+
+} // namespace MOShared
+
+
+enum class Exit
+{
+ None = 0x00,
+ Normal = 0x01,
+ Restart = 0x02,
+ Force = 0x04
+};
+
+const int RestartExitCode = INT_MAX;
+const int ReselectExitCode = INT_MAX - 1;
+
+using ExitFlags = QFlags<Exit>;
+Q_DECLARE_OPERATORS_FOR_FLAGS(ExitFlags);
+
+bool ExitModOrganizer(ExitFlags e=Exit::Normal);
+bool ModOrganizerExiting();
+bool ModOrganizerCanCloseNow();
+void ResetExitFlag();
+
+bool isNxmLink(const QString& link);
+
+#endif // UTIL_H
diff --git a/src/shared/windows_error.cpp b/src/shared/windows_error.cpp
index 97a58a20..49fb9bc7 100644
--- a/src/shared/windows_error.cpp
+++ b/src/shared/windows_error.cpp
@@ -1,49 +1,49 @@
-/*
-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 "windows_error.h"
-#include <sstream>
-
-namespace MOShared {
-
-std::string windows_error::constructMessage(const std::string& input, int inErrorCode)
-{
- std::ostringstream finalMessage;
- finalMessage << input;
-
- LPSTR buffer = nullptr;
-
- DWORD errorCode = inErrorCode != -1 ? inErrorCode : ::GetLastError();
-
- // TODO: the message is not english?
- if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
- nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&buffer, 0, nullptr) == 0) {
- finalMessage << " (errorcode " << errorCode << ")";
- } else {
- LPSTR lastChar = buffer + strlen(buffer) - 2;
- *lastChar = '\0';
- finalMessage << " (" << buffer << " [" << errorCode << "])";
- LocalFree(buffer); // allocated by FormatMessage
- }
-
- ::SetLastError(errorCode); // restore error code because FormatMessage might have modified it
- return finalMessage.str();
-}
-
-} // namespace MOShared
+/*
+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 "windows_error.h"
+#include <sstream>
+
+namespace MOShared {
+
+std::string windows_error::constructMessage(const std::string& input, int inErrorCode)
+{
+ std::ostringstream finalMessage;
+ finalMessage << input;
+
+ LPSTR buffer = nullptr;
+
+ DWORD errorCode = inErrorCode != -1 ? inErrorCode : ::GetLastError();
+
+ // TODO: the message is not english?
+ if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+ nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&buffer, 0, nullptr) == 0) {
+ finalMessage << " (errorcode " << errorCode << ")";
+ } else {
+ LPSTR lastChar = buffer + strlen(buffer) - 2;
+ *lastChar = '\0';
+ finalMessage << " (" << buffer << " [" << errorCode << "])";
+ LocalFree(buffer); // allocated by FormatMessage
+ }
+
+ ::SetLastError(errorCode); // restore error code because FormatMessage might have modified it
+ return finalMessage.str();
+}
+
+} // namespace MOShared
diff --git a/src/shared/windows_error.h b/src/shared/windows_error.h
index 03ce0abd..ea506dc1 100644
--- a/src/shared/windows_error.h
+++ b/src/shared/windows_error.h
@@ -1,44 +1,44 @@
-/*
-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 WINDOWS_ERROR_H
-#define WINDOWS_ERROR_H
-
-
-#include <stdexcept>
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-
-namespace MOShared {
-
-class windows_error : public std::runtime_error {
-public:
- windows_error(const std::string& message, int errorcode = ::GetLastError())
- : runtime_error(constructMessage(message, errorcode)), m_ErrorCode(errorcode)
- {}
- int getErrorCode() const { return m_ErrorCode; }
-private:
- std::string constructMessage(const std::string& input, int errorcode);
-private:
- int m_ErrorCode;
-};
-
-} // namespace MOShared
-
-#endif // WINDOWS_ERROR_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 WINDOWS_ERROR_H
+#define WINDOWS_ERROR_H
+
+
+#include <stdexcept>
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+namespace MOShared {
+
+class windows_error : public std::runtime_error {
+public:
+ windows_error(const std::string& message, int errorcode = ::GetLastError())
+ : runtime_error(constructMessage(message, errorcode)), m_ErrorCode(errorcode)
+ {}
+ int getErrorCode() const { return m_ErrorCode; }
+private:
+ std::string constructMessage(const std::string& input, int errorcode);
+private:
+ int m_ErrorCode;
+};
+
+} // namespace MOShared
+
+#endif // WINDOWS_ERROR_H
diff --git a/src/spawn.cpp b/src/spawn.cpp
index 5b14c52c..858c3061 100644
--- a/src/spawn.cpp
+++ b/src/spawn.cpp
@@ -1,1082 +1,1082 @@
-/*
-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 "spawn.h"
-
-#include "report.h"
-#include "utility.h"
-#include "env.h"
-#include "envwindows.h"
-#include "envsecurity.h"
-#include "envmodule.h"
-#include "settings.h"
-#include "settingsdialogworkarounds.h"
-#include <errorcodes.h>
-#include <report.h>
-#include <log.h>
-#include <usvfs.h>
-#include <Shellapi.h>
-#include "shared/appconfig.h"
-#include "shared/windows_error.h"
-#include <QApplication>
-#include <QMessageBox>
-#include <QtDebug>
-#include <Shellapi.h>
-#include <fmt/format.h>
-#include <fmt/xchar.h>
-
-using namespace MOBase;
-using namespace MOShared;
-
-namespace spawn::dialogs
-{
-
-std::wstring makeRightsDetails(const env::FileSecurity& fs)
-{
- if (fs.rights.normalRights) {
- return L"(normal rights)";
- }
-
- if (fs.rights.list.isEmpty()) {
- return L"(none)";
- }
-
- std::wstring s = fs.rights.list.join("|").toStdWString();
- if (!fs.rights.hasExecute) {
- s += L" (execute is missing)";
- }
-
- return s;
-}
-
-QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more={})
-{
- std::wstring owner, rights;
-
- if (sp.binary.isFile()) {
- const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath());
-
- if (fs.error.isEmpty()) {
- owner = fs.owner.toStdWString();
- rights = makeRightsDetails(fs);
- } else {
- owner = fs.error.toStdWString();
- rights = fs.error.toStdWString();
- }
- } else {
- owner = L"(file not found)";
- rights = L"(file not found)";
- }
-
- const bool cwdExists = (sp.currentDirectory.isEmpty() ?
- true : sp.currentDirectory.exists());
-
- const auto appDir = QCoreApplication::applicationDirPath();
- const auto sep = QDir::separator();
-
- const std::wstring usvfs_x86_dll =
- QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
-
- const std::wstring usvfs_x64_dll =
- QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
-
- const std::wstring usvfs_x86_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found";
-
- const std::wstring usvfs_x64_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found";
-
- std::wstring elevated;
- if (auto b=env::Environment().windowsInfo().isElevated()) {
- elevated = (*b ? L"yes" : L"no");
- } else {
- elevated = L"(not available)";
- }
-
- std::wstring f =
- L"Error {code} {codename}{more}: {error}\n"
- L" . binary: '{bin}'\n"
- L" . owner: {owner}\n"
- L" . rights: {rights}\n"
- L" . arguments: '{args}'\n"
- L" . cwd: '{cwd}'{cwdexists}\n"
- L" . stdout: {stdout}, stderr: {stderr}, hooked: {hooked}\n"
- L" . MO elevated: {elevated}";
-
- if (sp.hooked) {
- f += L"\n . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}";
- }
-
- const std::wstring wmore = (more.isEmpty() ? L"" : (", " + more).toStdWString());
-
- const auto s = fmt::format(f,
- fmt::arg(L"code", code),
- fmt::arg(L"codename", errorCodeName(code)),
- fmt::arg(L"more", wmore),
- fmt::arg(L"bin", QDir::toNativeSeparators(sp.binary.absoluteFilePath()).toStdWString()),
- fmt::arg(L"owner", owner),
- fmt::arg(L"rights", rights),
- fmt::arg(L"error", formatSystemMessage(code)),
- fmt::arg(L"args", sp.arguments.toStdWString()),
- fmt::arg(L"cwd", QDir::toNativeSeparators(sp.currentDirectory.absolutePath()).toStdWString()),
- fmt::arg(L"cwdexists", (cwdExists ? L"" : L" (not found)")),
- fmt::arg(L"stdout", (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes")),
- fmt::arg(L"stderr", (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes")),
- fmt::arg(L"hooked", (sp.hooked ? L"yes" : L"no")),
- fmt::arg(L"x86_dll", usvfs_x86_dll),
- fmt::arg(L"x64_dll", usvfs_x64_dll),
- fmt::arg(L"x86_proxy", usvfs_x86_proxy),
- fmt::arg(L"x64_proxy", usvfs_x64_proxy),
- fmt::arg(L"elevated", elevated));
-
- return QString::fromStdWString(s);
-}
-
-QString makeContent(const SpawnParameters& sp, DWORD code)
-{
- if (code == ERROR_INVALID_PARAMETER) {
- return QObject::tr(
- "This error typically happens because an antivirus has deleted critical "
- "files from Mod Organizer's installation folder or has made them "
- "generally inaccessible. Add an exclusion for Mod Organizer's "
- "installation folder in your antivirus, reinstall Mod Organizer and try "
- "again.");
- } else if (code == ERROR_ACCESS_DENIED) {
- return QObject::tr(
- "This error typically happens because an antivirus is preventing Mod "
- "Organizer from starting programs. Add an exclusion for Mod Organizer's "
- "installation folder in your antivirus and try again.");
- } else if (code == ERROR_FILE_NOT_FOUND) {
- return QObject::tr("The file '%1' does not exist.")
- .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath()));
- } else if (code == ERROR_DIRECTORY) {
- if (!sp.currentDirectory.exists()) {
- return QObject::tr("The working directory '%1' does not exist.")
- .arg(QDir::toNativeSeparators(sp.currentDirectory.absolutePath()));
- }
- }
-
- return QString::fromStdWString(formatSystemMessage(code));
-}
-
-QMessageBox::StandardButton badSteamReg(
- QWidget* parent, const QString& keyName, const QString& valueName)
-{
- const auto details = QString(
- "can't start steam, registry value at '%1' is empty or doesn't exist")
- .arg(keyName + "\\" + valueName);
-
- log::error("{}", details);
-
- return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
- .main(QObject::tr("Cannot start Steam"))
- .content(QObject::tr(
- "The path to the Steam executable cannot be found. You might try "
- "reinstalling Steam."))
- .details(details)
- .icon(QMessageBox::Critical)
- .button({
- QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."),
- QMessageBox::Yes})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .exec();
-}
-
-QMessageBox::StandardButton startSteamFailed(
- QWidget* parent,
- const QString& keyName, const QString& valueName, const QString& exe,
- const SpawnParameters& sp, DWORD e)
-{
- auto details = QString(
- "a steam install was found in the registry at '%1': '%2'\n\n")
- .arg(keyName + "\\" + valueName)
- .arg(exe);
-
- details += makeDetails(sp, e);
-
- log::error("{}", details);
-
- return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
- .main(QObject::tr("Cannot start Steam"))
- .content(makeContent(sp, e))
- .details(details)
- .icon(QMessageBox::Critical)
- .button({
- QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."),
- QMessageBox::Yes})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .exec();
-}
-
-void spawnFailed(QWidget* parent, const SpawnParameters& sp, DWORD code)
-{
- const auto details = makeDetails(sp, code);
- log::error("{}", details);
-
- const auto title = QObject::tr("Cannot launch program");
-
- const auto mainText = QObject::tr("Cannot start %1")
- .arg(sp.binary.fileName());
-
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(makeContent(sp, code))
- .details(details)
- .icon(QMessageBox::Critical)
- .exec();
-}
-
-void helperFailed(
- QWidget* parent, DWORD code, const QString& why, const std::wstring& binary,
- const std::wstring& cwd, const std::wstring& args)
-{
- SpawnParameters sp;
- sp.binary = QFileInfo(QString::fromStdWString(binary));
- sp.currentDirectory.setPath(QString::fromStdWString(cwd));
- sp.arguments = QString::fromStdWString(args);
-
- const auto details = makeDetails(sp, code, "in " + why);
- log::error("{}", details);
-
- const auto title = QObject::tr("Cannot launch helper");
-
- const auto mainText = QObject::tr("Cannot start %1")
- .arg(sp.binary.fileName());
-
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(makeContent(sp, code))
- .details(details)
- .icon(QMessageBox::Critical)
- .exec();
-}
-
-bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp)
-{
- const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED);
-
- log::error("{}", details);
-
- const auto title = QObject::tr("Elevation required");
-
- const auto mainText = QObject::tr("Cannot start %1")
- .arg(sp.binary.fileName());
-
- const auto content = QObject::tr(
- "This program is requesting to run as administrator but Mod Organizer "
- "itself is not running as administrator. Running programs as administrator "
- "is typically unnecessary as long as the game and Mod Organizer have been "
- "installed outside \"Program Files\".\r\n\r\n"
- "You can restart Mod Organizer as administrator and try launching the "
- "program again.");
-
- log::debug("asking user to restart MO as administrator");
-
- const auto r = MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .button({
- QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
- QMessageBox::Yes})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .exec();
-
- return (r == QMessageBox::Yes);
-}
-
-QMessageBox::StandardButton confirmStartSteam(
- QWidget* parent, const SpawnParameters& sp, const QString& details)
-{
- const auto title = QObject::tr("Launch Steam");
- const auto mainText = QObject::tr("This program requires Steam");
- const auto content = QObject::tr(
- "Mod Organizer has detected that this program likely requires Steam to be "
- "running to function properly.");
-
- return MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .button({
- QObject::tr("Start Steam"),
- QMessageBox::Yes})
- .button({
- QObject::tr("Continue without starting Steam"),
- QObject::tr("The program might fail to run."),
- QMessageBox::No})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .remember("steamQuery", sp.binary.fileName())
- .exec();
-}
-
-QMessageBox::StandardButton confirmRestartAsAdminForSteam(
- QWidget* parent, const SpawnParameters& sp)
-{
- const auto title = QObject::tr("Elevation required");
- const auto mainText = QObject::tr("Steam is running as administrator");
- const auto content = QObject::tr(
- "Running Steam as administrator is typically unnecessary and can cause "
- "problems when Mod Organizer itself is not running as administrator."
- "\r\n\r\n"
- "You can restart Mod Organizer as administrator and try launching the "
- "program again.");
-
- return MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .icon(QMessageBox::Question)
- .button({
- QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
- QMessageBox::Yes})
- .button({
- QObject::tr("Continue"),
- QObject::tr("The program might fail to run."),
- QMessageBox::No})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .remember("steamAdminQuery", sp.binary.fileName())
- .exec();
-}
-
-bool eventLogNotRunning(
- QWidget* parent, const env::Service& s, const SpawnParameters& sp)
-{
- const auto title = QObject::tr("Event Log not running");
- const auto mainText = QObject::tr("The Event Log service is not running");
- const auto content = QObject::tr(
- "The Windows Event Log service is not running. This can prevent USVFS from "
- "running properly and your mods may not be recognized by the program being "
- "launched.");
-
- const auto r = MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(s.toString())
- .icon(QMessageBox::Question)
- .remember("eventLogService", sp.binary.fileName())
- .button({
- QObject::tr("Continue"),
- QObject::tr("Your mods might not work."),
- QMessageBox::Yes})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .exec();
-
- return (r == QMessageBox::Yes);
-}
-
-QMessageBox::StandardButton confirmBlacklisted(
- QWidget* parent, const SpawnParameters& sp, Settings& settings)
-{
- const auto title = QObject::tr("Blacklisted program");
- const auto mainText = QObject::tr("The program %1 is blacklisted")
- .arg(sp.binary.fileName());
- const auto content = QObject::tr(
- "The program you are attempting to launch is blacklisted in the virtual "
- "filesystem. This will likely prevent it from seeing any mods, INI files "
- "or any other virtualized files.");
-
- const auto details =
- "Executable: " + sp.binary.fileName() + "\n"
- "Current blacklist: " + settings.executablesBlacklist();
-
- auto r = MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .remember("blacklistedExecutable", sp.binary.fileName())
- .button({
- QObject::tr("Continue"),
- QObject::tr("Your mods might not work."),
- QMessageBox::Yes})
- .button({
- QObject::tr("Change the blacklist"),
- QMessageBox::Retry})
- .button({
- QObject::tr("Cancel"),
- QMessageBox::Cancel})
- .exec();
-
- if (r == QMessageBox::Retry) {
- if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) {
- r = QMessageBox::Cancel;
- }
- }
-
- return r;
-}
-
-} // namespace
-
-
-namespace spawn
-{
-
-void logSpawning(const SpawnParameters& sp, const QString& realCmd)
-{
- log::debug(
- "spawning binary:\n"
- " . exe: '{}'\n"
- " . args: '{}'\n"
- " . cwd: '{}'\n"
- " . steam id: '{}'\n"
- " . hooked: {}\n"
- " . stdout: {}\n"
- " . stderr: {}\n"
- " . real cmd: '{}'",
- sp.binary.absoluteFilePath(), sp.arguments,
- sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked,
- (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"),
- (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"),
- realCmd);
-}
-
-DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
-{
- BOOL inheritHandles = FALSE;
-
- STARTUPINFO si = {};
- si.cb = sizeof(si);
-
- // inherit handles if we plan to use stdout or stderr reroute
- if (sp.stdOut != INVALID_HANDLE_VALUE) {
- si.hStdOutput = sp.stdOut;
- inheritHandles = TRUE;
- si.dwFlags |= STARTF_USESTDHANDLES;
- }
-
- if (sp.stdErr != INVALID_HANDLE_VALUE) {
- si.hStdError = sp.stdErr;
- inheritHandles = TRUE;
- si.dwFlags |= STARTF_USESTDHANDLES;
- }
-
- const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath());
- const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath());
-
- QString commandLine = "\"" + bin + "\"";
- if (!sp.arguments.isEmpty()) {
- commandLine += " " + sp.arguments;
- }
-
- const QString moPath = QCoreApplication::applicationDirPath();
- const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
-
- PROCESS_INFORMATION pi = {};
- BOOL success = FALSE;
-
- logSpawning(sp, commandLine);
-
- const auto wcommandLine = commandLine.toStdWString();
- const auto wcwd = cwd.toStdWString();
-
- const DWORD flags = CREATE_BREAKAWAY_FROM_JOB;
-
- if (sp.hooked) {
- success = ::CreateProcessHooked(
- nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
- inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
- } else {
- success = ::CreateProcess(
- nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
- inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
- }
-
- const auto e = GetLastError();
- env::setPath(oldPath);
-
- if (!success) {
- return e;
- }
-
- processHandle = pi.hProcess;
- ::CloseHandle(pi.hThread);
-
- return ERROR_SUCCESS;
-}
-
-bool restartAsAdmin(QWidget* parent)
-{
- WCHAR cwd[MAX_PATH] = {};
- if (!GetCurrentDirectory(MAX_PATH, cwd)) {
- cwd[0] = L'\0';
- }
-
- if (!helper::adminLaunch(
- parent,
- qApp->applicationDirPath().toStdWString(),
- qApp->applicationFilePath().toStdWString(),
- std::wstring(cwd)))
- {
- log::error("admin launch failed");
- return false;
- }
-
- log::debug("exiting MO");
- ExitModOrganizer(Exit::Force);
-
- return true;
-}
-
-void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp)
-{
- if (!dialogs::confirmRestartAsAdmin(parent, sp)) {
- log::debug("user declined");
- return;
- }
-
- log::info("restarting MO as administrator");
- restartAsAdmin(parent);
-}
-
-struct SteamStatus
-{
- bool running=false;
- bool accessible=false;
-};
-
-SteamStatus getSteamStatus()
-{
- SteamStatus ss;
-
- const auto ps = env::Environment().runningProcesses();
-
- for (const auto& p : ps) {
- if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) ||
- (p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0))
- {
- ss.running = true;
- ss.accessible = p.canAccess();
-
- log::debug(
- "'{}' is running, accessible={}",
- p.name(), (ss.accessible ? "yes" : "no"));
-
- break;
- }
- }
-
- return ss;
-}
-
-QString makeSteamArguments(const QString& username, const QString& password)
-{
- QString args;
-
- if (username != "") {
- args += "-login " + username;
-
- if (password != "") {
- args += " " + password;
- }
- }
-
- return args;
-}
-
-bool startSteam(QWidget* parent)
-{
- const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
- const QString valueName = "SteamExe";
-
- const QSettings steamSettings(keyName, QSettings::NativeFormat);
- const QString exe = steamSettings.value(valueName, "").toString();
-
- if (exe.isEmpty()) {
- return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes);
- }
-
- SpawnParameters sp;
- sp.binary = QFileInfo(exe);
-
- // See if username and password supplied. If so, pass them into steam.
- QString username, password;
- if (Settings::instance().steam().login(username, password)) {
- if (username.length() > 0)
- MOBase::log::getDefault().addToBlacklist(username.toStdString(), "STEAM_USERNAME");
- if (password.length() > 0)
- MOBase::log::getDefault().addToBlacklist(password.toStdString(), "STEAM_PASSWORD");
- sp.arguments = makeSteamArguments(username, password);
- }
-
- log::debug(
- "starting steam process:\n"
- " . program: '{}'\n"
- " . username={}, password={}",
- sp.binary.filePath().toStdString(),
- (username.isEmpty() ? "no" : "yes"),
- (password.isEmpty() ? "no" : "yes"));
-
- HANDLE ph = INVALID_HANDLE_VALUE;
- const auto e = spawn(sp, ph);
- ::CloseHandle(ph);
-
- if (e != ERROR_SUCCESS) {
- // make sure username and passwords are not shown
- sp.arguments = makeSteamArguments(
- (username.isEmpty() ? "" : "USERNAME"),
- (password.isEmpty() ? "" : "PASSWORD"));
-
- const auto r = dialogs::startSteamFailed(
- parent, keyName, valueName, exe, sp, e);
-
- return (r == QMessageBox::Yes);
- }
-
- QMessageBox::information(
- parent, QObject::tr("Waiting"),
- QObject::tr("Please press OK once you're logged into steam."));
-
- return true;
-}
-
-bool checkSteam(
- QWidget* parent, const SpawnParameters& sp,
- const QDir& gameDirectory, const QString &steamAppID, const Settings& settings)
-{
- static const std::vector<QString> steamFiles = {
- "steam_api.dll", "steam_api64.dll"
- };
-
- log::debug("checking steam");
-
- if (!steamAppID.isEmpty()) {
- env::set("SteamAPPId", steamAppID);
- } else {
- env::set("SteamAPPId", settings.steam().appID());
- }
-
-
- bool steamRequired = false;
- QString details;
-
- for (const auto& file : steamFiles) {
- const QFileInfo fi(gameDirectory.absoluteFilePath(file));
- if (fi.exists()) {
- details = QString(
- "managed game is located at '%1' and file '%2' exists")
- .arg(gameDirectory.absolutePath())
- .arg(fi.absoluteFilePath());
-
- log::debug("{}", details);
- steamRequired = true;
-
- break;
- }
- }
-
- if (!steamRequired) {
- log::debug("program doesn't seem to require steam");
- return true;
- }
-
-
- auto ss = getSteamStatus();
-
- if (!ss.running) {
- log::debug("steam isn't running, asking to start steam");
-
- const auto c = dialogs::confirmStartSteam(parent, sp, details);
-
- if (c == QDialogButtonBox::Yes) {
- log::debug("user wants to start steam");
-
- if (!startSteam(parent)) {
- // cancel
- return false;
- }
-
- // double-check that Steam is started
- ss = getSteamStatus();
- if (!ss.running) {
- log::error("steam is still not running, hoping for the best");
- return true;
- }
- } else if (c == QDialogButtonBox::No) {
- log::debug("user declined to start steam");
- return true;
- } else {
- log::debug("user cancelled");
- return false;
- }
- }
-
- if (ss.running && !ss.accessible) {
- log::debug("steam is running but is not accessible, asking to restart MO");
- const auto c = dialogs::confirmRestartAsAdminForSteam(parent, sp);
-
- if (c == QDialogButtonBox::Yes) {
- restartAsAdmin(parent);
- return false;
- } else if (c == QDialogButtonBox::No) {
- log::debug("user declined to restart MO, continuing");
- return true;
- } else {
- log::debug("user cancelled");
- return false;
- }
- }
-
- return true;
-}
-
-bool checkBlacklist(
- QWidget* parent, const SpawnParameters& sp, Settings& settings)
-{
- for (;;) {
- if (!settings.isExecutableBlacklisted(sp.binary.fileName())) {
- return true;
- }
-
- const auto r = dialogs::confirmBlacklisted(parent, sp, settings);
-
- if (r != QMessageBox::Retry) {
- return (r == QMessageBox::Yes);
- }
- }
-}
-
-HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
-{
- HANDLE handle = INVALID_HANDLE_VALUE;
- const auto e = spawn::spawn(sp, handle);
-
- switch (e)
- {
- case ERROR_SUCCESS:
- {
- return handle;
- }
-
- case ERROR_ELEVATION_REQUIRED:
- {
- startBinaryAdmin(parent, sp);
- return INVALID_HANDLE_VALUE;
- }
-
- default:
- {
- dialogs::spawnFailed(parent, sp, e);
- return INVALID_HANDLE_VALUE;
- }
- }
-}
-
-QString getExecutableForJarFile(const QString& jarFile)
-{
- const std::wstring jarFileW = jarFile.toStdWString();
-
- WCHAR buffer[MAX_PATH];
-
- const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer);
- const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
-
- // anything <= 32 signals failure
- if (r <= 32) {
- log::warn(
- "failed to find executable associated with file '{}', {}",
- jarFile, shell::formatError(r));
-
- return {};
- }
-
- DWORD binaryType = 0;
-
- if (!::GetBinaryTypeW(buffer, &binaryType)) {
- const auto e = ::GetLastError();
-
- log::warn(
- "failed to determine binary type of '{}', {}",
- QString::fromWCharArray(buffer), formatSystemMessage(e));
-
- return {};
- }
-
- if (binaryType != SCS_32BIT_BINARY && binaryType != SCS_64BIT_BINARY) {
- log::warn(
- "unexpected binary type {} for file '{}'",
- binaryType, QString::fromWCharArray(buffer));
-
- return {};
- }
-
- return QString::fromWCharArray(buffer);
-}
-
-QString getJavaHome()
-{
- const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment";
- const QString value = "CurrentVersion";
-
- QSettings reg(key, QSettings::NativeFormat);
-
- if (!reg.contains(value)) {
- log::warn("key '{}\\{}' doesn't exist", key, value);
- return {};
- }
-
- const QString currentVersion = reg.value("CurrentVersion").toString();
- const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
-
- if (!reg.contains(javaHome)) {
- log::warn(
- "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
- currentVersion, key, value, key, javaHome);
-
- return {};
- }
-
- const auto path = reg.value(javaHome).toString();
- return path + "\\bin\\javaw.exe";
-}
-
-QString findJavaInstallation(const QString& jarFile)
-{
- // try to find java automatically based on the given jar file
- if (!jarFile.isEmpty()) {
- const auto s = getExecutableForJarFile(jarFile);
- if (!s.isEmpty()) {
- return s;
- }
- }
-
- // second attempt: look to the registry
- const auto s = getJavaHome();
- if (!s.isEmpty()) {
- return s;
- }
-
- // not found
- return {};
-}
-
-bool isBatchFile(const QFileInfo& target)
-{
- const auto batchExtensions = {"cmd", "bat"};
-
- const QString extension = target.suffix();
- for (auto&& e : batchExtensions) {
- if (extension.compare(e, Qt::CaseInsensitive) == 0) {
- return true;
- }
- }
-
- return false;
-}
-
-bool isExeFile(const QFileInfo& target)
-{
- return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
-}
-
-bool isJavaFile(const QFileInfo& target)
-{
- return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
-}
-
-QFileInfo getCmdPath()
-{
- const auto p = env::get("COMSPEC");
- if (!p.isEmpty()) {
- return QFileInfo(p);
- }
-
- QString systemDirectory;
-
- const std::size_t buffer_size = 1000;
- wchar_t buffer[buffer_size + 1] = {};
-
- const auto length = ::GetSystemDirectoryW(buffer, buffer_size);
- if (length != 0) {
- systemDirectory = QString::fromWCharArray(buffer, length);
-
- if (!systemDirectory.endsWith("\\")) {
- systemDirectory += "\\";
- }
- } else {
- systemDirectory = "C:\\Windows\\System32\\";
- }
-
- return QFileInfo(systemDirectory + "cmd.exe");
-}
-
-FileExecutionTypes getFileExecutionType(const QFileInfo& target)
-{
- if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) {
- return FileExecutionTypes::Executable;
- }
-
- return FileExecutionTypes::Other;
-}
-
-FileExecutionContext getFileExecutionContext(
- QWidget* parent, const QFileInfo& target)
-{
- if (isExeFile(target)) {
- return {
- target,
- "",
- FileExecutionTypes::Executable
- };
- }
-
- if (isBatchFile(target)) {
- return {
- getCmdPath(),
- QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
- FileExecutionTypes::Executable
- };
- }
-
- if (isJavaFile(target)) {
- auto java = findJavaInstallation(target.absoluteFilePath());
-
- if (java.isEmpty()) {
- java = QFileDialog::getOpenFileName(
- parent, QObject::tr("Select binary"),
- QString(), QObject::tr("Binary") + " (*.exe)");
- }
-
- if (!java.isEmpty()) {
- return {
- QFileInfo(java),
- QString("-jar \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
- FileExecutionTypes::Executable
- };
- }
- }
-
- return {{}, {}, FileExecutionTypes::Other};
-}
-
-} // namespace
-
-
-
-namespace helper
-{
-
-bool helperExec(
- QWidget* parent,
- const std::wstring& moDirectory, const std::wstring& commandLine, BOOL async)
-{
- const std::wstring fileName = moDirectory + L"\\helper.exe";
-
- env::HandlePtr process;
-
- {
- SHELLEXECUTEINFOW execInfo = {};
-
- ULONG flags = SEE_MASK_FLAG_NO_UI;
- if (!async)
- flags |= SEE_MASK_NOCLOSEPROCESS;
-
- execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
- execInfo.fMask = flags;
- execInfo.hwnd = 0;
- execInfo.lpVerb = L"runas";
- execInfo.lpFile = fileName.c_str();
- execInfo.lpParameters = commandLine.c_str();
- execInfo.lpDirectory = moDirectory.c_str();
- execInfo.nShow = SW_SHOW;
-
- if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) {
- const auto e = GetLastError();
-
- spawn::dialogs::helperFailed(
- parent, e, "ShellExecuteExW()", fileName, moDirectory, commandLine);
-
- return false;
- }
-
- if (async) {
- return true;
- }
-
- process.reset(execInfo.hProcess);
- }
-
- const auto r = ::WaitForSingleObject(process.get(), INFINITE);
-
- if (r != WAIT_OBJECT_0) {
- // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError()
- // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so
- // use that instead
- const auto code = (r == WAIT_ABANDONED ?
- ERROR_ABANDONED_WAIT_0 : GetLastError());
-
- spawn::dialogs::helperFailed(
- parent, code, "WaitForSingleObject()",
- fileName, moDirectory, commandLine);
-
- return false;
- }
-
- DWORD exitCode = 0;
- if (!GetExitCodeProcess(process.get(), &exitCode)) {
- const auto e = GetLastError();
-
- spawn::dialogs::helperFailed(
- parent, e, "GetExitCodeProcess()", fileName, moDirectory, commandLine);
-
- return false;
- }
-
- return (exitCode == 0);
-}
-
-bool backdateBSAs(
- QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath)
-{
- const std::wstring commandLine = fmt::format(
- L"backdateBSA \"{}\"", dataPath);
-
- return helperExec(parent, moPath, commandLine, FALSE);
-}
-
-bool adminLaunch(
- QWidget* parent, const std::wstring &moPath,
- const std::wstring &moFile, const std::wstring &workingDir)
-{
- const std::wstring commandLine = fmt::format(
- L"adminLaunch {} \"{}\" \"{}\"",
- ::GetCurrentProcessId(), moFile, workingDir);
-
- return helperExec(parent, moPath, commandLine, true);
-}
-
-} // namespace
+/*
+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 "spawn.h"
+
+#include "report.h"
+#include "utility.h"
+#include "env.h"
+#include "envwindows.h"
+#include "envsecurity.h"
+#include "envmodule.h"
+#include "settings.h"
+#include "settingsdialogworkarounds.h"
+#include <errorcodes.h>
+#include <report.h>
+#include <log.h>
+#include <usvfs.h>
+#include <Shellapi.h>
+#include "shared/appconfig.h"
+#include "shared/windows_error.h"
+#include <QApplication>
+#include <QMessageBox>
+#include <QtDebug>
+#include <Shellapi.h>
+#include <fmt/format.h>
+#include <fmt/xchar.h>
+
+using namespace MOBase;
+using namespace MOShared;
+
+namespace spawn::dialogs
+{
+
+std::wstring makeRightsDetails(const env::FileSecurity& fs)
+{
+ if (fs.rights.normalRights) {
+ return L"(normal rights)";
+ }
+
+ if (fs.rights.list.isEmpty()) {
+ return L"(none)";
+ }
+
+ std::wstring s = fs.rights.list.join("|").toStdWString();
+ if (!fs.rights.hasExecute) {
+ s += L" (execute is missing)";
+ }
+
+ return s;
+}
+
+QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more={})
+{
+ std::wstring owner, rights;
+
+ if (sp.binary.isFile()) {
+ const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath());
+
+ if (fs.error.isEmpty()) {
+ owner = fs.owner.toStdWString();
+ rights = makeRightsDetails(fs);
+ } else {
+ owner = fs.error.toStdWString();
+ rights = fs.error.toStdWString();
+ }
+ } else {
+ owner = L"(file not found)";
+ rights = L"(file not found)";
+ }
+
+ const bool cwdExists = (sp.currentDirectory.isEmpty() ?
+ true : sp.currentDirectory.exists());
+
+ const auto appDir = QCoreApplication::applicationDirPath();
+ const auto sep = QDir::separator();
+
+ const std::wstring usvfs_x86_dll =
+ QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
+
+ const std::wstring usvfs_x64_dll =
+ QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
+
+ const std::wstring usvfs_x86_proxy =
+ QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found";
+
+ const std::wstring usvfs_x64_proxy =
+ QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found";
+
+ std::wstring elevated;
+ if (auto b=env::Environment().windowsInfo().isElevated()) {
+ elevated = (*b ? L"yes" : L"no");
+ } else {
+ elevated = L"(not available)";
+ }
+
+ std::wstring f =
+ L"Error {code} {codename}{more}: {error}\n"
+ L" . binary: '{bin}'\n"
+ L" . owner: {owner}\n"
+ L" . rights: {rights}\n"
+ L" . arguments: '{args}'\n"
+ L" . cwd: '{cwd}'{cwdexists}\n"
+ L" . stdout: {stdout}, stderr: {stderr}, hooked: {hooked}\n"
+ L" . MO elevated: {elevated}";
+
+ if (sp.hooked) {
+ f += L"\n . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}";
+ }
+
+ const std::wstring wmore = (more.isEmpty() ? L"" : (", " + more).toStdWString());
+
+ const auto s = fmt::format(f,
+ fmt::arg(L"code", code),
+ fmt::arg(L"codename", errorCodeName(code)),
+ fmt::arg(L"more", wmore),
+ fmt::arg(L"bin", QDir::toNativeSeparators(sp.binary.absoluteFilePath()).toStdWString()),
+ fmt::arg(L"owner", owner),
+ fmt::arg(L"rights", rights),
+ fmt::arg(L"error", formatSystemMessage(code)),
+ fmt::arg(L"args", sp.arguments.toStdWString()),
+ fmt::arg(L"cwd", QDir::toNativeSeparators(sp.currentDirectory.absolutePath()).toStdWString()),
+ fmt::arg(L"cwdexists", (cwdExists ? L"" : L" (not found)")),
+ fmt::arg(L"stdout", (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes")),
+ fmt::arg(L"stderr", (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes")),
+ fmt::arg(L"hooked", (sp.hooked ? L"yes" : L"no")),
+ fmt::arg(L"x86_dll", usvfs_x86_dll),
+ fmt::arg(L"x64_dll", usvfs_x64_dll),
+ fmt::arg(L"x86_proxy", usvfs_x86_proxy),
+ fmt::arg(L"x64_proxy", usvfs_x64_proxy),
+ fmt::arg(L"elevated", elevated));
+
+ return QString::fromStdWString(s);
+}
+
+QString makeContent(const SpawnParameters& sp, DWORD code)
+{
+ if (code == ERROR_INVALID_PARAMETER) {
+ return QObject::tr(
+ "This error typically happens because an antivirus has deleted critical "
+ "files from Mod Organizer's installation folder or has made them "
+ "generally inaccessible. Add an exclusion for Mod Organizer's "
+ "installation folder in your antivirus, reinstall Mod Organizer and try "
+ "again.");
+ } else if (code == ERROR_ACCESS_DENIED) {
+ return QObject::tr(
+ "This error typically happens because an antivirus is preventing Mod "
+ "Organizer from starting programs. Add an exclusion for Mod Organizer's "
+ "installation folder in your antivirus and try again.");
+ } else if (code == ERROR_FILE_NOT_FOUND) {
+ return QObject::tr("The file '%1' does not exist.")
+ .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath()));
+ } else if (code == ERROR_DIRECTORY) {
+ if (!sp.currentDirectory.exists()) {
+ return QObject::tr("The working directory '%1' does not exist.")
+ .arg(QDir::toNativeSeparators(sp.currentDirectory.absolutePath()));
+ }
+ }
+
+ return QString::fromStdWString(formatSystemMessage(code));
+}
+
+QMessageBox::StandardButton badSteamReg(
+ QWidget* parent, const QString& keyName, const QString& valueName)
+{
+ const auto details = QString(
+ "can't start steam, registry value at '%1' is empty or doesn't exist")
+ .arg(keyName + "\\" + valueName);
+
+ log::error("{}", details);
+
+ return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
+ .main(QObject::tr("Cannot start Steam"))
+ .content(QObject::tr(
+ "The path to the Steam executable cannot be found. You might try "
+ "reinstalling Steam."))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .button({
+ QObject::tr("Continue without starting Steam"),
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+}
+
+QMessageBox::StandardButton startSteamFailed(
+ QWidget* parent,
+ const QString& keyName, const QString& valueName, const QString& exe,
+ const SpawnParameters& sp, DWORD e)
+{
+ auto details = QString(
+ "a steam install was found in the registry at '%1': '%2'\n\n")
+ .arg(keyName + "\\" + valueName)
+ .arg(exe);
+
+ details += makeDetails(sp, e);
+
+ log::error("{}", details);
+
+ return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
+ .main(QObject::tr("Cannot start Steam"))
+ .content(makeContent(sp, e))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .button({
+ QObject::tr("Continue without starting Steam"),
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+}
+
+void spawnFailed(QWidget* parent, const SpawnParameters& sp, DWORD code)
+{
+ const auto details = makeDetails(sp, code);
+ log::error("{}", details);
+
+ const auto title = QObject::tr("Cannot launch program");
+
+ const auto mainText = QObject::tr("Cannot start %1")
+ .arg(sp.binary.fileName());
+
+ MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(makeContent(sp, code))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .exec();
+}
+
+void helperFailed(
+ QWidget* parent, DWORD code, const QString& why, const std::wstring& binary,
+ const std::wstring& cwd, const std::wstring& args)
+{
+ SpawnParameters sp;
+ sp.binary = QFileInfo(QString::fromStdWString(binary));
+ sp.currentDirectory.setPath(QString::fromStdWString(cwd));
+ sp.arguments = QString::fromStdWString(args);
+
+ const auto details = makeDetails(sp, code, "in " + why);
+ log::error("{}", details);
+
+ const auto title = QObject::tr("Cannot launch helper");
+
+ const auto mainText = QObject::tr("Cannot start %1")
+ .arg(sp.binary.fileName());
+
+ MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(makeContent(sp, code))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .exec();
+}
+
+bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp)
+{
+ const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED);
+
+ log::error("{}", details);
+
+ const auto title = QObject::tr("Elevation required");
+
+ const auto mainText = QObject::tr("Cannot start %1")
+ .arg(sp.binary.fileName());
+
+ const auto content = QObject::tr(
+ "This program is requesting to run as administrator but Mod Organizer "
+ "itself is not running as administrator. Running programs as administrator "
+ "is typically unnecessary as long as the game and Mod Organizer have been "
+ "installed outside \"Program Files\".\r\n\r\n"
+ "You can restart Mod Organizer as administrator and try launching the "
+ "program again.");
+
+ log::debug("asking user to restart MO as administrator");
+
+ const auto r = MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .button({
+ QObject::tr("Restart Mod Organizer as administrator"),
+ QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+
+ return (r == QMessageBox::Yes);
+}
+
+QMessageBox::StandardButton confirmStartSteam(
+ QWidget* parent, const SpawnParameters& sp, const QString& details)
+{
+ const auto title = QObject::tr("Launch Steam");
+ const auto mainText = QObject::tr("This program requires Steam");
+ const auto content = QObject::tr(
+ "Mod Organizer has detected that this program likely requires Steam to be "
+ "running to function properly.");
+
+ return MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .button({
+ QObject::tr("Start Steam"),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Continue without starting Steam"),
+ QObject::tr("The program might fail to run."),
+ QMessageBox::No})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .remember("steamQuery", sp.binary.fileName())
+ .exec();
+}
+
+QMessageBox::StandardButton confirmRestartAsAdminForSteam(
+ QWidget* parent, const SpawnParameters& sp)
+{
+ const auto title = QObject::tr("Elevation required");
+ const auto mainText = QObject::tr("Steam is running as administrator");
+ const auto content = QObject::tr(
+ "Running Steam as administrator is typically unnecessary and can cause "
+ "problems when Mod Organizer itself is not running as administrator."
+ "\r\n\r\n"
+ "You can restart Mod Organizer as administrator and try launching the "
+ "program again.");
+
+ return MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .icon(QMessageBox::Question)
+ .button({
+ QObject::tr("Restart Mod Organizer as administrator"),
+ QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Continue"),
+ QObject::tr("The program might fail to run."),
+ QMessageBox::No})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .remember("steamAdminQuery", sp.binary.fileName())
+ .exec();
+}
+
+bool eventLogNotRunning(
+ QWidget* parent, const env::Service& s, const SpawnParameters& sp)
+{
+ const auto title = QObject::tr("Event Log not running");
+ const auto mainText = QObject::tr("The Event Log service is not running");
+ const auto content = QObject::tr(
+ "The Windows Event Log service is not running. This can prevent USVFS from "
+ "running properly and your mods may not be recognized by the program being "
+ "launched.");
+
+ const auto r = MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(s.toString())
+ .icon(QMessageBox::Question)
+ .remember("eventLogService", sp.binary.fileName())
+ .button({
+ QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+
+ return (r == QMessageBox::Yes);
+}
+
+QMessageBox::StandardButton confirmBlacklisted(
+ QWidget* parent, const SpawnParameters& sp, Settings& settings)
+{
+ const auto title = QObject::tr("Blacklisted program");
+ const auto mainText = QObject::tr("The program %1 is blacklisted")
+ .arg(sp.binary.fileName());
+ const auto content = QObject::tr(
+ "The program you are attempting to launch is blacklisted in the virtual "
+ "filesystem. This will likely prevent it from seeing any mods, INI files "
+ "or any other virtualized files.");
+
+ const auto details =
+ "Executable: " + sp.binary.fileName() + "\n"
+ "Current blacklist: " + settings.executablesBlacklist();
+
+ auto r = MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .remember("blacklistedExecutable", sp.binary.fileName())
+ .button({
+ QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Change the blacklist"),
+ QMessageBox::Retry})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+
+ if (r == QMessageBox::Retry) {
+ if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) {
+ r = QMessageBox::Cancel;
+ }
+ }
+
+ return r;
+}
+
+} // namespace
+
+
+namespace spawn
+{
+
+void logSpawning(const SpawnParameters& sp, const QString& realCmd)
+{
+ log::debug(
+ "spawning binary:\n"
+ " . exe: '{}'\n"
+ " . args: '{}'\n"
+ " . cwd: '{}'\n"
+ " . steam id: '{}'\n"
+ " . hooked: {}\n"
+ " . stdout: {}\n"
+ " . stderr: {}\n"
+ " . real cmd: '{}'",
+ sp.binary.absoluteFilePath(), sp.arguments,
+ sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked,
+ (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"),
+ (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"),
+ realCmd);
+}
+
+DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
+{
+ BOOL inheritHandles = FALSE;
+
+ STARTUPINFO si = {};
+ si.cb = sizeof(si);
+
+ // inherit handles if we plan to use stdout or stderr reroute
+ if (sp.stdOut != INVALID_HANDLE_VALUE) {
+ si.hStdOutput = sp.stdOut;
+ inheritHandles = TRUE;
+ si.dwFlags |= STARTF_USESTDHANDLES;
+ }
+
+ if (sp.stdErr != INVALID_HANDLE_VALUE) {
+ si.hStdError = sp.stdErr;
+ inheritHandles = TRUE;
+ si.dwFlags |= STARTF_USESTDHANDLES;
+ }
+
+ const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath());
+ const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath());
+
+ QString commandLine = "\"" + bin + "\"";
+ if (!sp.arguments.isEmpty()) {
+ commandLine += " " + sp.arguments;
+ }
+
+ const QString moPath = QCoreApplication::applicationDirPath();
+ const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
+
+ PROCESS_INFORMATION pi = {};
+ BOOL success = FALSE;
+
+ logSpawning(sp, commandLine);
+
+ const auto wcommandLine = commandLine.toStdWString();
+ const auto wcwd = cwd.toStdWString();
+
+ const DWORD flags = CREATE_BREAKAWAY_FROM_JOB;
+
+ if (sp.hooked) {
+ success = ::CreateProcessHooked(
+ nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
+ } else {
+ success = ::CreateProcess(
+ nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
+ }
+
+ const auto e = GetLastError();
+ env::setPath(oldPath);
+
+ if (!success) {
+ return e;
+ }
+
+ processHandle = pi.hProcess;
+ ::CloseHandle(pi.hThread);
+
+ return ERROR_SUCCESS;
+}
+
+bool restartAsAdmin(QWidget* parent)
+{
+ WCHAR cwd[MAX_PATH] = {};
+ if (!GetCurrentDirectory(MAX_PATH, cwd)) {
+ cwd[0] = L'\0';
+ }
+
+ if (!helper::adminLaunch(
+ parent,
+ qApp->applicationDirPath().toStdWString(),
+ qApp->applicationFilePath().toStdWString(),
+ std::wstring(cwd)))
+ {
+ log::error("admin launch failed");
+ return false;
+ }
+
+ log::debug("exiting MO");
+ ExitModOrganizer(Exit::Force);
+
+ return true;
+}
+
+void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp)
+{
+ if (!dialogs::confirmRestartAsAdmin(parent, sp)) {
+ log::debug("user declined");
+ return;
+ }
+
+ log::info("restarting MO as administrator");
+ restartAsAdmin(parent);
+}
+
+struct SteamStatus
+{
+ bool running=false;
+ bool accessible=false;
+};
+
+SteamStatus getSteamStatus()
+{
+ SteamStatus ss;
+
+ const auto ps = env::Environment().runningProcesses();
+
+ for (const auto& p : ps) {
+ if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) ||
+ (p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0))
+ {
+ ss.running = true;
+ ss.accessible = p.canAccess();
+
+ log::debug(
+ "'{}' is running, accessible={}",
+ p.name(), (ss.accessible ? "yes" : "no"));
+
+ break;
+ }
+ }
+
+ return ss;
+}
+
+QString makeSteamArguments(const QString& username, const QString& password)
+{
+ QString args;
+
+ if (username != "") {
+ args += "-login " + username;
+
+ if (password != "") {
+ args += " " + password;
+ }
+ }
+
+ return args;
+}
+
+bool startSteam(QWidget* parent)
+{
+ const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
+ const QString valueName = "SteamExe";
+
+ const QSettings steamSettings(keyName, QSettings::NativeFormat);
+ const QString exe = steamSettings.value(valueName, "").toString();
+
+ if (exe.isEmpty()) {
+ return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes);
+ }
+
+ SpawnParameters sp;
+ sp.binary = QFileInfo(exe);
+
+ // See if username and password supplied. If so, pass them into steam.
+ QString username, password;
+ if (Settings::instance().steam().login(username, password)) {
+ if (username.length() > 0)
+ MOBase::log::getDefault().addToBlacklist(username.toStdString(), "STEAM_USERNAME");
+ if (password.length() > 0)
+ MOBase::log::getDefault().addToBlacklist(password.toStdString(), "STEAM_PASSWORD");
+ sp.arguments = makeSteamArguments(username, password);
+ }
+
+ log::debug(
+ "starting steam process:\n"
+ " . program: '{}'\n"
+ " . username={}, password={}",
+ sp.binary.filePath().toStdString(),
+ (username.isEmpty() ? "no" : "yes"),
+ (password.isEmpty() ? "no" : "yes"));
+
+ HANDLE ph = INVALID_HANDLE_VALUE;
+ const auto e = spawn(sp, ph);
+ ::CloseHandle(ph);
+
+ if (e != ERROR_SUCCESS) {
+ // make sure username and passwords are not shown
+ sp.arguments = makeSteamArguments(
+ (username.isEmpty() ? "" : "USERNAME"),
+ (password.isEmpty() ? "" : "PASSWORD"));
+
+ const auto r = dialogs::startSteamFailed(
+ parent, keyName, valueName, exe, sp, e);
+
+ return (r == QMessageBox::Yes);
+ }
+
+ QMessageBox::information(
+ parent, QObject::tr("Waiting"),
+ QObject::tr("Please press OK once you're logged into steam."));
+
+ return true;
+}
+
+bool checkSteam(
+ QWidget* parent, const SpawnParameters& sp,
+ const QDir& gameDirectory, const QString &steamAppID, const Settings& settings)
+{
+ static const std::vector<QString> steamFiles = {
+ "steam_api.dll", "steam_api64.dll"
+ };
+
+ log::debug("checking steam");
+
+ if (!steamAppID.isEmpty()) {
+ env::set("SteamAPPId", steamAppID);
+ } else {
+ env::set("SteamAPPId", settings.steam().appID());
+ }
+
+
+ bool steamRequired = false;
+ QString details;
+
+ for (const auto& file : steamFiles) {
+ const QFileInfo fi(gameDirectory.absoluteFilePath(file));
+ if (fi.exists()) {
+ details = QString(
+ "managed game is located at '%1' and file '%2' exists")
+ .arg(gameDirectory.absolutePath())
+ .arg(fi.absoluteFilePath());
+
+ log::debug("{}", details);
+ steamRequired = true;
+
+ break;
+ }
+ }
+
+ if (!steamRequired) {
+ log::debug("program doesn't seem to require steam");
+ return true;
+ }
+
+
+ auto ss = getSteamStatus();
+
+ if (!ss.running) {
+ log::debug("steam isn't running, asking to start steam");
+
+ const auto c = dialogs::confirmStartSteam(parent, sp, details);
+
+ if (c == QDialogButtonBox::Yes) {
+ log::debug("user wants to start steam");
+
+ if (!startSteam(parent)) {
+ // cancel
+ return false;
+ }
+
+ // double-check that Steam is started
+ ss = getSteamStatus();
+ if (!ss.running) {
+ log::error("steam is still not running, hoping for the best");
+ return true;
+ }
+ } else if (c == QDialogButtonBox::No) {
+ log::debug("user declined to start steam");
+ return true;
+ } else {
+ log::debug("user cancelled");
+ return false;
+ }
+ }
+
+ if (ss.running && !ss.accessible) {
+ log::debug("steam is running but is not accessible, asking to restart MO");
+ const auto c = dialogs::confirmRestartAsAdminForSteam(parent, sp);
+
+ if (c == QDialogButtonBox::Yes) {
+ restartAsAdmin(parent);
+ return false;
+ } else if (c == QDialogButtonBox::No) {
+ log::debug("user declined to restart MO, continuing");
+ return true;
+ } else {
+ log::debug("user cancelled");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool checkBlacklist(
+ QWidget* parent, const SpawnParameters& sp, Settings& settings)
+{
+ for (;;) {
+ if (!settings.isExecutableBlacklisted(sp.binary.fileName())) {
+ return true;
+ }
+
+ const auto r = dialogs::confirmBlacklisted(parent, sp, settings);
+
+ if (r != QMessageBox::Retry) {
+ return (r == QMessageBox::Yes);
+ }
+ }
+}
+
+HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
+{
+ HANDLE handle = INVALID_HANDLE_VALUE;
+ const auto e = spawn::spawn(sp, handle);
+
+ switch (e)
+ {
+ case ERROR_SUCCESS:
+ {
+ return handle;
+ }
+
+ case ERROR_ELEVATION_REQUIRED:
+ {
+ startBinaryAdmin(parent, sp);
+ return INVALID_HANDLE_VALUE;
+ }
+
+ default:
+ {
+ dialogs::spawnFailed(parent, sp, e);
+ return INVALID_HANDLE_VALUE;
+ }
+ }
+}
+
+QString getExecutableForJarFile(const QString& jarFile)
+{
+ const std::wstring jarFileW = jarFile.toStdWString();
+
+ WCHAR buffer[MAX_PATH];
+
+ const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer);
+ const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
+
+ // anything <= 32 signals failure
+ if (r <= 32) {
+ log::warn(
+ "failed to find executable associated with file '{}', {}",
+ jarFile, shell::formatError(r));
+
+ return {};
+ }
+
+ DWORD binaryType = 0;
+
+ if (!::GetBinaryTypeW(buffer, &binaryType)) {
+ const auto e = ::GetLastError();
+
+ log::warn(
+ "failed to determine binary type of '{}', {}",
+ QString::fromWCharArray(buffer), formatSystemMessage(e));
+
+ return {};
+ }
+
+ if (binaryType != SCS_32BIT_BINARY && binaryType != SCS_64BIT_BINARY) {
+ log::warn(
+ "unexpected binary type {} for file '{}'",
+ binaryType, QString::fromWCharArray(buffer));
+
+ return {};
+ }
+
+ return QString::fromWCharArray(buffer);
+}
+
+QString getJavaHome()
+{
+ const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment";
+ const QString value = "CurrentVersion";
+
+ QSettings reg(key, QSettings::NativeFormat);
+
+ if (!reg.contains(value)) {
+ log::warn("key '{}\\{}' doesn't exist", key, value);
+ return {};
+ }
+
+ const QString currentVersion = reg.value("CurrentVersion").toString();
+ const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
+
+ if (!reg.contains(javaHome)) {
+ log::warn(
+ "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
+ currentVersion, key, value, key, javaHome);
+
+ return {};
+ }
+
+ const auto path = reg.value(javaHome).toString();
+ return path + "\\bin\\javaw.exe";
+}
+
+QString findJavaInstallation(const QString& jarFile)
+{
+ // try to find java automatically based on the given jar file
+ if (!jarFile.isEmpty()) {
+ const auto s = getExecutableForJarFile(jarFile);
+ if (!s.isEmpty()) {
+ return s;
+ }
+ }
+
+ // second attempt: look to the registry
+ const auto s = getJavaHome();
+ if (!s.isEmpty()) {
+ return s;
+ }
+
+ // not found
+ return {};
+}
+
+bool isBatchFile(const QFileInfo& target)
+{
+ const auto batchExtensions = {"cmd", "bat"};
+
+ const QString extension = target.suffix();
+ for (auto&& e : batchExtensions) {
+ if (extension.compare(e, Qt::CaseInsensitive) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool isExeFile(const QFileInfo& target)
+{
+ return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
+}
+
+bool isJavaFile(const QFileInfo& target)
+{
+ return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
+}
+
+QFileInfo getCmdPath()
+{
+ const auto p = env::get("COMSPEC");
+ if (!p.isEmpty()) {
+ return QFileInfo(p);
+ }
+
+ QString systemDirectory;
+
+ const std::size_t buffer_size = 1000;
+ wchar_t buffer[buffer_size + 1] = {};
+
+ const auto length = ::GetSystemDirectoryW(buffer, buffer_size);
+ if (length != 0) {
+ systemDirectory = QString::fromWCharArray(buffer, length);
+
+ if (!systemDirectory.endsWith("\\")) {
+ systemDirectory += "\\";
+ }
+ } else {
+ systemDirectory = "C:\\Windows\\System32\\";
+ }
+
+ return QFileInfo(systemDirectory + "cmd.exe");
+}
+
+FileExecutionTypes getFileExecutionType(const QFileInfo& target)
+{
+ if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) {
+ return FileExecutionTypes::Executable;
+ }
+
+ return FileExecutionTypes::Other;
+}
+
+FileExecutionContext getFileExecutionContext(
+ QWidget* parent, const QFileInfo& target)
+{
+ if (isExeFile(target)) {
+ return {
+ target,
+ "",
+ FileExecutionTypes::Executable
+ };
+ }
+
+ if (isBatchFile(target)) {
+ return {
+ getCmdPath(),
+ QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable
+ };
+ }
+
+ if (isJavaFile(target)) {
+ auto java = findJavaInstallation(target.absoluteFilePath());
+
+ if (java.isEmpty()) {
+ java = QFileDialog::getOpenFileName(
+ parent, QObject::tr("Select binary"),
+ QString(), QObject::tr("Binary") + " (*.exe)");
+ }
+
+ if (!java.isEmpty()) {
+ return {
+ QFileInfo(java),
+ QString("-jar \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable
+ };
+ }
+ }
+
+ return {{}, {}, FileExecutionTypes::Other};
+}
+
+} // namespace
+
+
+
+namespace helper
+{
+
+bool helperExec(
+ QWidget* parent,
+ const std::wstring& moDirectory, const std::wstring& commandLine, BOOL async)
+{
+ const std::wstring fileName = moDirectory + L"\\helper.exe";
+
+ env::HandlePtr process;
+
+ {
+ SHELLEXECUTEINFOW execInfo = {};
+
+ ULONG flags = SEE_MASK_FLAG_NO_UI;
+ if (!async)
+ flags |= SEE_MASK_NOCLOSEPROCESS;
+
+ execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ execInfo.fMask = flags;
+ execInfo.hwnd = 0;
+ execInfo.lpVerb = L"runas";
+ execInfo.lpFile = fileName.c_str();
+ execInfo.lpParameters = commandLine.c_str();
+ execInfo.lpDirectory = moDirectory.c_str();
+ execInfo.nShow = SW_SHOW;
+
+ if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) {
+ const auto e = GetLastError();
+
+ spawn::dialogs::helperFailed(
+ parent, e, "ShellExecuteExW()", fileName, moDirectory, commandLine);
+
+ return false;
+ }
+
+ if (async) {
+ return true;
+ }
+
+ process.reset(execInfo.hProcess);
+ }
+
+ const auto r = ::WaitForSingleObject(process.get(), INFINITE);
+
+ if (r != WAIT_OBJECT_0) {
+ // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError()
+ // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so
+ // use that instead
+ const auto code = (r == WAIT_ABANDONED ?
+ ERROR_ABANDONED_WAIT_0 : GetLastError());
+
+ spawn::dialogs::helperFailed(
+ parent, code, "WaitForSingleObject()",
+ fileName, moDirectory, commandLine);
+
+ return false;
+ }
+
+ DWORD exitCode = 0;
+ if (!GetExitCodeProcess(process.get(), &exitCode)) {
+ const auto e = GetLastError();
+
+ spawn::dialogs::helperFailed(
+ parent, e, "GetExitCodeProcess()", fileName, moDirectory, commandLine);
+
+ return false;
+ }
+
+ return (exitCode == 0);
+}
+
+bool backdateBSAs(
+ QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath)
+{
+ const std::wstring commandLine = fmt::format(
+ L"backdateBSA \"{}\"", dataPath);
+
+ return helperExec(parent, moPath, commandLine, FALSE);
+}
+
+bool adminLaunch(
+ QWidget* parent, const std::wstring &moPath,
+ const std::wstring &moFile, const std::wstring &workingDir)
+{
+ const std::wstring commandLine = fmt::format(
+ L"adminLaunch {} \"{}\" \"{}\"",
+ ::GetCurrentProcessId(), moFile, workingDir);
+
+ return helperExec(parent, moPath, commandLine, true);
+}
+
+} // namespace
diff --git a/src/spawn.h b/src/spawn.h
index 0628781e..44f690b0 100644
--- a/src/spawn.h
+++ b/src/spawn.h
@@ -1,121 +1,121 @@
-/*
-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 SPAWN_H
-#define SPAWN_H
-
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <tchar.h>
-#include <QFileInfo>
-#include <QDir>
-
-class Settings;
-
-namespace spawn
-{
-
-/*
- * @param binary the binary to spawn
- * @param arguments arguments to pass to the binary
- * @param profileName name of the active profile
- * @param currentDirectory the directory to use as the working directory to run in
- * @param logLevel log level to be used by the hook library. Ignored if hooked is false
- * @param hooked if set, the binary is started with mo injected
- * @param stdout if not equal to INVALID_HANDLE_VALUE, this is used as stdout for the process
- * @param stderr if not equal to INVALID_HANDLE_VALUE, this is used as stderr for the process
-*/
-struct SpawnParameters
-{
- QFileInfo binary;
- QString arguments;
- QDir currentDirectory;
- QString steamAppID;
- bool hooked = false;
- HANDLE stdOut = INVALID_HANDLE_VALUE;
- HANDLE stdErr = INVALID_HANDLE_VALUE;
-};
-
-
-bool checkSteam(
- QWidget* parent, const SpawnParameters& sp,
- const QDir& gameDirectory, const QString &steamAppID, const Settings& settings);
-
-bool checkBlacklist(
- QWidget* parent, const SpawnParameters& sp, Settings& settings);
-
-/**
- * @brief spawn a binary with Mod Organizer injected
- * @return the process handle
- **/
-HANDLE startBinary(QWidget* parent, const SpawnParameters& sp);
-
-
-enum class FileExecutionTypes
-{
- Executable = 1,
- Other
-};
-
-struct FileExecutionContext
-{
- QFileInfo binary;
- QString arguments;
- FileExecutionTypes type;
-};
-
-QString findJavaInstallation(const QString& jarFile);
-
-FileExecutionContext getFileExecutionContext(
- QWidget* parent, const QFileInfo& target);
-
-FileExecutionTypes getFileExecutionType(const QFileInfo& target);
-
-} // namespace
-
-
-// convenience functions to work with the external helper program, which is used
-// to make changes on the system that require administrative rights, so that
-// ModOrganizer itself can run without special privileges
-//
-namespace helper
-{
-
-/**
-* @brief sets the last modified time for all .bsa-files in the target directory well into the past
-* @param moPath absolute path to the modOrganizer base directory
-* @param dataPath the path taht contains the .bsa-files, usually the data directory of the game
-**/
-bool backdateBSAs(
- QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath);
-
-/**
-* @brief waits for the current process to exit and restarts it as an administrator
-* @param moPath absolute path to the modOrganizer base directory
-* @param moFile file name of modOrganizer
-* @param workingDir current working directory
-**/
-bool adminLaunch(
- QWidget* parent, const std::wstring &moPath,
- const std::wstring &moFile, const std::wstring &workingDir);
-
-} // namespace
-
-#endif // SPAWN_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 SPAWN_H
+#define SPAWN_H
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <tchar.h>
+#include <QFileInfo>
+#include <QDir>
+
+class Settings;
+
+namespace spawn
+{
+
+/*
+ * @param binary the binary to spawn
+ * @param arguments arguments to pass to the binary
+ * @param profileName name of the active profile
+ * @param currentDirectory the directory to use as the working directory to run in
+ * @param logLevel log level to be used by the hook library. Ignored if hooked is false
+ * @param hooked if set, the binary is started with mo injected
+ * @param stdout if not equal to INVALID_HANDLE_VALUE, this is used as stdout for the process
+ * @param stderr if not equal to INVALID_HANDLE_VALUE, this is used as stderr for the process
+*/
+struct SpawnParameters
+{
+ QFileInfo binary;
+ QString arguments;
+ QDir currentDirectory;
+ QString steamAppID;
+ bool hooked = false;
+ HANDLE stdOut = INVALID_HANDLE_VALUE;
+ HANDLE stdErr = INVALID_HANDLE_VALUE;
+};
+
+
+bool checkSteam(
+ QWidget* parent, const SpawnParameters& sp,
+ const QDir& gameDirectory, const QString &steamAppID, const Settings& settings);
+
+bool checkBlacklist(
+ QWidget* parent, const SpawnParameters& sp, Settings& settings);
+
+/**
+ * @brief spawn a binary with Mod Organizer injected
+ * @return the process handle
+ **/
+HANDLE startBinary(QWidget* parent, const SpawnParameters& sp);
+
+
+enum class FileExecutionTypes
+{
+ Executable = 1,
+ Other
+};
+
+struct FileExecutionContext
+{
+ QFileInfo binary;
+ QString arguments;
+ FileExecutionTypes type;
+};
+
+QString findJavaInstallation(const QString& jarFile);
+
+FileExecutionContext getFileExecutionContext(
+ QWidget* parent, const QFileInfo& target);
+
+FileExecutionTypes getFileExecutionType(const QFileInfo& target);
+
+} // namespace
+
+
+// convenience functions to work with the external helper program, which is used
+// to make changes on the system that require administrative rights, so that
+// ModOrganizer itself can run without special privileges
+//
+namespace helper
+{
+
+/**
+* @brief sets the last modified time for all .bsa-files in the target directory well into the past
+* @param moPath absolute path to the modOrganizer base directory
+* @param dataPath the path taht contains the .bsa-files, usually the data directory of the game
+**/
+bool backdateBSAs(
+ QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath);
+
+/**
+* @brief waits for the current process to exit and restarts it as an administrator
+* @param moPath absolute path to the modOrganizer base directory
+* @param moFile file name of modOrganizer
+* @param workingDir current working directory
+**/
+bool adminLaunch(
+ QWidget* parent, const std::wstring &moPath,
+ const std::wstring &moFile, const std::wstring &workingDir);
+
+} // namespace
+
+#endif // SPAWN_H
+
diff --git a/src/syncoverwritedialog.cpp b/src/syncoverwritedialog.cpp
index 41c2b202..1966463b 100644
--- a/src/syncoverwritedialog.cpp
+++ b/src/syncoverwritedialog.cpp
@@ -1,171 +1,171 @@
-/*
-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 "syncoverwritedialog.h"
-#include "shared/directoryentry.h"
-#include "shared/fileentry.h"
-#include "shared/filesorigin.h"
-#include "ui_syncoverwritedialog.h"
-
-#include <utility.h>
-#include <report.h>
-#include <log.h>
-
-#include <QDir>
-#include <QDirIterator>
-#include <QComboBox>
-#include <QStringList>
-
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-SyncOverwriteDialog::SyncOverwriteDialog(const QString &path, DirectoryEntry *directoryStructure, QWidget *parent)
- : TutorableDialog("SyncOverwrite", parent),
- ui(new Ui::SyncOverwriteDialog), m_SourcePath(path), m_DirectoryStructure(directoryStructure)
-{
- ui->setupUi(this);
- refresh(path);
-
- QHeaderView *headerView = ui->syncTree->header();
-#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
- headerView->setSectionResizeMode(0, QHeaderView::Stretch);
- headerView->setSectionResizeMode(1, QHeaderView::Interactive);
-#else
- headerView->setResizeMode(0, QHeaderView::Stretch);
- headerView->setResizeMode(1, QHeaderView::Interactive);
-#endif
-}
-
-
-SyncOverwriteDialog::~SyncOverwriteDialog()
-{
- delete ui;
-}
-
-
-static void addToComboBox(QComboBox *box, const QString &name, const QVariant &userData)
-{
- if (QString::compare(name, "overwrite", Qt::CaseInsensitive) != 0) {
- box->addItem(name, userData);
- }
-}
-
-
-void SyncOverwriteDialog::readTree(const QString &path, DirectoryEntry *directoryStructure, QTreeWidgetItem *subTree)
-{
- QDir overwrite(path);
- overwrite.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
- QDirIterator dirIter(overwrite);
- while (dirIter.hasNext()) {
- dirIter.next();
- QFileInfo fileInfo = dirIter.fileInfo();
-
- QString file = fileInfo.fileName();
- if (file == "meta.ini") {
- continue;
- }
-
- QTreeWidgetItem *newItem = new QTreeWidgetItem(subTree, QStringList(file));
-
- if (fileInfo.isDir()) {
- DirectoryEntry *subDir = directoryStructure->findSubDirectory(ToWString(file));
- if (subDir != nullptr) {
- readTree(fileInfo.absoluteFilePath(), subDir, newItem);
- } else {
- log::error("no directory structure for {}?", file);
- delete newItem;
- newItem = nullptr;
- }
- } else {
- const FileEntryPtr entry = directoryStructure->findFile(ToWString(file));
- QComboBox* combo = new QComboBox(ui->syncTree);
- combo->addItem(tr("<don't sync>"), -1);
- if (entry.get() != nullptr) {
- bool ignore;
- int origin = entry->getOrigin(ignore);
- addToComboBox(combo, ToQString(m_DirectoryStructure->getOriginByID(origin).getName()), origin);
- const auto &alternatives = entry->getAlternatives();
- for (const auto& alt : alternatives) {
- addToComboBox(combo, ToQString(m_DirectoryStructure->getOriginByID(alt.originID()).getName()), alt.originID());
- }
- combo->setCurrentIndex(combo->count() - 1);
- } else {
- combo->setCurrentIndex(0);
- }
- ui->syncTree->setItemWidget(newItem, 1, combo);
- }
- if (newItem != nullptr) {
- subTree->addChild(newItem);
- }
- }
-}
-
-
-void SyncOverwriteDialog::refresh(const QString &path)
-{
- QTreeWidgetItem *rootItem = new QTreeWidgetItem(ui->syncTree, QStringList("<data>"));
- readTree(path, m_DirectoryStructure, rootItem);
- ui->syncTree->addTopLevelItem(rootItem);
- ui->syncTree->expandAll();
-}
-
-
-void SyncOverwriteDialog::applyTo(QTreeWidgetItem *item, const QString &path, const QString &modDirectory)
-{
- for (int i = 0; i < item->childCount(); ++i) {
- QTreeWidgetItem *child = item->child(i);
- QString filePath;
- if (path.length() != 0) {
- filePath = path + "/" + child->text(0);
- } else {
- filePath = child->text(0);
- }
- if (child->childCount() != 0) {
- applyTo(child, filePath, modDirectory);
- } else {
- QComboBox *comboBox = qobject_cast<QComboBox*>(ui->syncTree->itemWidget(child, 1));
- if (comboBox != nullptr) {
- int originID = comboBox->itemData(comboBox->currentIndex(), Qt::UserRole).toInt();
- if (originID != -1) {
- FilesOrigin &origin = m_DirectoryStructure->getOriginByID(originID);
- QString source = m_SourcePath + "/" + filePath;
- QString destination = modDirectory + "/" + ToQString(origin.getName()) + "/" + filePath;
- if (!QFile::remove(destination)) {
- reportError(tr("failed to remove %1").arg(destination));
- } else if (!QFile::rename(source, destination)) {
- reportError(tr("failed to move %1 to %2").arg(source).arg(destination));
- }
- }
- }
- }
- }
-
- QDir dir(m_SourcePath + "/" + path);
- if ((path.length() > 0) && (dir.count() == 2)) {
- dir.rmpath(".");
- }
-}
-
-
-void SyncOverwriteDialog::apply(const QString &modDirectory)
-{
- applyTo(ui->syncTree->topLevelItem(0), "", modDirectory);
-}
+/*
+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 "syncoverwritedialog.h"
+#include "shared/directoryentry.h"
+#include "shared/fileentry.h"
+#include "shared/filesorigin.h"
+#include "ui_syncoverwritedialog.h"
+
+#include <utility.h>
+#include <report.h>
+#include <log.h>
+
+#include <QDir>
+#include <QDirIterator>
+#include <QComboBox>
+#include <QStringList>
+
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+SyncOverwriteDialog::SyncOverwriteDialog(const QString &path, DirectoryEntry *directoryStructure, QWidget *parent)
+ : TutorableDialog("SyncOverwrite", parent),
+ ui(new Ui::SyncOverwriteDialog), m_SourcePath(path), m_DirectoryStructure(directoryStructure)
+{
+ ui->setupUi(this);
+ refresh(path);
+
+ QHeaderView *headerView = ui->syncTree->header();
+#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+ headerView->setSectionResizeMode(1, QHeaderView::Interactive);
+#else
+ headerView->setResizeMode(0, QHeaderView::Stretch);
+ headerView->setResizeMode(1, QHeaderView::Interactive);
+#endif
+}
+
+
+SyncOverwriteDialog::~SyncOverwriteDialog()
+{
+ delete ui;
+}
+
+
+static void addToComboBox(QComboBox *box, const QString &name, const QVariant &userData)
+{
+ if (QString::compare(name, "overwrite", Qt::CaseInsensitive) != 0) {
+ box->addItem(name, userData);
+ }
+}
+
+
+void SyncOverwriteDialog::readTree(const QString &path, DirectoryEntry *directoryStructure, QTreeWidgetItem *subTree)
+{
+ QDir overwrite(path);
+ overwrite.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
+ QDirIterator dirIter(overwrite);
+ while (dirIter.hasNext()) {
+ dirIter.next();
+ QFileInfo fileInfo = dirIter.fileInfo();
+
+ QString file = fileInfo.fileName();
+ if (file == "meta.ini") {
+ continue;
+ }
+
+ QTreeWidgetItem *newItem = new QTreeWidgetItem(subTree, QStringList(file));
+
+ if (fileInfo.isDir()) {
+ DirectoryEntry *subDir = directoryStructure->findSubDirectory(ToWString(file));
+ if (subDir != nullptr) {
+ readTree(fileInfo.absoluteFilePath(), subDir, newItem);
+ } else {
+ log::error("no directory structure for {}?", file);
+ delete newItem;
+ newItem = nullptr;
+ }
+ } else {
+ const FileEntryPtr entry = directoryStructure->findFile(ToWString(file));
+ QComboBox* combo = new QComboBox(ui->syncTree);
+ combo->addItem(tr("<don't sync>"), -1);
+ if (entry.get() != nullptr) {
+ bool ignore;
+ int origin = entry->getOrigin(ignore);
+ addToComboBox(combo, ToQString(m_DirectoryStructure->getOriginByID(origin).getName()), origin);
+ const auto &alternatives = entry->getAlternatives();
+ for (const auto& alt : alternatives) {
+ addToComboBox(combo, ToQString(m_DirectoryStructure->getOriginByID(alt.originID()).getName()), alt.originID());
+ }
+ combo->setCurrentIndex(combo->count() - 1);
+ } else {
+ combo->setCurrentIndex(0);
+ }
+ ui->syncTree->setItemWidget(newItem, 1, combo);
+ }
+ if (newItem != nullptr) {
+ subTree->addChild(newItem);
+ }
+ }
+}
+
+
+void SyncOverwriteDialog::refresh(const QString &path)
+{
+ QTreeWidgetItem *rootItem = new QTreeWidgetItem(ui->syncTree, QStringList("<data>"));
+ readTree(path, m_DirectoryStructure, rootItem);
+ ui->syncTree->addTopLevelItem(rootItem);
+ ui->syncTree->expandAll();
+}
+
+
+void SyncOverwriteDialog::applyTo(QTreeWidgetItem *item, const QString &path, const QString &modDirectory)
+{
+ for (int i = 0; i < item->childCount(); ++i) {
+ QTreeWidgetItem *child = item->child(i);
+ QString filePath;
+ if (path.length() != 0) {
+ filePath = path + "/" + child->text(0);
+ } else {
+ filePath = child->text(0);
+ }
+ if (child->childCount() != 0) {
+ applyTo(child, filePath, modDirectory);
+ } else {
+ QComboBox *comboBox = qobject_cast<QComboBox*>(ui->syncTree->itemWidget(child, 1));
+ if (comboBox != nullptr) {
+ int originID = comboBox->itemData(comboBox->currentIndex(), Qt::UserRole).toInt();
+ if (originID != -1) {
+ FilesOrigin &origin = m_DirectoryStructure->getOriginByID(originID);
+ QString source = m_SourcePath + "/" + filePath;
+ QString destination = modDirectory + "/" + ToQString(origin.getName()) + "/" + filePath;
+ if (!QFile::remove(destination)) {
+ reportError(tr("failed to remove %1").arg(destination));
+ } else if (!QFile::rename(source, destination)) {
+ reportError(tr("failed to move %1 to %2").arg(source).arg(destination));
+ }
+ }
+ }
+ }
+ }
+
+ QDir dir(m_SourcePath + "/" + path);
+ if ((path.length() > 0) && (dir.count() == 2)) {
+ dir.rmpath(".");
+ }
+}
+
+
+void SyncOverwriteDialog::apply(const QString &modDirectory)
+{
+ applyTo(ui->syncTree->topLevelItem(0), "", modDirectory);
+}
diff --git a/src/syncoverwritedialog.h b/src/syncoverwritedialog.h
index cb4c92fb..e05c2072 100644
--- a/src/syncoverwritedialog.h
+++ b/src/syncoverwritedialog.h
@@ -1,56 +1,56 @@
-/*
-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 SYNCOVERWRITEDIALOG_H
-#define SYNCOVERWRITEDIALOG_H
-
-#include "tutorabledialog.h"
-#include "shared/fileregisterfwd.h"
-#include <QTreeWidgetItem>
-
-namespace Ui {
-class SyncOverwriteDialog;
-}
-
-class SyncOverwriteDialog : public MOBase::TutorableDialog
-{
- Q_OBJECT
-
-public:
- explicit SyncOverwriteDialog(
- const QString &path, MOShared::DirectoryEntry *directoryStructure,
- QWidget *parent = 0);
-
- ~SyncOverwriteDialog();
-
- void apply(const QString &modDirectory);
-private:
- void refresh(const QString &path);
- void readTree(const QString &path, MOShared::DirectoryEntry *directoryStructure, QTreeWidgetItem *subTree);
- void applyTo(QTreeWidgetItem *item, const QString &path, const QString &modDirectory);
-
-private:
-
- Ui::SyncOverwriteDialog *ui;
- QString m_SourcePath;
- MOShared::DirectoryEntry *m_DirectoryStructure;
-
-};
-
-#endif // SYNCOVERWRITEDIALOG_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 SYNCOVERWRITEDIALOG_H
+#define SYNCOVERWRITEDIALOG_H
+
+#include "tutorabledialog.h"
+#include "shared/fileregisterfwd.h"
+#include <QTreeWidgetItem>
+
+namespace Ui {
+class SyncOverwriteDialog;
+}
+
+class SyncOverwriteDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
+
+public:
+ explicit SyncOverwriteDialog(
+ const QString &path, MOShared::DirectoryEntry *directoryStructure,
+ QWidget *parent = 0);
+
+ ~SyncOverwriteDialog();
+
+ void apply(const QString &modDirectory);
+private:
+ void refresh(const QString &path);
+ void readTree(const QString &path, MOShared::DirectoryEntry *directoryStructure, QTreeWidgetItem *subTree);
+ void applyTo(QTreeWidgetItem *item, const QString &path, const QString &modDirectory);
+
+private:
+
+ Ui::SyncOverwriteDialog *ui;
+ QString m_SourcePath;
+ MOShared::DirectoryEntry *m_DirectoryStructure;
+
+};
+
+#endif // SYNCOVERWRITEDIALOG_H
diff --git a/src/transfersavesdialog.cpp b/src/transfersavesdialog.cpp
index 5288d408..e7c84e0a 100644
--- a/src/transfersavesdialog.cpp
+++ b/src/transfersavesdialog.cpp
@@ -1,283 +1,283 @@
-/*
-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 "transfersavesdialog.h"
-
-#include "ui_transfersavesdialog.h"
-#include "iplugingame.h"
-#include "isavegame.h"
-#include "savegameinfo.h"
-#include <utility.h>
-#include <log.h>
-
-#include <QtDebug>
-#include <QDateTime>
-#include <QDir>
-#include <QFile>
-#include <QFileInfo>
-#include <QFlags>
-#include <QLabel>
-#include <QListWidget>
-#include <QListWidgetItem>
-#include <QMessageBox>
-#include <QPushButton>
-#include <QStringList>
-
-using namespace MOBase;
-using namespace MOShared;
-
-TransferSavesDialog::TransferSavesDialog(const Profile &profile, IPluginGame const *gamePlugin, QWidget *parent)
- : TutorableDialog("TransferSaves", parent)
- , ui(new Ui::TransferSavesDialog)
- , m_Profile(profile)
- , m_GamePlugin(gamePlugin)
-{
- ui->setupUi(this);
- ui->label_2->setText(tr("Characters for profile %1").arg(m_Profile.name()));
- refreshGlobalSaves();
- refreshLocalSaves();
- refreshGlobalCharacters();
- refreshLocalCharacters();
-}
-
-TransferSavesDialog::~TransferSavesDialog()
-{
- delete ui;
-}
-
-void TransferSavesDialog::refreshGlobalSaves()
-{
- refreshSaves(m_GlobalSaves, m_GamePlugin->savesDirectory().absolutePath());
-}
-
-
-void TransferSavesDialog::refreshLocalSaves()
-{
- refreshSaves(m_LocalSaves, m_Profile.savePath());
-}
-
-
-void TransferSavesDialog::refreshGlobalCharacters()
-{
- refreshCharacters(m_GlobalSaves, ui->globalCharacterList, ui->copyToLocalBtn, ui->moveToLocalBtn);
-}
-
-
-void TransferSavesDialog::refreshLocalCharacters()
-{
- refreshCharacters(m_LocalSaves, ui->localCharacterList, ui->copyToGlobalBtn, ui->moveToGlobalBtn);
-}
-
-
-bool TransferSavesDialog::testOverwrite(OverwriteMode &overwriteMode, const QString &destinationFile)
-{
- QMessageBox::StandardButton res = overwriteMode == OVERWRITE_YES ? QMessageBox::Yes : QMessageBox::No;
- if (overwriteMode == OVERWRITE_ASK) {
- res = QMessageBox::question(this, tr("Overwrite"),
- tr("Overwrite the file \"%1\"").arg(destinationFile),
- QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll);
- if (res == QMessageBox::YesToAll) {
- overwriteMode = OVERWRITE_YES;
- res = QMessageBox::Yes;
- } else if (res == QMessageBox::NoToAll) {
- overwriteMode = OVERWRITE_NO;
- res = QMessageBox::No;
- }
- }
- return res == QMessageBox::Yes;
-}
-
-
-#define MOVE_SAVES "Move all save games of character \"%1\""
-#define COPY_SAVES "Copy all save games of character \"%1\""
-
-#define TO_PROFILE "to the profile?"
-#define TO_GLOBAL "to the global location? Please be aware that this will mess up the running number of save games."
-
-void TransferSavesDialog::on_moveToLocalBtn_clicked()
-{
- QString character = ui->globalCharacterList->currentItem()->text();
- if (transferCharacters(
- character, MOVE_SAVES TO_PROFILE,
- m_GamePlugin->savesDirectory(),
- m_GlobalSaves[character],
- m_Profile.savePath(),
- [this](const QString &source, const QString &destination) -> bool {
- return shellMove(source, destination, this);
- },
- "Failed to move {} to {}")) {
- refreshGlobalSaves();
- refreshGlobalCharacters();
- refreshLocalSaves();
- refreshLocalCharacters();
- }
-}
-
-void TransferSavesDialog::on_copyToLocalBtn_clicked()
-{
- QString character = ui->globalCharacterList->currentItem()->text();
- if (transferCharacters(
- character, COPY_SAVES TO_PROFILE,
- m_GamePlugin->savesDirectory(),
- m_GlobalSaves[character],
- m_Profile.savePath(),
- [this](const QString &source, const QString &destination) -> bool {
- return shellCopy(source, destination, this);
- },
- "Failed to copy {} to {}")) {
- refreshLocalSaves();
- refreshLocalCharacters();
- }
-}
-
-void TransferSavesDialog::on_moveToGlobalBtn_clicked()
-{
- QString character = ui->localCharacterList->currentItem()->text();
- if (transferCharacters(
- character, MOVE_SAVES TO_GLOBAL,
- m_Profile.savePath(),
- m_LocalSaves[character],
- m_GamePlugin->savesDirectory().absolutePath(),
- [this](const QString &source, const QString &destination) -> bool {
- return shellMove(source, destination, this);
- },
- "Failed to move {} to {}")) {
- refreshGlobalSaves();
- refreshGlobalCharacters();
- refreshLocalSaves();
- refreshLocalCharacters();
- }
-}
-
-void TransferSavesDialog::on_copyToGlobalBtn_clicked()
-{
- QString character = ui->localCharacterList->currentItem()->text();
- if (transferCharacters(
- character, COPY_SAVES TO_GLOBAL,
- m_Profile.savePath(),
- m_LocalSaves[character],
- m_GamePlugin->savesDirectory().absolutePath(),
- [this](const QString &source, const QString &destination) -> bool {
- return shellCopy(source, destination, this);
- },
- "Failed to copy {} to {}")) {
- refreshGlobalSaves();
- refreshGlobalCharacters();
- }
-}
-
-void TransferSavesDialog::on_doneButton_clicked()
-{
- close();
-}
-
-void TransferSavesDialog::on_globalCharacterList_currentTextChanged(const QString &currentText)
-{
- ui->globalSavesList->clear();
- //sadly this can get called while we're resetting the list, with an invalid
- //name, so we have to check.
- SaveCollection::const_iterator saveList = m_GlobalSaves.find(currentText);
- if (saveList != m_GlobalSaves.end()) {
- for (SaveListItem const &save : saveList->second) {
- ui->globalSavesList->addItem(QFileInfo(save->getFilepath()).fileName());
- }
- }
-}
-
-void TransferSavesDialog::on_localCharacterList_currentTextChanged(const QString &currentText)
-{
- ui->localSavesList->clear();
- //sadly this can get called while we're resetting the list, with an invalid
- //name, so we have to check.
- SaveCollection::const_iterator saveList = m_LocalSaves.find(currentText);
- if (saveList != m_LocalSaves.end()) {
- for (SaveListItem const &save : saveList->second) {
- ui->localSavesList->addItem(QFileInfo(save->getFilepath()).fileName());
- }
- }
-}
-
-void TransferSavesDialog::refreshSaves(SaveCollection &saveCollection, QString const &savedir)
-{
- saveCollection.clear();
-
- auto saves = m_GamePlugin->listSaves(savedir);
- std::sort(saves.begin(), saves.end(), [](auto const& lhs, auto const& rhs) {
- return lhs->getCreationTime() > rhs->getCreationTime();
- });
-
- for (auto& save: saves) {
- saveCollection[save->getSaveGroupIdentifier()].push_back(save);
- }
-}
-
-void TransferSavesDialog::refreshCharacters(const SaveCollection &saveCollection,
- QListWidget *charList, QPushButton *copy, QPushButton *move)
-{
- charList->clear();
- for (SaveCollection::value_type const &val : saveCollection) {
- charList->addItem(val.first);
- }
- if (charList->count() > 0) {
- charList->setCurrentRow(0);
- copy->setEnabled(true);
- move->setEnabled(true);
- } else {
- copy->setEnabled(false);
- move->setEnabled(false);
- }
-}
-
-bool TransferSavesDialog::transferCharacters(
- QString const &character, char const *message,
- QDir const& sourceDirectory,
- SaveList &saves,
- QDir const& destination,
- const std::function<bool(const QString &, const QString &)> &method,
- char const *errmsg)
-{
- if (QMessageBox::question(this, tr("Confirm"),
- tr(message).arg(character),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
- return false;
- }
-
- OverwriteMode overwriteMode = OVERWRITE_ASK;
-
- for (SaveListItem const &save : saves) {
- for (QString source : save->allFiles()) {
- QFileInfo sourceFile(source);
- QString destinationFile(destination.absoluteFilePath(sourceDirectory.relativeFilePath(source)));
-
- //If the file is already there, let them skip (or not).
- if (QFile::exists(destinationFile)) {
- if (! testOverwrite(overwriteMode, destinationFile)) {
- continue;
- }
- //OK, they want to remove it.
- QFile::remove(destinationFile);
- }
-
- if (!method(sourceFile.absoluteFilePath(), destinationFile)) {
- log::error(errmsg, sourceFile.absoluteFilePath(), destinationFile);
- }
- }
- }
- return true;
-}
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "transfersavesdialog.h"
+
+#include "ui_transfersavesdialog.h"
+#include "iplugingame.h"
+#include "isavegame.h"
+#include "savegameinfo.h"
+#include <utility.h>
+#include <log.h>
+
+#include <QtDebug>
+#include <QDateTime>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QFlags>
+#include <QLabel>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QStringList>
+
+using namespace MOBase;
+using namespace MOShared;
+
+TransferSavesDialog::TransferSavesDialog(const Profile &profile, IPluginGame const *gamePlugin, QWidget *parent)
+ : TutorableDialog("TransferSaves", parent)
+ , ui(new Ui::TransferSavesDialog)
+ , m_Profile(profile)
+ , m_GamePlugin(gamePlugin)
+{
+ ui->setupUi(this);
+ ui->label_2->setText(tr("Characters for profile %1").arg(m_Profile.name()));
+ refreshGlobalSaves();
+ refreshLocalSaves();
+ refreshGlobalCharacters();
+ refreshLocalCharacters();
+}
+
+TransferSavesDialog::~TransferSavesDialog()
+{
+ delete ui;
+}
+
+void TransferSavesDialog::refreshGlobalSaves()
+{
+ refreshSaves(m_GlobalSaves, m_GamePlugin->savesDirectory().absolutePath());
+}
+
+
+void TransferSavesDialog::refreshLocalSaves()
+{
+ refreshSaves(m_LocalSaves, m_Profile.savePath());
+}
+
+
+void TransferSavesDialog::refreshGlobalCharacters()
+{
+ refreshCharacters(m_GlobalSaves, ui->globalCharacterList, ui->copyToLocalBtn, ui->moveToLocalBtn);
+}
+
+
+void TransferSavesDialog::refreshLocalCharacters()
+{
+ refreshCharacters(m_LocalSaves, ui->localCharacterList, ui->copyToGlobalBtn, ui->moveToGlobalBtn);
+}
+
+
+bool TransferSavesDialog::testOverwrite(OverwriteMode &overwriteMode, const QString &destinationFile)
+{
+ QMessageBox::StandardButton res = overwriteMode == OVERWRITE_YES ? QMessageBox::Yes : QMessageBox::No;
+ if (overwriteMode == OVERWRITE_ASK) {
+ res = QMessageBox::question(this, tr("Overwrite"),
+ tr("Overwrite the file \"%1\"").arg(destinationFile),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll);
+ if (res == QMessageBox::YesToAll) {
+ overwriteMode = OVERWRITE_YES;
+ res = QMessageBox::Yes;
+ } else if (res == QMessageBox::NoToAll) {
+ overwriteMode = OVERWRITE_NO;
+ res = QMessageBox::No;
+ }
+ }
+ return res == QMessageBox::Yes;
+}
+
+
+#define MOVE_SAVES "Move all save games of character \"%1\""
+#define COPY_SAVES "Copy all save games of character \"%1\""
+
+#define TO_PROFILE "to the profile?"
+#define TO_GLOBAL "to the global location? Please be aware that this will mess up the running number of save games."
+
+void TransferSavesDialog::on_moveToLocalBtn_clicked()
+{
+ QString character = ui->globalCharacterList->currentItem()->text();
+ if (transferCharacters(
+ character, MOVE_SAVES TO_PROFILE,
+ m_GamePlugin->savesDirectory(),
+ m_GlobalSaves[character],
+ m_Profile.savePath(),
+ [this](const QString &source, const QString &destination) -> bool {
+ return shellMove(source, destination, this);
+ },
+ "Failed to move {} to {}")) {
+ refreshGlobalSaves();
+ refreshGlobalCharacters();
+ refreshLocalSaves();
+ refreshLocalCharacters();
+ }
+}
+
+void TransferSavesDialog::on_copyToLocalBtn_clicked()
+{
+ QString character = ui->globalCharacterList->currentItem()->text();
+ if (transferCharacters(
+ character, COPY_SAVES TO_PROFILE,
+ m_GamePlugin->savesDirectory(),
+ m_GlobalSaves[character],
+ m_Profile.savePath(),
+ [this](const QString &source, const QString &destination) -> bool {
+ return shellCopy(source, destination, this);
+ },
+ "Failed to copy {} to {}")) {
+ refreshLocalSaves();
+ refreshLocalCharacters();
+ }
+}
+
+void TransferSavesDialog::on_moveToGlobalBtn_clicked()
+{
+ QString character = ui->localCharacterList->currentItem()->text();
+ if (transferCharacters(
+ character, MOVE_SAVES TO_GLOBAL,
+ m_Profile.savePath(),
+ m_LocalSaves[character],
+ m_GamePlugin->savesDirectory().absolutePath(),
+ [this](const QString &source, const QString &destination) -> bool {
+ return shellMove(source, destination, this);
+ },
+ "Failed to move {} to {}")) {
+ refreshGlobalSaves();
+ refreshGlobalCharacters();
+ refreshLocalSaves();
+ refreshLocalCharacters();
+ }
+}
+
+void TransferSavesDialog::on_copyToGlobalBtn_clicked()
+{
+ QString character = ui->localCharacterList->currentItem()->text();
+ if (transferCharacters(
+ character, COPY_SAVES TO_GLOBAL,
+ m_Profile.savePath(),
+ m_LocalSaves[character],
+ m_GamePlugin->savesDirectory().absolutePath(),
+ [this](const QString &source, const QString &destination) -> bool {
+ return shellCopy(source, destination, this);
+ },
+ "Failed to copy {} to {}")) {
+ refreshGlobalSaves();
+ refreshGlobalCharacters();
+ }
+}
+
+void TransferSavesDialog::on_doneButton_clicked()
+{
+ close();
+}
+
+void TransferSavesDialog::on_globalCharacterList_currentTextChanged(const QString &currentText)
+{
+ ui->globalSavesList->clear();
+ //sadly this can get called while we're resetting the list, with an invalid
+ //name, so we have to check.
+ SaveCollection::const_iterator saveList = m_GlobalSaves.find(currentText);
+ if (saveList != m_GlobalSaves.end()) {
+ for (SaveListItem const &save : saveList->second) {
+ ui->globalSavesList->addItem(QFileInfo(save->getFilepath()).fileName());
+ }
+ }
+}
+
+void TransferSavesDialog::on_localCharacterList_currentTextChanged(const QString &currentText)
+{
+ ui->localSavesList->clear();
+ //sadly this can get called while we're resetting the list, with an invalid
+ //name, so we have to check.
+ SaveCollection::const_iterator saveList = m_LocalSaves.find(currentText);
+ if (saveList != m_LocalSaves.end()) {
+ for (SaveListItem const &save : saveList->second) {
+ ui->localSavesList->addItem(QFileInfo(save->getFilepath()).fileName());
+ }
+ }
+}
+
+void TransferSavesDialog::refreshSaves(SaveCollection &saveCollection, QString const &savedir)
+{
+ saveCollection.clear();
+
+ auto saves = m_GamePlugin->listSaves(savedir);
+ std::sort(saves.begin(), saves.end(), [](auto const& lhs, auto const& rhs) {
+ return lhs->getCreationTime() > rhs->getCreationTime();
+ });
+
+ for (auto& save: saves) {
+ saveCollection[save->getSaveGroupIdentifier()].push_back(save);
+ }
+}
+
+void TransferSavesDialog::refreshCharacters(const SaveCollection &saveCollection,
+ QListWidget *charList, QPushButton *copy, QPushButton *move)
+{
+ charList->clear();
+ for (SaveCollection::value_type const &val : saveCollection) {
+ charList->addItem(val.first);
+ }
+ if (charList->count() > 0) {
+ charList->setCurrentRow(0);
+ copy->setEnabled(true);
+ move->setEnabled(true);
+ } else {
+ copy->setEnabled(false);
+ move->setEnabled(false);
+ }
+}
+
+bool TransferSavesDialog::transferCharacters(
+ QString const &character, char const *message,
+ QDir const& sourceDirectory,
+ SaveList &saves,
+ QDir const& destination,
+ const std::function<bool(const QString &, const QString &)> &method,
+ char const *errmsg)
+{
+ if (QMessageBox::question(this, tr("Confirm"),
+ tr(message).arg(character),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return false;
+ }
+
+ OverwriteMode overwriteMode = OVERWRITE_ASK;
+
+ for (SaveListItem const &save : saves) {
+ for (QString source : save->allFiles()) {
+ QFileInfo sourceFile(source);
+ QString destinationFile(destination.absoluteFilePath(sourceDirectory.relativeFilePath(source)));
+
+ //If the file is already there, let them skip (or not).
+ if (QFile::exists(destinationFile)) {
+ if (! testOverwrite(overwriteMode, destinationFile)) {
+ continue;
+ }
+ //OK, they want to remove it.
+ QFile::remove(destinationFile);
+ }
+
+ if (!method(sourceFile.absoluteFilePath(), destinationFile)) {
+ log::error(errmsg, sourceFile.absoluteFilePath(), destinationFile);
+ }
+ }
+ }
+ return true;
+}
diff --git a/src/transfersavesdialog.h b/src/transfersavesdialog.h
index fe78aa83..98d99f9e 100644
--- a/src/transfersavesdialog.h
+++ b/src/transfersavesdialog.h
@@ -1,103 +1,103 @@
-/*
-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 TRANSFERSAVESDIALOG_H
-#define TRANSFERSAVESDIALOG_H
-
-#include "tutorabledialog.h"
-#include "profile.h"
-
-class QListWidget;
-#include <QObject>
-class QPushButton;
-#include <QString>
-class QWidget;
-
-#include <memory>
-#include <map>
-#include <vector>
-
-namespace Ui { class TransferSavesDialog; }
-namespace MOBase { class IPluginGame; }
-namespace MOBase { class ISaveGame; }
-
-class TransferSavesDialog : public MOBase::TutorableDialog {
- Q_OBJECT
-
-public:
- explicit TransferSavesDialog(const Profile &profile,
- MOBase::IPluginGame const *gamePlugin,
- QWidget *parent = 0);
- ~TransferSavesDialog();
-
-private slots:
-
- void on_moveToLocalBtn_clicked();
-
- void on_doneButton_clicked();
-
- void on_globalCharacterList_currentTextChanged(const QString &currentText);
-
- void on_localCharacterList_currentTextChanged(const QString &currentText);
-
- void on_copyToLocalBtn_clicked();
-
- void on_moveToGlobalBtn_clicked();
-
- void on_copyToGlobalBtn_clicked();
-
-private:
- enum OverwriteMode { OVERWRITE_ASK, OVERWRITE_YES, OVERWRITE_NO };
-
-private:
- void refreshGlobalCharacters();
- void refreshLocalCharacters();
- void refreshGlobalSaves();
- void refreshLocalSaves();
- bool testOverwrite(OverwriteMode &overwriteMode,
- const QString &destinationFile);
-
-private:
- Ui::TransferSavesDialog *ui;
-
- Profile m_Profile;
-
- MOBase::IPluginGame const *m_GamePlugin;
-
- using SaveListItem = std::shared_ptr<const MOBase::ISaveGame>;
- using SaveList = std::vector<SaveListItem>;
- using SaveCollection = std::map<QString, SaveList>;
-
- SaveCollection m_GlobalSaves;
- SaveCollection m_LocalSaves;
-
- void refreshSaves(SaveCollection &saveCollection, const QString &savedir);
- void refreshCharacters(SaveCollection const &saveCollection,
- QListWidget *charList, QPushButton *copy,
- QPushButton *move);
-
- bool transferCharacters(
- QString const &character, char const *message,
- QDir const& sourceDirectory, SaveList &saves,
- QDir const& dest,
- const std::function<bool(const QString &, const QString &)> &method,
- char const *errmsg);
-};
-
-#endif // TRANSFERSAVESDIALOG_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 TRANSFERSAVESDIALOG_H
+#define TRANSFERSAVESDIALOG_H
+
+#include "tutorabledialog.h"
+#include "profile.h"
+
+class QListWidget;
+#include <QObject>
+class QPushButton;
+#include <QString>
+class QWidget;
+
+#include <memory>
+#include <map>
+#include <vector>
+
+namespace Ui { class TransferSavesDialog; }
+namespace MOBase { class IPluginGame; }
+namespace MOBase { class ISaveGame; }
+
+class TransferSavesDialog : public MOBase::TutorableDialog {
+ Q_OBJECT
+
+public:
+ explicit TransferSavesDialog(const Profile &profile,
+ MOBase::IPluginGame const *gamePlugin,
+ QWidget *parent = 0);
+ ~TransferSavesDialog();
+
+private slots:
+
+ void on_moveToLocalBtn_clicked();
+
+ void on_doneButton_clicked();
+
+ void on_globalCharacterList_currentTextChanged(const QString &currentText);
+
+ void on_localCharacterList_currentTextChanged(const QString &currentText);
+
+ void on_copyToLocalBtn_clicked();
+
+ void on_moveToGlobalBtn_clicked();
+
+ void on_copyToGlobalBtn_clicked();
+
+private:
+ enum OverwriteMode { OVERWRITE_ASK, OVERWRITE_YES, OVERWRITE_NO };
+
+private:
+ void refreshGlobalCharacters();
+ void refreshLocalCharacters();
+ void refreshGlobalSaves();
+ void refreshLocalSaves();
+ bool testOverwrite(OverwriteMode &overwriteMode,
+ const QString &destinationFile);
+
+private:
+ Ui::TransferSavesDialog *ui;
+
+ Profile m_Profile;
+
+ MOBase::IPluginGame const *m_GamePlugin;
+
+ using SaveListItem = std::shared_ptr<const MOBase::ISaveGame>;
+ using SaveList = std::vector<SaveListItem>;
+ using SaveCollection = std::map<QString, SaveList>;
+
+ SaveCollection m_GlobalSaves;
+ SaveCollection m_LocalSaves;
+
+ void refreshSaves(SaveCollection &saveCollection, const QString &savedir);
+ void refreshCharacters(SaveCollection const &saveCollection,
+ QListWidget *charList, QPushButton *copy,
+ QPushButton *move);
+
+ bool transferCharacters(
+ QString const &character, char const *message,
+ QDir const& sourceDirectory, SaveList &saves,
+ QDir const& dest,
+ const std::function<bool(const QString &, const QString &)> &method,
+ char const *errmsg);
+};
+
+#endif // TRANSFERSAVESDIALOG_H
diff --git a/src/viewmarkingscrollbar.cpp b/src/viewmarkingscrollbar.cpp
index 56754237..5cd6d042 100644
--- a/src/viewmarkingscrollbar.cpp
+++ b/src/viewmarkingscrollbar.cpp
@@ -1,55 +1,55 @@
-#include "viewmarkingscrollbar.h"
-#include "modelutils.h"
-#include <QStyle>
-#include <QStyleOptionSlider>
-#include <QPainter>
-
-using namespace MOShared;
-
-ViewMarkingScrollBar::ViewMarkingScrollBar(QTreeView* view, int role)
- : QScrollBar(view)
- , m_view(view)
- , m_role(role)
-{
- // not implemented for horizontal sliders
- Q_ASSERT(this->orientation() == Qt::Vertical);
-}
-
-QColor ViewMarkingScrollBar::color(const QModelIndex& index) const
-{
- auto data = index.data(m_role);
- if (data.canConvert<QColor>()) {
- return data.value<QColor>();
- }
- return QColor();
-}
-
-void ViewMarkingScrollBar::paintEvent(QPaintEvent* event)
-{
- if (m_view->model() == nullptr) {
- return;
- }
- QScrollBar::paintEvent(event);
-
- QStyleOptionSlider styleOption;
- initStyleOption(&styleOption);
-
- QPainter painter(this);
-
- QRect handleRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption, QStyle::SC_ScrollBarSlider, this);
- QRect innerRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption, QStyle::SC_ScrollBarGroove, this);
-
- auto indices = visibleIndex(m_view, 0);
-
- painter.translate(innerRect.topLeft() + QPoint(0, 3));
- qreal scale = static_cast<qreal>(innerRect.height() - 3) / static_cast<qreal>(indices.size());
-
- for (int i = 0; i < indices.size(); ++i) {
- QColor color = this->color(indices[i]);
- if (color.isValid()) {
- painter.setPen(color);
- painter.setBrush(color);
- painter.drawRect(QRect(2, i * scale - 2, handleRect.width() - 5, 3));
- }
- }
-}
+#include "viewmarkingscrollbar.h"
+#include "modelutils.h"
+#include <QStyle>
+#include <QStyleOptionSlider>
+#include <QPainter>
+
+using namespace MOShared;
+
+ViewMarkingScrollBar::ViewMarkingScrollBar(QTreeView* view, int role)
+ : QScrollBar(view)
+ , m_view(view)
+ , m_role(role)
+{
+ // not implemented for horizontal sliders
+ Q_ASSERT(this->orientation() == Qt::Vertical);
+}
+
+QColor ViewMarkingScrollBar::color(const QModelIndex& index) const
+{
+ auto data = index.data(m_role);
+ if (data.canConvert<QColor>()) {
+ return data.value<QColor>();
+ }
+ return QColor();
+}
+
+void ViewMarkingScrollBar::paintEvent(QPaintEvent* event)
+{
+ if (m_view->model() == nullptr) {
+ return;
+ }
+ QScrollBar::paintEvent(event);
+
+ QStyleOptionSlider styleOption;
+ initStyleOption(&styleOption);
+
+ QPainter painter(this);
+
+ QRect handleRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption, QStyle::SC_ScrollBarSlider, this);
+ QRect innerRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption, QStyle::SC_ScrollBarGroove, this);
+
+ auto indices = visibleIndex(m_view, 0);
+
+ painter.translate(innerRect.topLeft() + QPoint(0, 3));
+ qreal scale = static_cast<qreal>(innerRect.height() - 3) / static_cast<qreal>(indices.size());
+
+ for (int i = 0; i < indices.size(); ++i) {
+ QColor color = this->color(indices[i]);
+ if (color.isValid()) {
+ painter.setPen(color);
+ painter.setBrush(color);
+ painter.drawRect(QRect(2, i * scale - 2, handleRect.width() - 5, 3));
+ }
+ }
+}
diff --git a/src/viewmarkingscrollbar.h b/src/viewmarkingscrollbar.h
index 01b5a8c0..3ffd4e97 100644
--- a/src/viewmarkingscrollbar.h
+++ b/src/viewmarkingscrollbar.h
@@ -1,26 +1,26 @@
-#ifndef VIEWMARKINGSCROLLBAR_H
-#define VIEWMARKINGSCROLLBAR_H
-
-#include <QTreeView>
-#include <QScrollBar>
-
-
-class ViewMarkingScrollBar : public QScrollBar
-{
-public:
- ViewMarkingScrollBar(QTreeView* view, int role);
-
-protected:
- void paintEvent(QPaintEvent *event) override;
-
- // retrieve the color of the marker for the given index
- //
- virtual QColor color(const QModelIndex& index) const;
-
-private:
- QTreeView* m_view;
- int m_role;
-};
-
-
-#endif // VIEWMARKINGSCROLLBAR_H
+#ifndef VIEWMARKINGSCROLLBAR_H
+#define VIEWMARKINGSCROLLBAR_H
+
+#include <QTreeView>
+#include <QScrollBar>
+
+
+class ViewMarkingScrollBar : public QScrollBar
+{
+public:
+ ViewMarkingScrollBar(QTreeView* view, int role);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+
+ // retrieve the color of the marker for the given index
+ //
+ virtual QColor color(const QModelIndex& index) const;
+
+private:
+ QTreeView* m_view;
+ int m_role;
+};
+
+
+#endif // VIEWMARKINGSCROLLBAR_H