diff options
| author | Mikaël Capelle <capelle.mikael@gmail.com> | 2022-05-17 11:37:19 +0200 |
|---|---|---|
| committer | Mikaël Capelle <capelle.mikael@gmail.com> | 2023-07-09 17:17:53 +0200 |
| commit | 86bb01ba9eac879d3685c439ac9da0028bc4bc80 (patch) | |
| tree | 90cc575c2d02113af459b7772418285f2e5caa1f | |
| parent | 7d6cb8528d20e36a4cee822263865ee2f7f32481 (diff) | |
Convert everything to CRLF.
110 files changed, 23843 insertions, 23836 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f8697127 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf 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: </td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr>"
- "<tr><td>Unmanaged/DLCs: </td><td align=right>%5</td><td align=right>%6</td></tr>"
- "<tr><td>Mod backups: </td><td align=right>%7</td><td align=right>%8</td></tr>"
- "<tr><td>Separators: </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: </td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr>" + "<tr><td>Unmanaged/DLCs: </td><td align=right>%5</td><td align=right>%6</td></tr>" + "<tr><td>Mod backups: </td><td align=right>%7</td><td align=right>%8</td></tr>" + "<tr><td>Separators: </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 ¤tText)
-{
- 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 ¤tText)
-{
- 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 ¤tText) +{ + 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 ¤tText) +{ + 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 ¤tText);
-
- void on_localCharacterList_currentTextChanged(const QString ¤tText);
-
- 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 ¤tText); + + void on_localCharacterList_currentTextChanged(const QString ¤tText); + + 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 |
