From 86bb01ba9eac879d3685c439ac9da0028bc4bc80 Mon Sep 17 00:00:00 2001 From: Mikaƫl Capelle Date: Tue, 17 May 2022 11:37:19 +0200 Subject: Convert everything to CRLF. --- .gitattributes | 7 + src/aboutdialog.cpp | 248 +-- src/aboutdialog.h | 166 +- src/activatemodsdialog.cpp | 192 +-- src/activatemodsdialog.h | 152 +- src/bbcode.cpp | 584 +++---- src/bbcode.h | 80 +- src/browserdialog.cpp | 594 +++---- src/browserdialog.h | 252 +-- src/browserview.cpp | 152 +- src/browserview.h | 162 +- src/categories.cpp | 838 +++++----- src/categories.h | 434 +++--- src/categoriesdialog.cpp | 500 +++--- src/categoriesdialog.h | 150 +- src/credentialsdialog.cpp | 86 +- src/credentialsdialog.h | 88 +- src/csvbuilder.cpp | 544 +++---- src/csvbuilder.h | 186 +-- src/directoryrefresher.cpp | 1020 ++++++------ src/directoryrefresher.h | 422 ++--- src/downloadlist.cpp | 582 +++---- src/downloadlist.h | 208 +-- src/downloadlistview.cpp | 894 +++++------ src/downloadlistview.h | 256 +-- src/editexecutablesdialog.h | 486 +++--- src/executableslist.cpp | 982 ++++++------ src/executableslist.h | 456 +++--- src/filedialogmemory.cpp | 164 +- src/filedialogmemory.h | 98 +- src/genericicondelegate.cpp | 58 +- src/genericicondelegate.h | 70 +- src/icondelegate.cpp | 174 +-- src/icondelegate.h | 114 +- src/iuserinterface.h | 66 +- src/loghighlighter.cpp | 102 +- src/loghighlighter.h | 90 +- src/loglist.cpp | 794 +++++----- src/loglist.h | 188 +-- src/messagedialog.cpp | 216 +-- src/messagedialog.h | 134 +- src/moapplication.cpp | 1326 ++++++++-------- src/moapplication.h | 222 +-- src/modflagicondelegate.cpp | 186 +-- src/modflagicondelegate.h | 70 +- src/modidlineedit.cpp | 2 +- src/modidlineedit.h | 2 +- src/modinfodialog.h | 520 +++---- src/modlist.h | 814 +++++----- src/modlistsortproxy.cpp | 1410 ++++++++--------- src/modlistsortproxy.h | 344 ++-- src/modlistview.cpp | 2930 +++++++++++++++++------------------ src/modlistview.h | 688 ++++---- src/motddialog.cpp | 102 +- src/motddialog.h | 92 +- src/multiprocess.cpp | 204 +-- src/multiprocess.h | 140 +- src/noeditdelegate.cpp | 20 +- src/noeditdelegate.h | 24 +- src/organizercore.h | 1004 ++++++------ src/organizerproxy.cpp | 750 ++++----- src/organizerproxy.h | 260 ++-- src/overwriteinfodialog.cpp | 550 +++---- src/overwriteinfodialog.h | 256 +-- src/persistentcookiejar.cpp | 148 +- src/persistentcookiejar.h | 60 +- src/plugincontainer.cpp | 2380 ++++++++++++++-------------- src/plugincontainer.h | 996 ++++++------ src/pluginlist.cpp | 3442 ++++++++++++++++++++--------------------- src/pluginlist.h | 838 +++++----- src/pluginlistsortproxy.cpp | 306 ++-- src/pluginlistsortproxy.h | 136 +- src/previewdialog.cpp | 128 +- src/previewdialog.h | 80 +- src/previewgenerator.cpp | 116 +- src/previewgenerator.h | 94 +- src/problemsdialog.cpp | 226 +-- src/problemsdialog.h | 90 +- src/profileinputdialog.cpp | 96 +- src/profileinputdialog.h | 88 +- src/profilesdialog.cpp | 806 +++++----- src/profilesdialog.h | 264 ++-- src/qtgroupingproxy.cpp | 2076 ++++++++++++------------- src/qtgroupingproxy.h | 306 ++-- src/queryoverwritedialog.cpp | 134 +- src/queryoverwritedialog.h | 122 +- src/savetextasdialog.cpp | 100 +- src/savetextasdialog.h | 62 +- src/selectiondialog.cpp | 218 +-- src/selectiondialog.h | 148 +- src/selfupdater.cpp | 722 ++++----- src/selfupdater.h | 306 ++-- src/serverinfo.cpp | 356 ++--- src/serverinfo.h | 140 +- src/shared/appconfig.cpp | 66 +- src/shared/appconfig.h | 80 +- src/shared/directoryentry.cpp | 1956 +++++++++++------------ src/shared/directoryentry.h | 642 ++++---- src/shared/util.cpp | 888 +++++------ src/shared/util.h | 178 +-- src/shared/windows_error.cpp | 98 +- src/shared/windows_error.h | 88 +- src/spawn.cpp | 2164 +++++++++++++------------- src/spawn.h | 242 +-- src/syncoverwritedialog.cpp | 342 ++-- src/syncoverwritedialog.h | 112 +- src/transfersavesdialog.cpp | 566 +++---- src/transfersavesdialog.h | 206 +-- src/viewmarkingscrollbar.cpp | 110 +- src/viewmarkingscrollbar.h | 52 +- 110 files changed, 23843 insertions(+), 23836 deletions(-) create mode 100644 .gitattributes 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 . -*/ - - -#include "aboutdialog.h" -#include "ui_aboutdialog.h" -#include "shared/util.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -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("%1 %2").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 . +*/ + + +#include "aboutdialog.h" +#include "ui_aboutdialog.h" +#include "shared/util.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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("%1 %2").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 . -*/ - - -#define ABOUTDIALOG_H - -#include -class QListWidgetItem; -#include -#include - -#include - -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 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 . +*/ + + +#define ABOUTDIALOG_H + +#include +class QListWidgetItem; +#include +#include + +#include + +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 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 . -*/ - -#include "activatemodsdialog.h" -#include "ui_activatemodsdialog.h" - -#include -#include -#include -#include -#include - -#include - -ActivateModsDialog::ActivateModsDialog(SaveGameInfo::MissingAssets const &missingAssets, QWidget *parent) - : TutorableDialog("ActivateMods", parent), ui(new Ui::ActivateModsDialog) -{ - ui->setupUi(this); - - QTableWidget *modsTable = findChild("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 ActivateModsDialog::getModsToActivate() -{ - std::set result; - QTableWidget *modsTable = findChild("modsTable"); - - for (int row = 0; row < modsTable->rowCount(); ++row) { - QComboBox *comboBox = dynamic_cast(modsTable->cellWidget(row, 1)); - if (comboBox != nullptr) { - result.insert(comboBox->currentText()); - } - } - return result; -} - - -std::set ActivateModsDialog::getESPsToActivate() -{ - std::set result; - QTableWidget *modsTable = findChild("modsTable"); - - for (int row = 0; row < modsTable->rowCount(); ++row) { - QComboBox *comboBox = dynamic_cast(modsTable->cellWidget(row, 1)); - if (comboBox != nullptr) { - QLabel *espName = dynamic_cast(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 . +*/ + +#include "activatemodsdialog.h" +#include "ui_activatemodsdialog.h" + +#include +#include +#include +#include +#include + +#include + +ActivateModsDialog::ActivateModsDialog(SaveGameInfo::MissingAssets const &missingAssets, QWidget *parent) + : TutorableDialog("ActivateMods", parent), ui(new Ui::ActivateModsDialog) +{ + ui->setupUi(this); + + QTableWidget *modsTable = findChild("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 ActivateModsDialog::getModsToActivate() +{ + std::set result; + QTableWidget *modsTable = findChild("modsTable"); + + for (int row = 0; row < modsTable->rowCount(); ++row) { + QComboBox *comboBox = dynamic_cast(modsTable->cellWidget(row, 1)); + if (comboBox != nullptr) { + result.insert(comboBox->currentText()); + } + } + return result; +} + + +std::set ActivateModsDialog::getESPsToActivate() +{ + std::set result; + QTableWidget *modsTable = findChild("modsTable"); + + for (int row = 0; row < modsTable->rowCount(); ++row) { + QComboBox *comboBox = dynamic_cast(modsTable->cellWidget(row, 1)); + if (comboBox != nullptr) { + QLabel *espName = dynamic_cast(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 . -*/ - -#ifndef ACTIVATEMODSDIALOG_H -#define ACTIVATEMODSDIALOG_H - -#include "savegameinfo.h" -#include "tutorabledialog.h" - -#include - -class QString; -class QWidget; - -#include - -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 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 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 . +*/ + +#ifndef ACTIVATEMODSDIALOG_H +#define ACTIVATEMODSDIALOG_H + +#include "savegameinfo.h" +#include "tutorabledialog.h" + +#include + +class QString; +class QWidget; + +#include + +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 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 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 . -*/ - -#include "bbcode.h" -#include -#include -#include - -namespace BBCode { - -namespace log = MOBase::log; - -class BBCodeMap { - - typedef std::map > 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("(\\[\\*\\]|)", 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("%2").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("%2").arg(color, content)); - } - } else { - log::warn("don't know how to deal with tag {}", tagName); - } - } else { - if (tagName == "*") { - temp.remove(QRegularExpression("(\\[/\\*\\])?(
)?$")); - } - 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\\]"), - "\\1"); - m_TagMap["i"] = std::make_pair(QRegularExpression("\\[i\\](.*)\\[/i\\]"), - "\\1"); - m_TagMap["u"] = std::make_pair(QRegularExpression("\\[u\\](.*)\\[/u\\]"), - "\\1"); - m_TagMap["s"] = std::make_pair(QRegularExpression("\\[s\\](.*)\\[/s\\]"), - "\\1"); - m_TagMap["sub"] = std::make_pair(QRegularExpression("\\[sub\\](.*)\\[/sub\\]"), - "\\1"); - m_TagMap["sup"] = std::make_pair(QRegularExpression("\\[sup\\](.*)\\[/sup\\]"), - "\\1"); - m_TagMap["size="] = std::make_pair(QRegularExpression("\\[size=([^\\]]*)\\](.*)\\[/size\\]"), - "\\2"); - m_TagMap["color="] = std::make_pair(QRegularExpression("\\[color=([^\\]]*)\\](.*)\\[/color\\]"), - ""); - m_TagMap["font="] = std::make_pair(QRegularExpression("\\[font=([^\\]]*)\\](.*)\\[/font\\]"), - "\\2"); - m_TagMap["center"] = std::make_pair(QRegularExpression("\\[center\\](.*)\\[/center\\]"), - "
\\1
"); - m_TagMap["right"] = std::make_pair(QRegularExpression("\\[right\\](.*)\\[/right\\]"), - "
\\1
"); - m_TagMap["quote"] = std::make_pair(QRegularExpression("\\[quote\\](.*)\\[/quote\\]"), - "
\\1
"); - m_TagMap["quote="] = std::make_pair(QRegularExpression("\\[quote=([^\\]]*)\\](.*)\\[/quote\\]"), - "
\\2
"); - m_TagMap["spoiler"] = std::make_pair(QRegularExpression("\\[spoiler\\](.*)\\[/spoiler\\]"), - "
Spoiler:
Show
\\1
"); - m_TagMap["code"] = std::make_pair(QRegularExpression("\\[code\\](.*)\\[/code\\]"), - "\\1"); - m_TagMap["heading"]= std::make_pair(QRegularExpression("\\[heading\\](.*)\\[/heading\\]"), - "

\\1

"); - m_TagMap["line"] = std::make_pair(QRegularExpression("\\[line\\]"), - "
"); - - // lists - m_TagMap["list"] = std::make_pair(QRegularExpression("\\[list\\](.*)\\[/list\\]"), - "
    \\1
"); - m_TagMap["list="] = std::make_pair(QRegularExpression("\\[list.*\\](.*)\\[/list\\]"), - "
    \\1
"); - m_TagMap["ul"] = std::make_pair(QRegularExpression("\\[ul\\](.*)\\[/ul\\]"), - "
    \\1
"); - m_TagMap["ol"] = std::make_pair(QRegularExpression("\\[ol\\](.*)\\[/ol\\]"), - "
    \\1
"); - m_TagMap["li"] = std::make_pair(QRegularExpression("\\[li\\](.*)\\[/li\\]"), - "
  • \\1
  • "); - - // tables - m_TagMap["table"] = std::make_pair(QRegularExpression("\\[table\\](.*)\\[/table\\]"), - "\\1
    "); - m_TagMap["tr"] = std::make_pair(QRegularExpression("\\[tr\\](.*)\\[/tr\\]"), - "\\1"); - m_TagMap["th"] = std::make_pair(QRegularExpression("\\[th\\](.*)\\[/th\\]"), - "\\1"); - m_TagMap["td"] = std::make_pair(QRegularExpression("\\[td\\](.*)\\[/td\\]"), - "\\1"); - - // web content - m_TagMap["url"] = std::make_pair(QRegularExpression("\\[url\\](.*)\\[/url\\]"), - "\\1"); - m_TagMap["url="] = std::make_pair(QRegularExpression("\\[url=([^\\]]*)\\](.*)\\[/url\\]"), - "\\2"); - m_TagMap["img"] = std::make_pair(QRegularExpression("\\[img(?:\\s*width=\\d+\\s*,?\\s*height=\\d+)?\\](.*)\\[/img\\]"), - ""); - m_TagMap["img="] = std::make_pair(QRegularExpression("\\[img=([^\\]]*)\\](.*)\\[/img\\]"), - "\"\\1\""); - m_TagMap["email="] = std::make_pair(QRegularExpression("\\[email=\"?([^\\]]*)\"?\\](.*)\\[/email\\]"), - "\\2"); - m_TagMap["youtube"] = std::make_pair(QRegularExpression("\\[youtube\\](.*)\\[/youtube\\]"), - "https://www.youtube.com/watch?v=\\1"); - - - // 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("\\[\\*\\](.*)"), - "
  • \\1
  • "); - - m_ColorMap.insert(std::make_pair("red", "FF0000")); - m_ColorMap.insert(std::make_pair("green", "00FF00")); - m_ColorMap.insert(std::make_pair("blue", "0000FF")); - m_ColorMap.insert(std::make_pair("black", "000000")); - m_ColorMap.insert(std::make_pair("gray", "7F7F7F")); - m_ColorMap.insert(std::make_pair("white", "FFFFFF")); - m_ColorMap.insert(std::make_pair("yellow", "FFFF00")); - m_ColorMap.insert(std::make_pair("cyan", "00FFFF")); - m_ColorMap.insert(std::make_pair("magenta", "FF00FF")); - m_ColorMap.insert(std::make_pair("brown", "A52A2A")); - m_ColorMap.insert(std::make_pair("orange", "FFA500")); - m_ColorMap.insert(std::make_pair("gold", "FFD700")); - m_ColorMap.insert(std::make_pair("deepskyblue", "00BFFF")); - m_ColorMap.insert(std::make_pair("salmon", "FA8072")); - m_ColorMap.insert(std::make_pair("dodgerblue", "1E90FF")); - m_ColorMap.insert(std::make_pair("greenyellow", "ADFF2F")); - m_ColorMap.insert(std::make_pair("peru", "CD853F")); - } - -private: - - QRegularExpression m_TagNameExp; - TagMap m_TagMap; - std::map 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", "
    "); - 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 . +*/ + +#include "bbcode.h" +#include +#include +#include + +namespace BBCode { + +namespace log = MOBase::log; + +class BBCodeMap { + + typedef std::map > 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("(\\[\\*\\]|)", 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("%2").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("%2").arg(color, content)); + } + } else { + log::warn("don't know how to deal with tag {}", tagName); + } + } else { + if (tagName == "*") { + temp.remove(QRegularExpression("(\\[/\\*\\])?(
    )?$")); + } + 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\\]"), + "\\1"); + m_TagMap["i"] = std::make_pair(QRegularExpression("\\[i\\](.*)\\[/i\\]"), + "\\1"); + m_TagMap["u"] = std::make_pair(QRegularExpression("\\[u\\](.*)\\[/u\\]"), + "\\1"); + m_TagMap["s"] = std::make_pair(QRegularExpression("\\[s\\](.*)\\[/s\\]"), + "\\1"); + m_TagMap["sub"] = std::make_pair(QRegularExpression("\\[sub\\](.*)\\[/sub\\]"), + "\\1"); + m_TagMap["sup"] = std::make_pair(QRegularExpression("\\[sup\\](.*)\\[/sup\\]"), + "\\1"); + m_TagMap["size="] = std::make_pair(QRegularExpression("\\[size=([^\\]]*)\\](.*)\\[/size\\]"), + "\\2"); + m_TagMap["color="] = std::make_pair(QRegularExpression("\\[color=([^\\]]*)\\](.*)\\[/color\\]"), + ""); + m_TagMap["font="] = std::make_pair(QRegularExpression("\\[font=([^\\]]*)\\](.*)\\[/font\\]"), + "\\2"); + m_TagMap["center"] = std::make_pair(QRegularExpression("\\[center\\](.*)\\[/center\\]"), + "
    \\1
    "); + m_TagMap["right"] = std::make_pair(QRegularExpression("\\[right\\](.*)\\[/right\\]"), + "
    \\1
    "); + m_TagMap["quote"] = std::make_pair(QRegularExpression("\\[quote\\](.*)\\[/quote\\]"), + "
    \\1
    "); + m_TagMap["quote="] = std::make_pair(QRegularExpression("\\[quote=([^\\]]*)\\](.*)\\[/quote\\]"), + "
    \\2
    "); + m_TagMap["spoiler"] = std::make_pair(QRegularExpression("\\[spoiler\\](.*)\\[/spoiler\\]"), + "
    Spoiler:
    Show
    \\1
    "); + m_TagMap["code"] = std::make_pair(QRegularExpression("\\[code\\](.*)\\[/code\\]"), + "\\1"); + m_TagMap["heading"]= std::make_pair(QRegularExpression("\\[heading\\](.*)\\[/heading\\]"), + "

    \\1

    "); + m_TagMap["line"] = std::make_pair(QRegularExpression("\\[line\\]"), + "
    "); + + // lists + m_TagMap["list"] = std::make_pair(QRegularExpression("\\[list\\](.*)\\[/list\\]"), + "
      \\1
    "); + m_TagMap["list="] = std::make_pair(QRegularExpression("\\[list.*\\](.*)\\[/list\\]"), + "
      \\1
    "); + m_TagMap["ul"] = std::make_pair(QRegularExpression("\\[ul\\](.*)\\[/ul\\]"), + "
      \\1
    "); + m_TagMap["ol"] = std::make_pair(QRegularExpression("\\[ol\\](.*)\\[/ol\\]"), + "
      \\1
    "); + m_TagMap["li"] = std::make_pair(QRegularExpression("\\[li\\](.*)\\[/li\\]"), + "
  • \\1
  • "); + + // tables + m_TagMap["table"] = std::make_pair(QRegularExpression("\\[table\\](.*)\\[/table\\]"), + "\\1
    "); + m_TagMap["tr"] = std::make_pair(QRegularExpression("\\[tr\\](.*)\\[/tr\\]"), + "\\1"); + m_TagMap["th"] = std::make_pair(QRegularExpression("\\[th\\](.*)\\[/th\\]"), + "\\1"); + m_TagMap["td"] = std::make_pair(QRegularExpression("\\[td\\](.*)\\[/td\\]"), + "\\1"); + + // web content + m_TagMap["url"] = std::make_pair(QRegularExpression("\\[url\\](.*)\\[/url\\]"), + "\\1"); + m_TagMap["url="] = std::make_pair(QRegularExpression("\\[url=([^\\]]*)\\](.*)\\[/url\\]"), + "\\2"); + m_TagMap["img"] = std::make_pair(QRegularExpression("\\[img(?:\\s*width=\\d+\\s*,?\\s*height=\\d+)?\\](.*)\\[/img\\]"), + ""); + m_TagMap["img="] = std::make_pair(QRegularExpression("\\[img=([^\\]]*)\\](.*)\\[/img\\]"), + "\"\\1\""); + m_TagMap["email="] = std::make_pair(QRegularExpression("\\[email=\"?([^\\]]*)\"?\\](.*)\\[/email\\]"), + "\\2"); + m_TagMap["youtube"] = std::make_pair(QRegularExpression("\\[youtube\\](.*)\\[/youtube\\]"), + "https://www.youtube.com/watch?v=\\1"); + + + // 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("\\[\\*\\](.*)"), + "
  • \\1
  • "); + + m_ColorMap.insert(std::make_pair("red", "FF0000")); + m_ColorMap.insert(std::make_pair("green", "00FF00")); + m_ColorMap.insert(std::make_pair("blue", "0000FF")); + m_ColorMap.insert(std::make_pair("black", "000000")); + m_ColorMap.insert(std::make_pair("gray", "7F7F7F")); + m_ColorMap.insert(std::make_pair("white", "FFFFFF")); + m_ColorMap.insert(std::make_pair("yellow", "FFFF00")); + m_ColorMap.insert(std::make_pair("cyan", "00FFFF")); + m_ColorMap.insert(std::make_pair("magenta", "FF00FF")); + m_ColorMap.insert(std::make_pair("brown", "A52A2A")); + m_ColorMap.insert(std::make_pair("orange", "FFA500")); + m_ColorMap.insert(std::make_pair("gold", "FFD700")); + m_ColorMap.insert(std::make_pair("deepskyblue", "00BFFF")); + m_ColorMap.insert(std::make_pair("salmon", "FA8072")); + m_ColorMap.insert(std::make_pair("dodgerblue", "1E90FF")); + m_ColorMap.insert(std::make_pair("greenyellow", "ADFF2F")); + m_ColorMap.insert(std::make_pair("peru", "CD853F")); + } + +private: + + QRegularExpression m_TagNameExp; + TagMap m_TagMap; + std::map 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", "
    "); + 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 . -*/ - -#ifndef BBCODE_H -#define BBCODE_H - - -#include - - -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 . +*/ + +#ifndef BBCODE_H +#define BBCODE_H + + +#include + + +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 . -*/ - -#include "browserdialog.h" - -#include "ui_browserdialog.h" -#include "browserview.h" -#include "messagedialog.h" -#include "report.h" -#include "persistentcookiejar.h" -#include "settings.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -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("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(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(std::max(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(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(sender()); - if (page == nullptr) { - log::error("sender not a page"); - return; - } - /*browserview *view = qobject_cast(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(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(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 . +*/ + +#include "browserdialog.h" + +#include "ui_browserdialog.h" +#include "browserview.h" +#include "messagedialog.h" +#include "report.h" +#include "persistentcookiejar.h" +#include "settings.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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("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(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(std::max(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(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(sender()); + if (page == nullptr) { + log::error("sender not a page"); + return; + } + /*browserview *view = qobject_cast(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(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(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 . -*/ - -#ifndef BROWSERDIALOG_H -#define BROWSERDIALOG_H - -#include -#include -#include -#include -#include -#include -#include -#include - - -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 . +*/ + +#ifndef BROWSERDIALOG_H +#define BROWSERDIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include + + +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 . -*/ - -#include "browserview.h" - -#include -#include -#include -#include -#include -#include -#include -#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(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(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(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 . +*/ + +#include "browserview.h" + +#include +#include +#include +#include +#include +#include +#include +#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(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(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(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 . -*/ - -#ifndef NEXUSVIEW_H -#define NEXUSVIEW_H - - -class QEvent; -class QUrl; -class QWidget; -#include -#include - -/** - * @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 . +*/ + +#ifndef NEXUSVIEW_H +#define NEXUSVIEW_H + + +class QEvent; +class QUrl; +class QWidget; +#include +#include + +/** + * @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 . -*/ - -#include "categories.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - - -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 cells = line.split('|'); - if (cells.count() != 4) { - log::error( - "invalid category line {}: {} ({} cells)", - lineNum, line.constData(), cells.count()); - } else { - std::vector nexusIDs; - if (cells[2].length() > 0) { - QList nexusIDStrings = cells[2].split(','); - for (QList::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::iterator iter = m_Categories.begin(); - iter != m_Categories.end(); ++iter) { - iter->m_HasChildren = false; - } - - for (std::vector::const_iterator categoryIter = m_Categories.begin(); - categoryIter != m_Categories.end(); ++categoryIter) { - if (categoryIter->m_ParentID != 0) { - std::map::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::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 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 &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 &nexusIDs, int parentID) -{ - int index = static_cast(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 seen; - return isDescendantOfImpl(id, parentID, seen); -} - -bool CategoryFactory::isDescendantOfImpl( - int id, int parentID, std::set& seen) const -{ - if (!seen.insert(id).second) { - log::error("cycle in category: {}", id); - return false; - } - - std::map::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(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::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::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 . +*/ + +#include "categories.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + + +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 cells = line.split('|'); + if (cells.count() != 4) { + log::error( + "invalid category line {}: {} ({} cells)", + lineNum, line.constData(), cells.count()); + } else { + std::vector nexusIDs; + if (cells[2].length() > 0) { + QList nexusIDStrings = cells[2].split(','); + for (QList::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::iterator iter = m_Categories.begin(); + iter != m_Categories.end(); ++iter) { + iter->m_HasChildren = false; + } + + for (std::vector::const_iterator categoryIter = m_Categories.begin(); + categoryIter != m_Categories.end(); ++categoryIter) { + if (categoryIter->m_ParentID != 0) { + std::map::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::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 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 &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 &nexusIDs, int parentID) +{ + int index = static_cast(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 seen; + return isDescendantOfImpl(id, parentID, seen); +} + +bool CategoryFactory::isDescendantOfImpl( + int id, int parentID, std::set& seen) const +{ + if (!seen.insert(id).second) { + log::error("cycle in category: {}", id); + return false; + } + + std::map::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(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::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::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 . -*/ - -#ifndef CATEGORIES_H -#define CATEGORIES_H - - -#include -#include -#include -#include - - -/** - * @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 &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 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 &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 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 &nexusID, int parentID); - - void setParents(); - - static void cleanup(); - -private: - - static CategoryFactory *s_Instance; - - std::vector m_Categories; - std::map m_IDMap; - std::map m_NexusMap; - -private: - // called by isDescendantOf() - bool isDescendantOfImpl(int id, int parentID, std::set& 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 . +*/ + +#ifndef CATEGORIES_H +#define CATEGORIES_H + + +#include +#include +#include +#include + + +/** + * @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 &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 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 &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 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 &nexusID, int parentID); + + void setParents(); + + static void cleanup(); + +private: + + static CategoryFactory *s_Instance; + + std::vector m_Categories; + std::map m_IDMap; + std::map m_NexusMap; + +private: + // called by isDescendantOf() + bool isDescendantOfImpl(int id, int parentID, std::set& 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 . -*/ - -#include "categoriesdialog.h" -#include "ui_categoriesdialog.h" -#include "categories.h" -#include "utility.h" -#include "settings.h" -#include -#include -#include -#include - - -class NewIDValidator : public QIntValidator { -public: - NewIDValidator(const std::set &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 &m_UsedIDs; -}; - - -class ExistingIDValidator : public QIntValidator { -public: - ExistingIDValidator(const std::set &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 &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(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 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::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 idItem(new QTableWidgetItem()); - idItem->setData(Qt::DisplayRole, category.m_ID); - - QScopedPointer nameItem(new QTableWidgetItem(category.m_Name)); - QScopedPointer nexusIDItem(new QTableWidgetItem(MOBase::VectorJoin(category.m_NexusIDs, ","))); - QScopedPointer 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 . +*/ + +#include "categoriesdialog.h" +#include "ui_categoriesdialog.h" +#include "categories.h" +#include "utility.h" +#include "settings.h" +#include +#include +#include +#include + + +class NewIDValidator : public QIntValidator { +public: + NewIDValidator(const std::set &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 &m_UsedIDs; +}; + + +class ExistingIDValidator : public QIntValidator { +public: + ExistingIDValidator(const std::set &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 &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(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 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::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 idItem(new QTableWidgetItem()); + idItem->setData(Qt::DisplayRole, category.m_ID); + + QScopedPointer nameItem(new QTableWidgetItem(category.m_Name)); + QScopedPointer nexusIDItem(new QTableWidgetItem(MOBase::VectorJoin(category.m_NexusIDs, ","))); + QScopedPointer 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 . -*/ - -#ifndef CATEGORIESDIALOG_H -#define CATEGORIESDIALOG_H - -#include "tutorabledialog.h" -#include - -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 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 . +*/ + +#ifndef CATEGORIESDIALOG_H +#define CATEGORIESDIALOG_H + +#include "tutorabledialog.h" +#include + +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 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 . -*/ - -#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 . +*/ + +#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 . -*/ - -#ifndef CREDENTIALSDIALOG_H -#define CREDENTIALSDIALOG_H - -#include - -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 . +*/ + +#ifndef CREDENTIALSDIALOG_H +#define CREDENTIALSDIALOG_H + +#include + +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 > &fields) -{ - std::vector fieldNames; - std::map 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()) { - throw CSVException(QObject::tr("invalid type for \"%1\" (should be integer)").arg(field)); - } - } break; - case TYPE_STRING: { - if (!value.canConvert()) { - throw CSVException(QObject::tr("invalid type for \"%1\" (should be string)").arg(field)); - } - } break; - case TYPE_FLOAT: { - if (!value.canConvert()) { - 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 &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 &data) -{ - writeData(data, true); -} - - -void CSVBuilder::checkFields(const std::vector &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 > &fields) +{ + std::vector fieldNames; + std::map 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()) { + throw CSVException(QObject::tr("invalid type for \"%1\" (should be integer)").arg(field)); + } + } break; + case TYPE_STRING: { + if (!value.canConvert()) { + throw CSVException(QObject::tr("invalid type for \"%1\" (should be string)").arg(field)); + } + } break; + case TYPE_FLOAT: { + if (!value.canConvert()) { + 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 &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 &data) +{ + writeData(data, true); +} + + +void CSVBuilder::checkFields(const std::vector &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 -#include -#include -#include - - -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 > &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 &data); - -private: - - const char *lineBreak(); - const char separator(); - - void fieldValid(); - void checkFields(const std::vector &fields); - void checkValue(const QString &field, const QVariant &value); - void writeData(const std::map &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 m_QuoteMode; - std::vector m_Fields; - std::map m_FieldTypes; - std::map m_Defaults; - std::map m_RowBuffer; - -}; - -#endif // CSVBUILDER_H +#ifndef CSVBUILDER_H +#define CSVBUILDER_H + + +#include +#include +#include +#include + + +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 > &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 &data); + +private: + + const char *lineBreak(); + const char separator(); + + void fieldValid(); + void checkFields(const std::vector &fields); + void checkValue(const QString &field, const QVariant &value); + void writeData(const std::map &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 m_QuoteMode; + std::vector m_Fields; + std::map m_FieldTypes; + std::map m_Defaults; + std::map 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 . -*/ - -#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 - -#include -#include -#include - -#include - - -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& 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 > &mods - , const std::set &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(); - - QStringList loadOrder; - - GamePlugins* gamePlugins = game->feature(); - if (gamePlugins) { - loadOrder = gamePlugins->getLoadOrder(); - } - - std::vector lo; - for (auto&& s : loadOrder) { - lo.push_back(s.toStdWString()); - } - - std::vector archivesW; - for (auto&& a : archives) { - archivesW.push_back(a.toStdWString()); - } - - std::set 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 archives; - std::set 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(); - - QStringList loadOrder; - GamePlugins* gamePlugins = game->feature(); - if (gamePlugins) { - loadOrder = gamePlugins->getLoadOrder(); - } - - std::vector 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 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& entries, DirectoryRefreshProgress* progress) -{ - std::vector 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 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(); - - 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 . +*/ + +#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 + +#include +#include +#include + +#include + + +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& 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 > &mods + , const std::set &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(); + + QStringList loadOrder; + + GamePlugins* gamePlugins = game->feature(); + if (gamePlugins) { + loadOrder = gamePlugins->getLoadOrder(); + } + + std::vector lo; + for (auto&& s : loadOrder) { + lo.push_back(s.toStdWString()); + } + + std::vector archivesW; + for (auto&& a : archives) { + archivesW.push_back(a.toStdWString()); + } + + std::set 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 archives; + std::set 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(); + + QStringList loadOrder; + GamePlugins* gamePlugins = game->feature(); + if (gamePlugins) { + loadOrder = gamePlugins->getLoadOrder(); + } + + std::vector 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 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& entries, DirectoryRefreshProgress* progress) +{ + std::vector 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 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(); + + 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 . -*/ - -#ifndef DIRECTORYREFRESHER_H -#define DIRECTORYREFRESHER_H - -#include "shared/directoryentry.h" -#include "shared/fileregisterfwd.h" -#include "profile.h" -#include -#include -#include -#include -#include -#include - -/** - * @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 > &mods, const std::set &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& 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 m_Mods; - std::set m_EnabledArchives; - std::unique_ptr 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(m_modDone) / m_modCount; - percent = static_cast(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 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 . +*/ + +#ifndef DIRECTORYREFRESHER_H +#define DIRECTORYREFRESHER_H + +#include "shared/directoryentry.h" +#include "shared/fileregisterfwd.h" +#include "profile.h" +#include +#include +#include +#include +#include +#include + +/** + * @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 > &mods, const std::set &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& 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 m_Mods; + std::set m_EnabledArchives; + std::unique_ptr 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(m_modDone) / m_modCount; + percent = static_cast(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 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 . -*/ - -#include "organizercore.h" -#include "downloadlist.h" -#include "downloadmanager.h" -#include "modlistdropinfo.h" -#include "settings.h" -#include -#include -#include -#include -#include -#include - -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 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
    %4").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 . +*/ + +#include "organizercore.h" +#include "downloadlist.h" +#include "downloadmanager.h" +#include "modlistdropinfo.h" +#include "settings.h" +#include +#include +#include +#include +#include +#include + +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 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
    %4").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 . -*/ - -#ifndef DOWNLOADLIST_H -#define DOWNLOADLIST_H - -#include - -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 . +*/ + +#ifndef DOWNLOADLIST_H +#define DOWNLOADLIST_H + +#include + +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 . -*/ - -#include "downloadlist.h" -#include "downloadlistview.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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(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(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(action); - if (widgetAction != nullptr) { - const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); - if (checkBox != nullptr) { - header()->setSectionHidden(i, !checkBox->isChecked()); - } - } - ++i; - } - - qobject_cast(header())->customResizeSections(); -} - -void DownloadListView::resizeEvent(QResizeEvent *event) -{ - QTreeView::resizeEvent(event); - qobject_cast(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(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(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 . +*/ + +#include "downloadlist.h" +#include "downloadlistview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(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(action); + if (widgetAction != nullptr) { + const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); + if (checkBox != nullptr) { + header()->setSectionHidden(i, !checkBox->isChecked()); + } + } + ++i; + } + + qobject_cast(header())->customResizeSections(); +} + +void DownloadListView::resizeEvent(QResizeEvent *event) +{ + QTreeView::resizeEvent(event); + qobject_cast(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(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(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 . -*/ - -#ifndef DOWNLOADLISTWIDGET_H -#define DOWNLOADLISTWIDGET_H - -#include "downloadmanager.h" -#include "downloadlist.h" -#include -#include -#include -#include -#include -#include -#include - - -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 . +*/ + +#ifndef DOWNLOADLISTWIDGET_H +#define DOWNLOADLISTWIDGET_H + +#include "downloadmanager.h" +#include "downloadlist.h" +#include +#include +#include +#include +#include +#include +#include + + +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 . -*/ - -#ifndef EDITEXECUTABLESDIALOG_H -#define EDITEXECUTABLESDIALOG_H - -#include "tutorabledialog.h" -#include -#include "executableslist.h" -#include "profile.h" -#include "iplugingame.h" -#include -#include -#include - -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 ToggableMap -{ -public: - struct Value - { - bool enabled; - T value; - - Value(bool b, T&& v) - : enabled(b), value(std::forward(v)) - { - } - }; - - /** - * returns the Value associated with the given title, or empty - **/ - std::optional 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 m_map; -}; - - -/** - * @brief Dialog to manage the list of executables - **/ -class EditExecutablesDialog : public MOBase::TutorableDialog -{ - Q_OBJECT; - friend class IgnoreChanges; - -public: - using CustomOverwrites = ToggableMap; - using ForcedLibraries = ToggableMap>; - - 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; - 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 . +*/ + +#ifndef EDITEXECUTABLESDIALOG_H +#define EDITEXECUTABLESDIALOG_H + +#include "tutorabledialog.h" +#include +#include "executableslist.h" +#include "profile.h" +#include "iplugingame.h" +#include +#include +#include + +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 ToggableMap +{ +public: + struct Value + { + bool enabled; + T value; + + Value(bool b, T&& v) + : enabled(b), value(std::forward(v)) + { + } + }; + + /** + * returns the Value associated with the given title, or empty + **/ + std::optional 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 m_map; +}; + + +/** + * @brief Dialog to manage the list of executables + **/ +class EditExecutablesDialog : public MOBase::TutorableDialog +{ + Q_OBJECT; + friend class IgnoreChanges; + +public: + using CustomOverwrites = ToggableMap; + using ForcedLibraries = ToggableMap>; + + 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; + 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 . -*/ - -#include "executableslist.h" - -#include "iplugingame.h" -#include "utility.h" -#include "settings.h" -#include - -#include -#include -#include -#include - - -#include - - -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> v; - - for (const auto& item : *this) { - std::map 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 ExecutablesList::getPluginExecutables( - MOBase::IPluginGame const *game) const -{ - Q_ASSERT(game != nullptr); - - std::vector 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(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 ExecutablesList::makeNonConflictingTitle( - const QString& prefix) -{ - const int max = 100; - - QString title = prefix; - - for (int i=1; ibinaryInfo().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 . +*/ + +#include "executableslist.h" + +#include "iplugingame.h" +#include "utility.h" +#include "settings.h" +#include + +#include +#include +#include +#include + + +#include + + +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> v; + + for (const auto& item : *this) { + std::map 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 ExecutablesList::getPluginExecutables( + MOBase::IPluginGame const *game) const +{ + Q_ASSERT(game != nullptr); + + std::vector 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(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 ExecutablesList::makeNonConflictingTitle( + const QString& prefix) +{ + const int max = 100; + + QString title = prefix; + + for (int i=1; ibinaryInfo().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 . -*/ - -#ifndef EXECUTABLESLIST_H -#define EXECUTABLESLIST_H - -#include "executableinfo.h" - -#include -#include - -#include -#include - -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; - 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 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 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 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 . +*/ + +#ifndef EXECUTABLESLIST_H +#define EXECUTABLESLIST_H + +#include "executableinfo.h" + +#include +#include + +#include +#include + +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; + 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 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 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 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 . -*/ - -#include "filedialogmemory.h" -#include "settings.h" -#include - -static std::map 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 . +*/ + +#include "filedialogmemory.h" +#include "settings.h" +#include + +static std::map 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 . -*/ - -#ifndef FILEDIALOGMEMORY_H -#define FILEDIALOGMEMORY_H - - -#include -#include -#include - -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 . +*/ + +#ifndef FILEDIALOGMEMORY_H +#define FILEDIALOGMEMORY_H + + +#include +#include +#include + +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 -#include - - -GenericIconDelegate::GenericIconDelegate(QTreeView* parent, int role, int logicalIndex, int compactSize) - : IconDelegate(parent, logicalIndex, compactSize) - , m_Role(role) -{ -} - -QList GenericIconDelegate::getIcons(const QModelIndex &index) const -{ - QList 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 +#include + + +GenericIconDelegate::GenericIconDelegate(QTreeView* parent, int role, int logicalIndex, int compactSize) + : IconDelegate(parent, logicalIndex, compactSize) + , m_Role(role) +{ +} + +QList GenericIconDelegate::getIcons(const QModelIndex &index) const +{ + QList 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 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 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 . -*/ - -#include "icondelegate.h" -#include -#include -#include -#include -#include -#include - -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& 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(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 . +*/ + +#include "icondelegate.h" +#include +#include +#include +#include +#include +#include + +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& 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(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 . -*/ - -#ifndef ICONDELEGATE_H -#define ICONDELEGATE_H - -#include -#include -#include - - -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& icons); - - virtual QList 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 . +*/ + +#ifndef ICONDELEGATE_H +#define ICONDELEGATE_H + +#include +#include +#include + + +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& icons); + + virtual QList 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 -#include -#include -#include -#include - - -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 +#include +#include +#include +#include + + +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 . -*/ - -#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 . +*/ + +#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 . -*/ - -#ifndef LOGHIGHLIGHTER_H -#define LOGHIGHLIGHTER_H - -#include - -/** - * @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 . +*/ + +#ifndef LOGHIGHLIGHTER_H +#define LOGHIGHLIGHTER_H + +#include + +/** + * @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 . -*/ - -#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 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& 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(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(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(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(e.time.time_since_epoch()); - const auto s = duration_cast(ms); - - const std::time_t tt = s.count(); - const int frac = static_cast(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(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 . +*/ + +#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 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& 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(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(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(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(e.time.time_since_epoch()); + const auto s = duration_cast(ms); + + const std::time_t tt = s.count(); + const int frac = static_cast(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(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 . -*/ - -#ifndef LOGBUFFER_H -#define LOGBUFFER_H - - -#include "shared/appconfig.h" -#include "copyeventfilter.h" -#include -#include -#include - -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& 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 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 . +*/ + +#ifndef LOGBUFFER_H +#define LOGBUFFER_H + + +#include "shared/appconfig.h" +#include "copyeventfilter.h" +#include +#include +#include + +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& 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 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 . -*/ - -#include "messagedialog.h" -#include "ui_messagedialog.h" -#include -#include -#include -#include - -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 += "" + metrics.elidedText(word, Qt::ElideMiddle, ui->message->maximumWidth()) + ""; - } 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(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 . +*/ + +#include "messagedialog.h" +#include "ui_messagedialog.h" +#include +#include +#include +#include + +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 += "" + metrics.elidedText(word, Qt::ElideMiddle, ui->message->maximumWidth()) + ""; + } 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(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 . -*/ - -#ifndef MESSAGEDIALOG_H -#define MESSAGEDIALOG_H - -#include - -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 . +*/ + +#ifndef MESSAGEDIALOG_H +#define MESSAGEDIALOG_H + +#include + +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 . -*/ - -#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 -#include -#include -#include -#include "shared/appconfig.h" -#include -#include -#include -#include -#include -#include -#include -#include - -// 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(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(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 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 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 . +*/ + +#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 +#include +#include +#include +#include "shared/appconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// 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(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(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 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 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 . -*/ - -#ifndef MOAPPLICATION_H -#define MOAPPLICATION_H - -#include -#include -#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 m_modules; - - std::unique_ptr m_instance; - std::unique_ptr m_settings; - std::unique_ptr m_nexus; - std::unique_ptr m_plugins; - std::unique_ptr m_core; - - void externalMessage(const QString& message); - std::unique_ptr getCurrentInstance(bool forceSelect); - std::optional 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 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 . +*/ + +#ifndef MOAPPLICATION_H +#define MOAPPLICATION_H + +#include +#include +#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 m_modules; + + std::unique_ptr m_instance; + std::unique_ptr m_settings; + std::unique_ptr m_nexus; + std::unique_ptr m_plugins; + std::unique_ptr m_core; + + void externalMessage(const QString& message); + std::unique_ptr getCurrentInstance(bool forceSelect); + std::optional 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 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 -#include - -using namespace MOBase; - -ModFlagIconDelegate::ModFlagIconDelegate(ModListView* view, int column, int compactSize) - : IconDelegate(view, column, compactSize), m_view(view) -{ -} - -QList ModFlagIconDelegate::getIconsForFlags( - std::vector flags, bool compact) -{ - QList 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 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 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(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 +#include + +using namespace MOBase; + +ModFlagIconDelegate::ModFlagIconDelegate(ModListView* view, int column, int compactSize) + : IconDelegate(view, column, compactSize), m_view(view) +{ +} + +QList ModFlagIconDelegate::getIconsForFlags( + std::vector flags, bool compact) +{ + QList 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 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 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(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 - -#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 getIconsForFlags(std::vector flags, bool compact); - static QString getFlagIcon(ModInfo::EFlag flag); - - QList 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 + +#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 getIconsForFlags(std::vector flags, bool compact); + static QString getFlagIcon(ModInfo::EFlag flag); + + QList 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 . -*/ - -#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 - friend std::unique_ptr 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 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 tab); - - // returns whether this tab is part of the tab widget - // - bool isVisible() const; - }; - - std::unique_ptr ui; - OrganizerCore& m_core; - PluginContainer& m_plugin; - ModListView* m_modListView; - ModInfo::Ptr m_mod; - std::vector 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& 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& 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 . +*/ + +#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 + friend std::unique_ptr 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 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 tab); + + // returns whether this tab is part of the tab widget + // + bool isVisible() const; + }; + + std::unique_ptr ui; + OrganizerCore& m_core; + PluginContainer& m_plugin; + ModListView* m_modListView; + ModInfo::Ptr m_mod; + std::vector 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& 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& 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 . -*/ - -#ifndef MODLIST_H -#define MODLIST_H - -#include "moddatacontent.h" -#include "categories.h" -#include "nexusinterface.h" -#include "modinfo.h" -#include "profile.h" - -#include - -#include -#include -#include -#include -#include -#ifndef Q_MOC_RUN -#include -#endif -#include -#include -#include - - -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; - using SignalModRemoved = boost::signals2::signal; - using SignalModStateChanged = boost::signals2::signal&)>; - using SignalModMoved = boost::signals2::signal; - -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 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 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& func); - boost::signals2::connection onModRemoved(const std::function& func); - boost::signals2::connection onModStateChanged(const std::function&)>& func); - boost::signals2::connection onModMoved(const std::function &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 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 state; - }; - -private: - - OrganizerCore *m_Organizer; - Profile *m_Profile; - - NexusInterface *m_NexusInterface; - std::set 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 . +*/ + +#ifndef MODLIST_H +#define MODLIST_H + +#include "moddatacontent.h" +#include "categories.h" +#include "nexusinterface.h" +#include "modinfo.h" +#include "profile.h" + +#include + +#include +#include +#include +#include +#include +#ifndef Q_MOC_RUN +#include +#endif +#include +#include +#include + + +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; + using SignalModRemoved = boost::signals2::signal; + using SignalModStateChanged = boost::signals2::signal&)>; + using SignalModMoved = boost::signals2::signal; + +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 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 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& func); + boost::signals2::connection onModRemoved(const std::function& func); + boost::signals2::connection onModStateChanged(const std::function&)>& func); + boost::signals2::connection onModMoved(const std::function &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 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 state; + }; + +private: + + OrganizerCore *m_Organizer; + Profile *m_Profile; + + NexusInterface *m_NexusInterface; + std::set 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 . -*/ - -#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 -#include -#include -#include -#include -#include -#include -#include - -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) -{ - // 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 &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& 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(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 leftFlags = leftMod->getFlags(); - std::vector 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 leftFlags = leftMod->getConflictFlags(); - std::vector 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(content.id()); - }); - m_Organizer->modDataContents().forEachContentIn(rContents, [&rValue](auto const& content) { - rValue += 2U << static_cast(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 &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(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(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(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("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(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 . +*/ + +#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 +#include +#include +#include +#include +#include +#include +#include + +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) +{ + // 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 &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& 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(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 leftFlags = leftMod->getFlags(); + std::vector 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 leftFlags = leftMod->getConflictFlags(); + std::vector 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(content.id()); + }); + m_Organizer->modDataContents().forEachContentIn(rContents, [&rValue](auto const& content) { + rValue += 2U << static_cast(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 &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(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(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(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("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(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 . -*/ - -#ifndef MODLISTSORTPROXY_H -#define MODLISTSORTPROXY_H - -#include -#include -#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); - 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 &flags) const; - unsigned long conflictFlagsId(const std::vector& flags) const; - bool hasConflictFlag(const std::vector &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 m_Criteria; - QString m_Filter; - std::bitset m_EnabledColumns; - - bool m_FilterActive; - FilterMode m_FilterMode; - SeparatorsMode m_FilterSeparators; - - std::vector 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 . +*/ + +#ifndef MODLISTSORTPROXY_H +#define MODLISTSORTPROXY_H + +#include +#include +#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); + 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 &flags) const; + unsigned long conflictFlagsId(const std::vector& flags) const; + bool hasConflictFlag(const std::vector &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 m_Criteria; + QString m_Filter; + std::bitset m_EnabledColumns; + + bool m_FilterActive; + FilterMode m_FilterMode; + SeparatorsMode m_FilterSeparators; + + std::vector 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 -#include -#include - -#include - -#include "filesystemutilities.h" -#include - -#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(); - } - 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 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 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& 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 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 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("" - "" - "" - "" - "" - "" - "
    TypeAllVisible
    Enabled mods: %1 / %2%3 / %4
    Unmanaged/DLCs: %5%6
    Mod backups: %7%8
    Separators: %9%10
    ") - .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 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(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(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::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 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& 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 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 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 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 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 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 ModListView::contentsIcons(const QModelIndex& index, bool* forceCompact) const -{ - auto contents = this->contents(index, forceCompact); - QList 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(""); - m_core->modDataContents().forEachContentIn(contents, [&result](auto const& content) { - result.append(QString("" - "") - .arg(content.icon()).arg(content.name())); - }); - result.append("
    %2
    "); - return result; -} - -void ModListView::onFiltersCriteria(const std::vector& 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(""); - } - - 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(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(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 +#include +#include + +#include + +#include "filesystemutilities.h" +#include + +#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(); + } + 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 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 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& 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 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 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("" + "" + "" + "" + "" + "" + "
    TypeAllVisible
    Enabled mods: %1 / %2%3 / %4
    Unmanaged/DLCs: %5%6
    Mod backups: %7%8
    Separators: %9%10
    ") + .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 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(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(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::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 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& 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 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 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 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 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 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 ModListView::contentsIcons(const QModelIndex& index, bool* forceCompact) const +{ + auto contents = this->contents(index, forceCompact); + QList 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(""); + m_core->modDataContents().forEachContentIn(contents, [&result](auto const& content) { + result.append(QString("" + "") + .arg(content.icon()).arg(content.name())); + }); + result.append("
    %2
    "); + return result; +} + +void ModListView::onFiltersCriteria(const std::vector& 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(""); + } + + 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(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(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 -#include -#include - -#include -#include -#include -#include - -#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 nextMod(unsigned int index) const; - std::optional 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& 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& 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& 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 overwrite; - std::set overwritten; - std::set archiveOverwrite; - std::set archiveOverwritten; - std::set archiveLooseOverwrite; - std::set archiveLooseOverwritten; - - // selected plugins - std::set 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 modFlags( - const QModelIndex& index, bool* forceCompact = nullptr) const; - - // retrieve the conflicts flags for the given index - // - std::vector conflictFlags( - const QModelIndex& index, bool* forceCompact = nullptr) const; - - // retrieve the content icons and tooltip for the given index - // - std::set contents(const QModelIndex& index, bool* includeChildren) const; - QList 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 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 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> 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 +#include +#include + +#include +#include +#include +#include + +#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 nextMod(unsigned int index) const; + std::optional 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& 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& 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& 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 overwrite; + std::set overwritten; + std::set archiveOverwrite; + std::set archiveOverwritten; + std::set archiveLooseOverwrite; + std::set archiveLooseOverwritten; + + // selected plugins + std::set 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 modFlags( + const QModelIndex& index, bool* forceCompact = nullptr) const; + + // retrieve the conflicts flags for the given index + // + std::vector conflictFlags( + const QModelIndex& index, bool* forceCompact = nullptr) const; + + // retrieve the content icons and tooltip for the given index + // + std::set contents(const QModelIndex& index, bool* includeChildren) const; + QList 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 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 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> 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 . -*/ - -#include "motddialog.h" -#include "bbcode.h" -#include "utility.h" -#include "ui_motddialog.h" -#include "organizercore.h" -#include -#include - -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 . +*/ + +#include "motddialog.h" +#include "bbcode.h" +#include "utility.h" +#include "ui_motddialog.h" +#include "organizercore.h" +#include +#include + +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 . -*/ - -#ifndef MOTDDIALOG_H -#define MOTDDIALOG_H - -#include -#include - -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 . +*/ + +#ifndef MOTDDIALOG_H +#define MOTDDIALOG_H + +#include +#include + +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 -#include -#include - -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 +#include +#include + +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 -#include -#include - - -/** - * 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 +#include +#include + + +/** + * 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 - -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 + +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 -#include -#include -#include -#include -#include "executableinfo.h" -#include "moddatacontent.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class ModListSortProxy; -class PluginListSortProxy; -class Profile; -class IUserInterface; -class PluginContainer; -class DirectoryRefresher; - -namespace MOBase -{ - template 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 - bool operator()(InputIterator first, InputIterator last) const - { - while (first != last) { - if (!(*first)) { - return false; - } - ++first; - } - return true; - } - }; - -private: - - using SignalAboutToRunApplication = boost::signals2::signal; - using SignalFinishedRunApplication = boost::signals2::signal; - using SignalUserInterfaceInitialized = boost::signals2::signal; - using SignalProfileCreated = boost::signals2::signal; - using SignalProfileRenamed = boost::signals2::signal; - using SignalProfileRemoved = boost::signals2::signal; - using SignalProfileChanged = boost::signals2::signal; - using SignalPluginSettingChanged = boost::signals2::signal; - using SignalPluginEnabled = boost::signals2::signal; - -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 - void forEachContentIn(std::set 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 - void forEachContentInOrOut(std::set 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 - 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 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 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 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& 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 modInfos); - - void doAfterLogin(const std::function &function) { m_PostLoginTasks.append(function); } - void loggedInAction(QWidget* parent, std::function 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 &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 &filter) const; - QStringList getFileOrigins(const QString &fileName) const; - QList findFileInfos(const QString &path, const std::function &filter) const; - DownloadManager *downloadManager(); - PluginList *pluginList(); - ModList *modList(); - void refresh(bool saveChanges = true); - - boost::signals2::connection onAboutToRun(const std::function& func); - boost::signals2::connection onFinishedRun(const std::function& func); - boost::signals2::connection onUserInterfaceInitialized(std::function const& func); - boost::signals2::connection onProfileCreated(std::function const& func); - boost::signals2::connection onProfileRenamed(std::function const& func); - boost::signals2::connection onProfileRemoved(std::function const& func); - boost::signals2::connection onProfileChanged(std::function const& func); - boost::signals2::connection onPluginSettingChanged(std::function const& func); - boost::signals2::connection onPluginEnabled(std::function const& func); - boost::signals2::connection onPluginDisabled(std::function const& func); - -public: // IPluginDiagnose interface - - virtual std::vector 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 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 doInstall(const QString& archivePath, - MOBase::GuessedValue modName, ModInfo::Ptr currentMod, int priority, bool reinstallation); - - void saveCurrentProfile(); - void storeSettings(); - - void updateModActiveState(int index, bool active); - void updateModsActiveState(const QList &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 const& indices) const; - - bool createDirectory(const QString &path); - - QString oldMO1HookDll() const; - - /** - * @brief return a descriptor of the mappings real file->virtual file - */ - std::vector fileMapping(const QString &profile, - const QString &customOverwrite); - - std::vector - 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 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> m_PostLoginTasks; - QList> m_PostRefreshTasks; - - ExecutablesList m_ExecutablesList; - QStringList m_PendingDownloads; - QStringList m_DefaultArchives; - QStringList m_ActiveArchives; - - std::unique_ptr m_DirectoryRefresher; - MOShared::DirectoryEntry *m_DirectoryStructure; - MOBase::MemoizedLocked> 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 +#include +#include +#include +#include +#include "executableinfo.h" +#include "moddatacontent.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ModListSortProxy; +class PluginListSortProxy; +class Profile; +class IUserInterface; +class PluginContainer; +class DirectoryRefresher; + +namespace MOBase +{ + template 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 + bool operator()(InputIterator first, InputIterator last) const + { + while (first != last) { + if (!(*first)) { + return false; + } + ++first; + } + return true; + } + }; + +private: + + using SignalAboutToRunApplication = boost::signals2::signal; + using SignalFinishedRunApplication = boost::signals2::signal; + using SignalUserInterfaceInitialized = boost::signals2::signal; + using SignalProfileCreated = boost::signals2::signal; + using SignalProfileRenamed = boost::signals2::signal; + using SignalProfileRemoved = boost::signals2::signal; + using SignalProfileChanged = boost::signals2::signal; + using SignalPluginSettingChanged = boost::signals2::signal; + using SignalPluginEnabled = boost::signals2::signal; + +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 + void forEachContentIn(std::set 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 + void forEachContentInOrOut(std::set 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 + 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 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 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 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& 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 modInfos); + + void doAfterLogin(const std::function &function) { m_PostLoginTasks.append(function); } + void loggedInAction(QWidget* parent, std::function 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 &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 &filter) const; + QStringList getFileOrigins(const QString &fileName) const; + QList findFileInfos(const QString &path, const std::function &filter) const; + DownloadManager *downloadManager(); + PluginList *pluginList(); + ModList *modList(); + void refresh(bool saveChanges = true); + + boost::signals2::connection onAboutToRun(const std::function& func); + boost::signals2::connection onFinishedRun(const std::function& func); + boost::signals2::connection onUserInterfaceInitialized(std::function const& func); + boost::signals2::connection onProfileCreated(std::function const& func); + boost::signals2::connection onProfileRenamed(std::function const& func); + boost::signals2::connection onProfileRemoved(std::function const& func); + boost::signals2::connection onProfileChanged(std::function const& func); + boost::signals2::connection onPluginSettingChanged(std::function const& func); + boost::signals2::connection onPluginEnabled(std::function const& func); + boost::signals2::connection onPluginDisabled(std::function const& func); + +public: // IPluginDiagnose interface + + virtual std::vector 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 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 doInstall(const QString& archivePath, + MOBase::GuessedValue modName, ModInfo::Ptr currentMod, int priority, bool reinstallation); + + void saveCurrentProfile(); + void storeSettings(); + + void updateModActiveState(int index, bool active); + void updateModsActiveState(const QList &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 const& indices) const; + + bool createDirectory(const QString &path); + + QString oldMO1HookDll() const; + + /** + * @brief return a descriptor of the mappings real file->virtual file + */ + std::vector fileMapping(const QString &profile, + const QString &customOverwrite); + + std::vector + 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 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> m_PostLoginTasks; + QList> m_PostRefreshTasks; + + ExecutablesList m_ExecutablesList; + QStringList m_PendingDownloads; + QStringList m_DefaultArchives; + QStringList m_ActiveArchives; + + std::unique_ptr m_DirectoryRefresher; + MOShared::DirectoryEntry *m_DirectoryStructure; + MOBase::MemoizedLocked> 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 -#include - -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(this, organizer->downloadManager())) - , m_ModListProxy(std::make_unique(this, organizer->modList())) - , m_PluginListProxy(std::make_unique(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 &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 &filter) const -{ - return m_Proxied->findFiles(path, filter); -} - -QStringList OrganizerProxy::findFiles(const QString& path, const QStringList& globFilters) const -{ - QList> 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 OrganizerProxy::findFileInfos(const QString &path, const std::function &filter) const -{ - return m_Proxied->findFileInfos(path, filter); -} - -std::shared_ptr 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& func) -{ - return m_Proxied->onAboutToRun(MOShared::callIfPluginActive(this, func, true)).connected(); -} - -bool OrganizerProxy::onFinishedRun(const std::function& func) -{ - return m_Proxied->onFinishedRun(MOShared::callIfPluginActive(this, func)).connected(); -} - -bool OrganizerProxy::onProfileCreated(std::function const& func) -{ - return m_ProfileCreated.connect(func).connected(); -} - -bool OrganizerProxy::onProfileRenamed(std::function const& func) -{ - return m_ProfileRenamed.connect(func).connected(); -} - -bool OrganizerProxy::onProfileRemoved(std::function const& func) -{ - return m_ProfileRemoved.connect(func).connected(); -} - -bool OrganizerProxy::onProfileChanged(std::function const& func) -{ - return m_ProfileChanged.connect(func).connected(); -} - -bool OrganizerProxy::onUserInterfaceInitialized(std::function 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 const& func) -{ - return m_PluginSettingChanged.connect(func).connected(); -} - -bool OrganizerProxy::onPluginEnabled(std::function const& func) -{ - return m_PluginEnabled.connect(func).connected(); -} - -bool OrganizerProxy::onPluginEnabled(const QString& pluginName, std::function const& func) -{ - return onPluginEnabled([=](const IPlugin* plugin) { - if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) { - func(); - } - }); -} - -bool OrganizerProxy::onPluginDisabled(std::function const& func) -{ - return m_PluginDisabled.connect(func).connected(); -} - -bool OrganizerProxy::onPluginDisabled(const QString& pluginName, std::function 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 +#include + +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(this, organizer->downloadManager())) + , m_ModListProxy(std::make_unique(this, organizer->modList())) + , m_PluginListProxy(std::make_unique(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 &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 &filter) const +{ + return m_Proxied->findFiles(path, filter); +} + +QStringList OrganizerProxy::findFiles(const QString& path, const QStringList& globFilters) const +{ + QList> 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 OrganizerProxy::findFileInfos(const QString &path, const std::function &filter) const +{ + return m_Proxied->findFileInfos(path, filter); +} + +std::shared_ptr 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& func) +{ + return m_Proxied->onAboutToRun(MOShared::callIfPluginActive(this, func, true)).connected(); +} + +bool OrganizerProxy::onFinishedRun(const std::function& func) +{ + return m_Proxied->onFinishedRun(MOShared::callIfPluginActive(this, func)).connected(); +} + +bool OrganizerProxy::onProfileCreated(std::function const& func) +{ + return m_ProfileCreated.connect(func).connected(); +} + +bool OrganizerProxy::onProfileRenamed(std::function const& func) +{ + return m_ProfileRenamed.connect(func).connected(); +} + +bool OrganizerProxy::onProfileRemoved(std::function const& func) +{ + return m_ProfileRemoved.connect(func).connected(); +} + +bool OrganizerProxy::onProfileChanged(std::function const& func) +{ + return m_ProfileChanged.connect(func).connected(); +} + +bool OrganizerProxy::onUserInterfaceInitialized(std::function 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 const& func) +{ + return m_PluginSettingChanged.connect(func).connected(); +} + +bool OrganizerProxy::onPluginEnabled(std::function const& func) +{ + return m_PluginEnabled.connect(func).connected(); +} + +bool OrganizerProxy::onPluginEnabled(const QString& pluginName, std::function const& func) +{ + return onPluginEnabled([=](const IPlugin* plugin) { + if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) { + func(); + } + }); +} + +bool OrganizerProxy::onPluginDisabled(std::function const& func) +{ + return m_PluginDisabled.connect(func).connected(); +} + +bool OrganizerProxy::onPluginDisabled(const QString& pluginName, std::function 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 - -#include -#include - -#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 &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 &filter) const override; - virtual QStringList findFiles(const QString &path, const QStringList &globFilters) const override; - virtual QStringList getFileOrigins(const QString &fileName) const override; - virtual QList findFileInfos(const QString &path, const std::function &filter) const override; - virtual std::shared_ptr 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 &func) override; - virtual bool onFinishedRun(const std::function &func) override; - virtual bool onUserInterfaceInitialized(std::function const& func) override; - virtual bool onProfileCreated(std::function const& func) override; - virtual bool onProfileRenamed(std::function const& func) override; - virtual bool onProfileRemoved(std::function const& func) override; - virtual bool onProfileChanged(std::function 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 const& func) override; - virtual bool onPluginEnabled(std::function const& func) override; - virtual bool onPluginEnabled(const QString& pluginName, std::function const& func) override; - virtual bool onPluginDisabled(std::function const& func) override; - virtual bool onPluginDisabled(const QString& pluginName, std::function 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 m_Connections; - - std::unique_ptr m_DownloadManagerProxy; - std::unique_ptr m_ModListProxy; - std::unique_ptr m_PluginListProxy; - -}; - -#endif // ORGANIZERPROXY_H +#ifndef ORGANIZERPROXY_H +#define ORGANIZERPROXY_H + +#include + +#include +#include + +#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 &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 &filter) const override; + virtual QStringList findFiles(const QString &path, const QStringList &globFilters) const override; + virtual QStringList getFileOrigins(const QString &fileName) const override; + virtual QList findFileInfos(const QString &path, const std::function &filter) const override; + virtual std::shared_ptr 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 &func) override; + virtual bool onFinishedRun(const std::function &func) override; + virtual bool onUserInterfaceInitialized(std::function const& func) override; + virtual bool onProfileCreated(std::function const& func) override; + virtual bool onProfileRenamed(std::function const& func) override; + virtual bool onProfileRemoved(std::function const& func) override; + virtual bool onProfileChanged(std::function 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 const& func) override; + virtual bool onPluginEnabled(std::function const& func) override; + virtual bool onPluginEnabled(const QString& pluginName, std::function const& func) override; + virtual bool onPluginDisabled(std::function const& func) override; + virtual bool onPluginDisabled(const QString& pluginName, std::function 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 m_Connections; + + std::unique_ptr m_DownloadManagerProxy; + std::unique_ptr m_ModListProxy; + std::unique_ptr 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 . -*/ - -#include "overwriteinfodialog.h" -#include "ui_overwriteinfodialog.h" -#include "report.h" -#include "utility.h" -#include "organizercore.h" -#include -#include -#include -#include - -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 . +*/ + +#include "overwriteinfodialog.h" +#include "ui_overwriteinfodialog.h" +#include "report.h" +#include "utility.h" +#include "organizercore.h" +#include +#include +#include +#include + +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 . -*/ - -#ifndef OVERWRITEINFODIALOG_H -#define OVERWRITEINFODIALOG_H - -#include "modinfo.h" -#include -#include - -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 . +*/ + +#ifndef OVERWRITEINFODIALOG_H +#define OVERWRITEINFODIALOG_H + +#include "modinfo.h" +#include +#include + +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 -#include -#include -#include - -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 cookies = allCookies(); - data << static_cast(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 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 +#include +#include +#include + +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 cookies = allCookies(); + data << static_cast(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 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 - - -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 + + +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 -#include "iuserinterface.h" -#include -#include "shared/appconfig.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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 -struct PluginTypeName; - -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Plugin"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Diagnose"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Game"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Installer"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Mod Page"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Preview"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Tool"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Proxy"); } }; -template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("File Mapper"); } }; - - -QStringList PluginContainer::pluginInterfaces() -{ - // Find all the names: - QStringList names; - boost::mp11::mp_for_each([&names](const auto* p) { - using plugin_type = std::decay_t; - auto name = PluginTypeName::value(); - if (!name.isEmpty()) { - names.append(name); - } - }); - - return names; -} - - -// PluginRequirementProxy - -const std::set 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 PluginRequirements::proxied() const -{ - std::vector children; - if (dynamic_cast(m_Plugin)) { - for (auto* obj : m_PluginContainer->plugins()) { - auto* plugin = qobject_cast(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 PluginRequirements::children() const -{ - std::vector children; - for (auto* obj : m_PluginContainer->plugins()) { - auto* plugin = qobject_cast(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 PluginRequirements::problems() const -{ - std::vector 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(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(requirement.get())) { - return gdep->gameNames(); - } - } - - return {}; -} - -std::vector PluginRequirements::requiredFor() const -{ - std::vector required; - std::set visited; - requiredFor(required, visited); - return required; -} - - -void PluginRequirements::requiredFor(std::vector &required, std::set& 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(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()); -} - -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([oPlugin, &names](const auto* p) { - using plugin_type = std::decay_t; - if (qobject_cast(oPlugin)) { - auto name = PluginTypeName::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::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([&](const auto* p) { - using plugin_type = std::decay_t; - if (lhsIdx < 0 && qobject_cast(lhs)) { - lhsIdx = count; - } - if (rhsIdx < 0 && qobject_cast(rhs)) { - rhsIdx = count; - } - ++count; - }); - return lhsIdx < rhsIdx; -} - -QStringList PluginContainer::pluginFileNames() const -{ - QStringList result; - for (QPluginLoader *loader : m_PluginLoaders) { - result.append(loader->fileName()); - } - std::vector proxyList = bf::at_key(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(m_Plugins); - auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) { - return qobject_cast(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(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(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(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(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(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(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(plugin); - if (diagnose != nullptr) { - bf::at_key(m_Plugins).push_back(diagnose); - bf::at_key(m_AccessPlugins)[diagnose] = pluginObj; - diagnose->onInvalidated([&]() { emit diagnosisUpdate(); }); - } - } - { // file mapper plugin - IPluginFileMapper *mapper = qobject_cast(plugin); - if (mapper != nullptr) { - bf::at_key(m_Plugins).push_back(mapper); - bf::at_key(m_AccessPlugins)[mapper] = pluginObj; - } - } - { // mod page plugin - IPluginModPage *modPage = qobject_cast(plugin); - if (initPlugin(modPage, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(modPage); - emit pluginRegistered(modPage); - return modPage; - } - } - { // game plugin - IPluginGame *game = qobject_cast(plugin); - if (game) { - game->detectGame(); - if (initPlugin(game, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(game); - registerGame(game); - emit pluginRegistered(game); - return game; - } - } - } - { // tool plugins - IPluginTool *tool = qobject_cast(plugin); - if (initPlugin(tool, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(tool); - emit pluginRegistered(tool); - return tool; - } - } - { // installer plugins - IPluginInstaller *installer = qobject_cast(plugin); - if (initPlugin(installer, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(installer); - if (m_Organizer) { - installer->setInstallationManager(m_Organizer->installationManager()); - } - emit pluginRegistered(installer); - return installer; - } - } - { // preview plugins - IPluginPreview *preview = qobject_cast(plugin); - if (initPlugin(preview, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(preview); - return preview; - } - } - { // proxy plugins - IPluginProxy *proxy = qobject_cast(plugin); - if (initPlugin(proxy, pluginProxy, skipInit)) { - bf::at_key(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(plugin); - if (initPlugin(dummy, pluginProxy, skipInit)) { - bf::at_key(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(m_Organizer->managedGame()); -} - -bool PluginContainer::isEnabled(IPlugin* plugin) const -{ - // Check if it's a game plugin: - if (implementInterface(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(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(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(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& plugins) const -{ - // setUserInterface() - if (m_UserInterface) { - for (auto* plugin : plugins) { - if (auto* proxy = qobject_cast(plugin)) { - proxy->setParentWidget(m_UserInterface->mainWindow()); - } - if (auto* modPage = qobject_cast(plugin)) { - modPage->setParentWidget(m_UserInterface->mainWindow()); - } - if (auto* tool = qobject_cast(plugin)) { - tool->setParentWidget(m_UserInterface->mainWindow()); - } - if (auto* installer = qobject_cast(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(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 PluginContainer::loadProxied(const QString& filepath, IPluginProxy* proxy) -{ - std::vector 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 matchingPlugins = proxy->load(filepath); - - // We are going to group plugin by names and "fix" them later: - std::map> 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 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 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 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()) { - 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(plugin)); - } - - startPluginsImpl(plugins); -} - -void PluginContainer::unloadPlugin(MOBase::IPlugin* plugin, QObject* object) -{ - if (auto* game = qobject_cast(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::value_type; - - // We do not want to remove from QObject since we are iterating over them. - if constexpr (!std::is_same{}) { - auto itp = std::find(t.second.begin(), t.second.end(), qobject_cast(object)); - if (itp != t.second.end()) { - t.second.erase(itp); - } - } - }); - - emit pluginUnregistered(plugin); - - // Remove from the members. - if (auto* diagnose = qobject_cast(object)) { - bf::at_key(m_AccessPlugins).erase(diagnose); - } - if (auto* mapper = qobject_cast(object)) { - bf::at_key(m_AccessPlugins).erase(mapper); - } - - auto& mapNames = bf::at_key(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(m_Plugins); - for (auto it = objects.begin(); it != objects.end(); ) { - auto* plugin = qobject_cast(*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(m_Plugins).push_back(this); - - if (m_Organizer) { - bf::at_key(m_Plugins).push_back(m_Organizer); - m_Organizer->connectPlugins(this); - } -} - -std::vector PluginContainer::activeProblems() const -{ - std::vector 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:") + "
      "; - for (const QString &plugin : m_FailedPlugins) { - result += "
    • " + plugin + "
    • "; - } - result += "
        "; - 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 +#include "iuserinterface.h" +#include +#include "shared/appconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 +struct PluginTypeName; + +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Plugin"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Diagnose"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Game"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Installer"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Mod Page"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Preview"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Tool"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("Proxy"); } }; +template <> struct PluginTypeName { static QString value() { return QT_TR_NOOP("File Mapper"); } }; + + +QStringList PluginContainer::pluginInterfaces() +{ + // Find all the names: + QStringList names; + boost::mp11::mp_for_each([&names](const auto* p) { + using plugin_type = std::decay_t; + auto name = PluginTypeName::value(); + if (!name.isEmpty()) { + names.append(name); + } + }); + + return names; +} + + +// PluginRequirementProxy + +const std::set 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 PluginRequirements::proxied() const +{ + std::vector children; + if (dynamic_cast(m_Plugin)) { + for (auto* obj : m_PluginContainer->plugins()) { + auto* plugin = qobject_cast(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 PluginRequirements::children() const +{ + std::vector children; + for (auto* obj : m_PluginContainer->plugins()) { + auto* plugin = qobject_cast(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 PluginRequirements::problems() const +{ + std::vector 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(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(requirement.get())) { + return gdep->gameNames(); + } + } + + return {}; +} + +std::vector PluginRequirements::requiredFor() const +{ + std::vector required; + std::set visited; + requiredFor(required, visited); + return required; +} + + +void PluginRequirements::requiredFor(std::vector &required, std::set& 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(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()); +} + +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([oPlugin, &names](const auto* p) { + using plugin_type = std::decay_t; + if (qobject_cast(oPlugin)) { + auto name = PluginTypeName::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::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([&](const auto* p) { + using plugin_type = std::decay_t; + if (lhsIdx < 0 && qobject_cast(lhs)) { + lhsIdx = count; + } + if (rhsIdx < 0 && qobject_cast(rhs)) { + rhsIdx = count; + } + ++count; + }); + return lhsIdx < rhsIdx; +} + +QStringList PluginContainer::pluginFileNames() const +{ + QStringList result; + for (QPluginLoader *loader : m_PluginLoaders) { + result.append(loader->fileName()); + } + std::vector proxyList = bf::at_key(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(m_Plugins); + auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) { + return qobject_cast(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(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(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(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(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(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(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(plugin); + if (diagnose != nullptr) { + bf::at_key(m_Plugins).push_back(diagnose); + bf::at_key(m_AccessPlugins)[diagnose] = pluginObj; + diagnose->onInvalidated([&]() { emit diagnosisUpdate(); }); + } + } + { // file mapper plugin + IPluginFileMapper *mapper = qobject_cast(plugin); + if (mapper != nullptr) { + bf::at_key(m_Plugins).push_back(mapper); + bf::at_key(m_AccessPlugins)[mapper] = pluginObj; + } + } + { // mod page plugin + IPluginModPage *modPage = qobject_cast(plugin); + if (initPlugin(modPage, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(modPage); + emit pluginRegistered(modPage); + return modPage; + } + } + { // game plugin + IPluginGame *game = qobject_cast(plugin); + if (game) { + game->detectGame(); + if (initPlugin(game, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(game); + registerGame(game); + emit pluginRegistered(game); + return game; + } + } + } + { // tool plugins + IPluginTool *tool = qobject_cast(plugin); + if (initPlugin(tool, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(tool); + emit pluginRegistered(tool); + return tool; + } + } + { // installer plugins + IPluginInstaller *installer = qobject_cast(plugin); + if (initPlugin(installer, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(installer); + if (m_Organizer) { + installer->setInstallationManager(m_Organizer->installationManager()); + } + emit pluginRegistered(installer); + return installer; + } + } + { // preview plugins + IPluginPreview *preview = qobject_cast(plugin); + if (initPlugin(preview, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(preview); + return preview; + } + } + { // proxy plugins + IPluginProxy *proxy = qobject_cast(plugin); + if (initPlugin(proxy, pluginProxy, skipInit)) { + bf::at_key(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(plugin); + if (initPlugin(dummy, pluginProxy, skipInit)) { + bf::at_key(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(m_Organizer->managedGame()); +} + +bool PluginContainer::isEnabled(IPlugin* plugin) const +{ + // Check if it's a game plugin: + if (implementInterface(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(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(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(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& plugins) const +{ + // setUserInterface() + if (m_UserInterface) { + for (auto* plugin : plugins) { + if (auto* proxy = qobject_cast(plugin)) { + proxy->setParentWidget(m_UserInterface->mainWindow()); + } + if (auto* modPage = qobject_cast(plugin)) { + modPage->setParentWidget(m_UserInterface->mainWindow()); + } + if (auto* tool = qobject_cast(plugin)) { + tool->setParentWidget(m_UserInterface->mainWindow()); + } + if (auto* installer = qobject_cast(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(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 PluginContainer::loadProxied(const QString& filepath, IPluginProxy* proxy) +{ + std::vector 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 matchingPlugins = proxy->load(filepath); + + // We are going to group plugin by names and "fix" them later: + std::map> 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 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 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 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()) { + 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(plugin)); + } + + startPluginsImpl(plugins); +} + +void PluginContainer::unloadPlugin(MOBase::IPlugin* plugin, QObject* object) +{ + if (auto* game = qobject_cast(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::value_type; + + // We do not want to remove from QObject since we are iterating over them. + if constexpr (!std::is_same{}) { + auto itp = std::find(t.second.begin(), t.second.end(), qobject_cast(object)); + if (itp != t.second.end()) { + t.second.erase(itp); + } + } + }); + + emit pluginUnregistered(plugin); + + // Remove from the members. + if (auto* diagnose = qobject_cast(object)) { + bf::at_key(m_AccessPlugins).erase(diagnose); + } + if (auto* mapper = qobject_cast(object)) { + bf::at_key(m_AccessPlugins).erase(mapper); + } + + auto& mapNames = bf::at_key(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(m_Plugins); + for (auto it = objects.begin(); it != objects.end(); ) { + auto* plugin = qobject_cast(*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(m_Plugins).push_back(this); + + if (m_Organizer) { + bf::at_key(m_Plugins).push_back(m_Organizer); + m_Organizer->connectPlugins(this); + } +} + +std::vector PluginContainer::activeProblems() const +{ + std::vector 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:") + "
          "; + for (const QString &plugin : m_FailedPlugins) { + result += "
        • " + plugin + "
        • "; + } + result += "
            "; + 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef Q_MOC_RUN -#include -#include -#include -#endif // Q_MOC_RUN -#include -#include - - -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 proxied() const; - - /** - * @return the master of this plugin, if any. - */ - MOBase::IPlugin* master() const; - - /** - * @return the plugins this plugin is master of. - */ - std::vector children() const; - - /** - * @return the list of problems to be resolved before enabling the plugin. - */ - std::vector 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 requiredFor() const; - -private: - - // The list of "Core" plugins. - static const std::set s_CorePlugins; - - // Accumulator version for requiredFor() to avoid infinite recursion. - void requiredFor(std::vector& required, std::set& 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> m_Requirements; - OrganizerProxy* m_Organizer; - std::vector 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>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair> - >; - - using AccessPluginMap = boost::fusion::map< - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair> - >; - - 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::value == boost::mp11::mp_size::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 - const std::vector &plugins() const { - typename boost::fusion::result_of::at_key::type temp = boost::fusion::at_key(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 - 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(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 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 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 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& 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 m_Requirements; - - std::map m_SupportedGames; - QStringList m_FailedPlugins; - std::vector 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef Q_MOC_RUN +#include +#include +#include +#endif // Q_MOC_RUN +#include +#include + + +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 proxied() const; + + /** + * @return the master of this plugin, if any. + */ + MOBase::IPlugin* master() const; + + /** + * @return the plugins this plugin is master of. + */ + std::vector children() const; + + /** + * @return the list of problems to be resolved before enabling the plugin. + */ + std::vector 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 requiredFor() const; + +private: + + // The list of "Core" plugins. + static const std::set s_CorePlugins; + + // Accumulator version for requiredFor() to avoid infinite recursion. + void requiredFor(std::vector& required, std::set& 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> m_Requirements; + OrganizerProxy* m_Organizer; + std::vector 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>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair> + >; + + using AccessPluginMap = boost::fusion::map< + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair> + >; + + 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::value == boost::mp11::mp_size::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 + const std::vector &plugins() const { + typename boost::fusion::result_of::at_key::type temp = boost::fusion::at_key(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 + 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(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 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 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 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& 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 m_Requirements; + + std::map m_SupportedGames; + QStringList m_FailedPlugins; + std::vector 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 . -*/ - -#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 -#include -#include -#include -#include "shared/windows_error.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#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& 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::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(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 layoutChange(this); - - QStringList primaryPlugins = m_GamePlugin->primaryPlugins(); - GamePlugins *gamePlugins = m_GamePlugin->feature(); - const bool lightPluginsAreSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false; - - m_CurrentProfile = profileName; - - QStringList availablePlugins; - - std::vector 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 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(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::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> 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 &lhs, const std::pair &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::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 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 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::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::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::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 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& 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(); - 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 layoutChange(this); - syncLoadOrder(); - // set priorities according to locked load order - std::map lockedLoadOrder; - std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(), - [&lockedLoadOrder] (const std::pair &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(m_ESPs.size() - 1)) && - (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) { - ++targetPrio; - } - - if (static_cast(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(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&)>& func) -{ - return m_PluginStateChanged.connect(func); -} - -void PluginList::pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const { - if (pluginNames.isEmpty()) { - return; - } - std::map infos; - for (auto& name : pluginNames) { - infos[name] = state; - } - m_PluginStateChanged(infos); -} - -boost::signals2::connection PluginList::onRefreshed(const std::function &callback) -{ - return m_Refreshed.connect(callback); -} - -boost::signals2::connection PluginList::onPluginMoved(const std::function &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(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(m_ESPs[i].priority)) = i; - } - - generatePluginIndexes(); -} - -void PluginList::generatePluginIndexes() -{ - int numESLs = 0; - int numSkipped = 0; - - GamePlugins* gamePlugins = m_GamePlugin->feature(); - 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(m_ESPs.size()); - } else { - return 0; - } -} - -int PluginList::columnCount(const QModelIndex &) const -{ - return COL_LASTCOLUMN + 1; -} - -void PluginList::testMasters() -{ - std::set 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 += "" + tr("Origin") + ": " + esp.originName; - - if (esp.forceEnabled) { - toolTip += - "
            " + - tr("This plugin can't be disabled (enforced by the game).") + - ""; - } else { - if (!esp.author.isEmpty()) { - toolTip += - "
            " + tr("Author") + ": " + - TruncateString(esp.author); - } - - if (esp.description.size() > 0) { - toolTip += - "
            " + tr("Description") + ": " + - TruncateString(esp.description); - } - - if (esp.masterUnset.size() > 0) { - toolTip += - "
            " + tr("Missing Masters") + ": " + - "" + TruncateString(QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) + ""; - } - - std::set 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 += - "
            " + tr("Enabled Masters") + ": " + - TruncateString(SetJoin(enabledMasters, ", ")); - } - - if (!esp.archives.empty()) { - toolTip += - "
            " + tr("Loads Archives") + ": " + - TruncateString(QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) + - "
            " + 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 += - "
            " + tr("Loads INI settings") + ": " - "
            " + 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 += - "

            " + 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 += "
              "; - - for (auto&& message : itor->second.messages) { - toolTip += "
            • " + message + "
            • "; - } - - toolTip += "
            "; - } - - // loot - toolTip += makeLootTooltip(itor->second.loot); - } - - return toolTip; -} - -QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const -{ - QString s; - - for (auto&& f : loot.incompatibilities) { - s += - "
          • " + tr("Incompatible with %1") - .arg(f.displayName.isEmpty() ? f.name : f.displayName) + - "
          • "; - } - - for (auto&& m : loot.missingMasters) { - s += "
          • " + tr("Depends on missing %1").arg(m) + "
          • "; - } - - for (auto&& m : loot.messages) { - s += "
          • "; - - 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 + "
          • "; - } - - for (auto&& d : loot.dirty) { - s += "
          • " + d.toString(false) + "
          • "; - } - - for (auto&& c : loot.clean) { - s += "
          • " + c.toString(true) + "
          • "; - } - - if (!s.isEmpty()) { - s = - "
            " - "
              " + - s + - "
            "; - } - - 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(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(m_ESPsByPriority.size())) - newPriorityTemp = static_cast(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(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(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 rows, int newPriority) -{ - ChangeBracket layoutChange(this); - const std::vector &esp = m_ESPs; - - int minPriority = INT_MAX; - int maxPriority = INT_MIN; - - // don't try to move plugins before force-enabled plugins - for (std::vector::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::const_iterator iter = rows.begin(); - iter != rows.end(); ++iter) { - if (m_ESPs[*iter].priority < newPriority) { - --newPriority; - break; - } - } - - for (std::vector::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 sourceRows; - - while (!stream.atEnd()) { - int sourceRow, col; - QMap 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(m_ESPs.size()))) { - newPriority = static_cast(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 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 . +*/ + +#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 +#include +#include +#include +#include "shared/windows_error.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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& 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::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(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 layoutChange(this); + + QStringList primaryPlugins = m_GamePlugin->primaryPlugins(); + GamePlugins *gamePlugins = m_GamePlugin->feature(); + const bool lightPluginsAreSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false; + + m_CurrentProfile = profileName; + + QStringList availablePlugins; + + std::vector 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 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(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::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> 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 &lhs, const std::pair &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::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 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 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::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::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::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 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& 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(); + 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 layoutChange(this); + syncLoadOrder(); + // set priorities according to locked load order + std::map lockedLoadOrder; + std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(), + [&lockedLoadOrder] (const std::pair &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(m_ESPs.size() - 1)) && + (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) { + ++targetPrio; + } + + if (static_cast(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(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&)>& func) +{ + return m_PluginStateChanged.connect(func); +} + +void PluginList::pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const { + if (pluginNames.isEmpty()) { + return; + } + std::map infos; + for (auto& name : pluginNames) { + infos[name] = state; + } + m_PluginStateChanged(infos); +} + +boost::signals2::connection PluginList::onRefreshed(const std::function &callback) +{ + return m_Refreshed.connect(callback); +} + +boost::signals2::connection PluginList::onPluginMoved(const std::function &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(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(m_ESPs[i].priority)) = i; + } + + generatePluginIndexes(); +} + +void PluginList::generatePluginIndexes() +{ + int numESLs = 0; + int numSkipped = 0; + + GamePlugins* gamePlugins = m_GamePlugin->feature(); + 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(m_ESPs.size()); + } else { + return 0; + } +} + +int PluginList::columnCount(const QModelIndex &) const +{ + return COL_LASTCOLUMN + 1; +} + +void PluginList::testMasters() +{ + std::set 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 += "" + tr("Origin") + ": " + esp.originName; + + if (esp.forceEnabled) { + toolTip += + "
            " + + tr("This plugin can't be disabled (enforced by the game).") + + ""; + } else { + if (!esp.author.isEmpty()) { + toolTip += + "
            " + tr("Author") + ": " + + TruncateString(esp.author); + } + + if (esp.description.size() > 0) { + toolTip += + "
            " + tr("Description") + ": " + + TruncateString(esp.description); + } + + if (esp.masterUnset.size() > 0) { + toolTip += + "
            " + tr("Missing Masters") + ": " + + "" + TruncateString(QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) + ""; + } + + std::set 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 += + "
            " + tr("Enabled Masters") + ": " + + TruncateString(SetJoin(enabledMasters, ", ")); + } + + if (!esp.archives.empty()) { + toolTip += + "
            " + tr("Loads Archives") + ": " + + TruncateString(QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) + + "
            " + 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 += + "
            " + tr("Loads INI settings") + ": " + "
            " + 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 += + "

            " + 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 += "
              "; + + for (auto&& message : itor->second.messages) { + toolTip += "
            • " + message + "
            • "; + } + + toolTip += "
            "; + } + + // loot + toolTip += makeLootTooltip(itor->second.loot); + } + + return toolTip; +} + +QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const +{ + QString s; + + for (auto&& f : loot.incompatibilities) { + s += + "
          • " + tr("Incompatible with %1") + .arg(f.displayName.isEmpty() ? f.name : f.displayName) + + "
          • "; + } + + for (auto&& m : loot.missingMasters) { + s += "
          • " + tr("Depends on missing %1").arg(m) + "
          • "; + } + + for (auto&& m : loot.messages) { + s += "
          • "; + + 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 + "
          • "; + } + + for (auto&& d : loot.dirty) { + s += "
          • " + d.toString(false) + "
          • "; + } + + for (auto&& c : loot.clean) { + s += "
          • " + c.toString(true) + "
          • "; + } + + if (!s.isEmpty()) { + s = + "
            " + "
              " + + s + + "
            "; + } + + 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(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(m_ESPsByPriority.size())) + newPriorityTemp = static_cast(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(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(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 rows, int newPriority) +{ + ChangeBracket layoutChange(this); + const std::vector &esp = m_ESPs; + + int minPriority = INT_MAX; + int maxPriority = INT_MIN; + + // don't try to move plugins before force-enabled plugins + for (std::vector::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::const_iterator iter = rows.begin(); + iter != rows.end(); ++iter) { + if (m_ESPs[*iter].priority < newPriority) { + --newPriority; + break; + } + } + + for (std::vector::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 sourceRows; + + while (!stream.atEnd()) { + int sourceRow, col; + QMap 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(m_ESPs.size()))) { + newPriority = static_cast(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 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 . -*/ - -#ifndef PLUGINLIST_H -#define PLUGINLIST_H - -#include -#include -#include "profile.h" -#include "loot.h" - -namespace MOBase { class IPluginGame; } - -#include -#include -#include -#include -#include -#include - -#pragma warning(push) -#pragma warning(disable: 4100) -#ifndef Q_MOC_RUN -#include -#include -#endif - -#include -#include - -class OrganizerCore; - - -template -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; -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; - using SignalPluginMoved = boost::signals2::signal; - using SignalPluginStateChanged = boost::signals2::signal&)>; - -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& 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& callback); - boost::signals2::connection onPluginMoved(const std::function& func); - boost::signals2::connection onPluginStateChanged(const std::function&)> &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 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 archives; - std::set masters; - mutable std::set 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 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 m_ESPs; - mutable std::map m_LastSaveHash; - - std::map m_ESPsByName; - std::vector m_ESPsByPriority; - - std::map m_LockedOrder; - - std::map 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 . +*/ + +#ifndef PLUGINLIST_H +#define PLUGINLIST_H + +#include +#include +#include "profile.h" +#include "loot.h" + +namespace MOBase { class IPluginGame; } + +#include +#include +#include +#include +#include +#include + +#pragma warning(push) +#pragma warning(disable: 4100) +#ifndef Q_MOC_RUN +#include +#include +#endif + +#include +#include + +class OrganizerCore; + + +template +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; +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; + using SignalPluginMoved = boost::signals2::signal; + using SignalPluginStateChanged = boost::signals2::signal&)>; + +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& 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& callback); + boost::signals2::connection onPluginMoved(const std::function& func); + boost::signals2::connection onPluginStateChanged(const std::function&)> &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 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 archives; + std::set masters; + mutable std::set 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 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 m_ESPs; + mutable std::map m_LastSaveHash; + + std::map m_ESPsByName; + std::vector m_ESPsByPriority; + + std::map m_LockedOrder; + + std::map 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 . -*/ - -#include "pluginlistsortproxy.h" -#include "messagedialog.h" -#include -#include -#include -#include -#include - - -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(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("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 . +*/ + +#include "pluginlistsortproxy.h" +#include "messagedialog.h" +#include +#include +#include +#include +#include + + +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(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("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 . -*/ - -#ifndef PLUGINLISTSORTPROXY_H -#define PLUGINLISTSORTPROXY_H - - -#include -#include -#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 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 . +*/ + +#ifndef PLUGINLISTSORTPROXY_H +#define PLUGINLISTSORTPROXY_H + + +#include +#include +#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 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 - -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 + +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 - -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 + +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 . -*/ - -#include "previewgenerator.h" - -#include -#include -#include -#include -#include - -#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(); - 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(); - 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 . +*/ + +#include "previewgenerator.h" + +#include +#include +#include +#include +#include + +#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(); + 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(); + 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 . -*/ - -#ifndef PREVIEWGENERATOR_H -#define PREVIEWGENERATOR_H - -#include -#include -#include -#include -#include - -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 . +*/ + +#ifndef PREVIEWGENERATOR_H +#define PREVIEWGENERATOR_H + +#include +#include +#include +#include +#include + +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 -#include -#include -#include -#include - -#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()) { - if (!m_PluginContainer.isEnabled(diagnose)) { - continue; - } - - std::vector 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(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(fixButton ->property("fix").value()); - 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 +#include +#include +#include +#include + +#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()) { + if (!m_PluginContainer.isEnabled(diagnose)) { + continue; + } + + std::vector 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(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(fixButton ->property("fix").value()); + 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 -#include -#include - - -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 +#include +#include + + +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 . -*/ - -#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 . +*/ + +#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 . -*/ - -#ifndef PROFILEINPUTDIALOG_H -#define PROFILEINPUTDIALOG_H - -#include - -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 . +*/ + +#ifndef PROFILEINPUTDIALOG_H +#define PROFILEINPUTDIALOG_H + +#include + +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 . -*/ - -#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 -#include -#include -#include -#include -#include -#include - -#include - -#include - -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(); - - if (invalidation == nullptr) { - ui->invalidationBox->setToolTip(tr("Archive invalidation isn't required for this game.")); - ui->invalidationBox->setEnabled(false); - } - - if (!m_Game->feature()) { - 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(); - - if (!currentProfile) { - return; - } - - m_Selected = currentProfile->name(); - close(); -} - -std::optional 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(); - 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(); - 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(); - - 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(); - 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(); - - 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(); - - 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(); - 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(); - - 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 . +*/ + +#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 +#include +#include +#include +#include +#include +#include + +#include + +#include + +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(); + + if (invalidation == nullptr) { + ui->invalidationBox->setToolTip(tr("Archive invalidation isn't required for this game.")); + ui->invalidationBox->setEnabled(false); + } + + if (!m_Game->feature()) { + 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(); + + if (!currentProfile) { + return; + } + + m_Selected = currentProfile->name(); + close(); +} + +std::optional 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(); + 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(); + 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(); + + 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(); + 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(); + + 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(); + + 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(); + 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(); + + 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 . -*/ - -#ifndef PROFILESDIALOG_H -#define PROFILESDIALOG_H - -#include "tutorabledialog.h" -class Profile; -class OrganizerCore; - -class QListWidget; -class QListWidgetItem; -#include -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 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 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 . +*/ + +#ifndef PROFILESDIALOG_H +#define PROFILESDIALOG_H + +#include "tutorabledialog.h" +class Profile; +class OrganizerCore; + +class QListWidget; +class QListWidgetItem; +#include +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 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 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 * - * * - * 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 . * - ****************************************************************************************/ - -// Modifications 2013-03-27 to 2013-03-29 by Sebastian Herbord - - -#include "qtgroupingproxy.h" -#include - -#include -#include -#include - -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 -QtGroupingProxy::belongsTo( const QModelIndex &idx ) -{ - QList 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 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::max(); - std::vector rmgroups; - - QMap > 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 -QtGroupingProxy::addSourceRow( const QModelIndex &idx ) -{ - QList updatedGroups; - QList 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::max() ) ) - m_groupMap.insert( std::numeric_limits::max(), QList() ); //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() ); //add an empty placeholder - } - - if( !updatedGroups.contains( updatedGroup ) ) - updatedGroups << updatedGroup; - } - - //update m_groupMap to the new source-model layout (one row added) - QMutableMapIterator > i( m_groupMap ); - while( i.hasNext() ) - { - i.next(); - QList &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::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()); - 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 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 > 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::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 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 >::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 &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 > 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 &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::max() ); - - log::debug("{}", s); -} +/**************************************************************************************** + * Copyright (c) 2007-2011 Bart Cerneels * + * * + * 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 . * + ****************************************************************************************/ + +// Modifications 2013-03-27 to 2013-03-29 by Sebastian Herbord + + +#include "qtgroupingproxy.h" +#include + +#include +#include +#include + +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 +QtGroupingProxy::belongsTo( const QModelIndex &idx ) +{ + QList 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 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::max(); + std::vector rmgroups; + + QMap > 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 +QtGroupingProxy::addSourceRow( const QModelIndex &idx ) +{ + QList updatedGroups; + QList 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::max() ) ) + m_groupMap.insert( std::numeric_limits::max(), QList() ); //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() ); //add an empty placeholder + } + + if( !updatedGroups.contains( updatedGroup ) ) + updatedGroups << updatedGroup; + } + + //update m_groupMap to the new source-model layout (one row added) + QMutableMapIterator > i( m_groupMap ); + while( i.hasNext() ) + { + i.next(); + QList &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::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()); + 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 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 > 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::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 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 >::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 &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 > 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 &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::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 * - * * - * 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 . * - ****************************************************************************************/ - -// Modifications 2013-03-27 to 2013-03-28 by Sebastian Herbord - -#ifndef GROUPINGPROXY_H -#define GROUPINGPROXY_H - -#include -#include -#include -#include -#include -#include - -typedef QMap ItemData; -typedef QMap 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 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 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 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 > m_groupMap; - /** The data cache of the groups. - * This can be pre-loaded with data in belongsTo() - */ - QList 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 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 * + * * + * 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 . * + ****************************************************************************************/ + +// Modifications 2013-03-27 to 2013-03-28 by Sebastian Herbord + +#ifndef GROUPINGPROXY_H +#define GROUPINGPROXY_H + +#include +#include +#include +#include +#include +#include + +typedef QMap ItemData; +typedef QMap 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 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 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 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 > m_groupMap; + /** The data cache of the groups. + * This can be pre-loaded with data in belongsTo() + */ + QList 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 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 . -*/ - -#include "queryoverwritedialog.h" -#include "ui_queryoverwritedialog.h" - -#include - -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 . +*/ + +#include "queryoverwritedialog.h" +#include "ui_queryoverwritedialog.h" + +#include + +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 . -*/ - -#ifndef QUERYOVERWRITEDIALOG_H -#define QUERYOVERWRITEDIALOG_H - -#include - -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 . +*/ + +#ifndef QUERYOVERWRITEDIALOG_H +#define QUERYOVERWRITEDIALOG_H + +#include + +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 -#include -#include - - -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 +#include +#include + + +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 - -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 + +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 . -*/ - -#include "selectiondialog.h" -#include "ui_selectiondialog.h" - -#include - -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(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 . +*/ + +#include "selectiondialog.h" +#include "ui_selectiondialog.h" + +#include + +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(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 . -*/ - -#ifndef SELECTIONDIALOG_H -#define SELECTIONDIALOG_H - -#include -#include - -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 . +*/ + +#ifndef SELECTIONDIALOG_H +#define SELECTIONDIALOG_H + +#include +#include + +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 . -*/ - -#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 -#include -#include "shared/util.h" -#include "updatedialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include //for VS_FIXEDFILEINFO, GetLastError - -#include -#include -#include //for size_t -#include - -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 . +*/ + +#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 +#include +#include "shared/util.h" +#include "updatedialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include //for VS_FIXEDFILEINFO, GetLastError + +#include +#include +#include //for size_t +#include + +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 . -*/ - -#ifndef SELFUPDATER_H -#define SELFUPDATER_H - -#include - -#include -#include - -class Archive; -class NexusInterface; -class PluginContainer; -namespace MOBase { class IPluginGame; } - -#include -#include -#include -#include -#include //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>; - 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 . +*/ + +#ifndef SELFUPDATER_H +#define SELFUPDATER_H + +#include + +#include +#include + +class Archive; +class NexusInterface; +class PluginContainer; +namespace MOBase { class IPluginGame; } + +#include +#include +#include +#include +#include //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>; + 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(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(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 -#include -#include - -class ServerInfo -{ -public: - using SpeedList = std::vector; - - 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; - 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 +#include +#include + +class ServerInfo +{ +public: + using SpeedList = std::vector; + + 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; + 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 . -*/ - -#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 . +*/ + +#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 . -*/ - -#ifndef APPCONFIG_H -#define APPCONFIG_H - -#include - -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 . +*/ + +#ifndef APPCONFIG_H +#define APPCONFIG_H + +#include + +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 . -*/ - -#include "directoryentry.h" -#include "originconnection.h" -#include "filesorigin.h" -#include "fileentry.h" -#include "../envfs.h" -#include "util.h" -#include "windows_error.h" -#include -#include - -namespace MOShared -{ - -using namespace MOBase; -const int MAXPATH_UNICODE = 32767; - -template -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, - boost::shared_ptr 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& archives, - const std::set& enabledArchives, - const std::vector& 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 DirectoryEntry::getFiles() const -{ - std::vector 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 &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 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; igetFile(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; igetSubFolder(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& 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 . +*/ + +#include "directoryentry.h" +#include "originconnection.h" +#include "filesorigin.h" +#include "fileentry.h" +#include "../envfs.h" +#include "util.h" +#include "windows_error.h" +#include +#include + +namespace MOShared +{ + +using namespace MOBase; +const int MAXPATH_UNICODE = 32767; + +template +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, + boost::shared_ptr 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& archives, + const std::set& enabledArchives, + const std::vector& 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 DirectoryEntry::getFiles() const +{ + std::vector 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 &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 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; igetFile(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; igetSubFolder(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& 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 . -*/ - -#ifndef MO_REGISTER_DIRECTORYENTRY_INCLUDED -#define MO_REGISTER_DIRECTORYENTRY_INCLUDED - -#include "fileregister.h" -#include - -namespace env -{ - class DirectoryWalker; - struct Directory; - struct File; -} - -namespace std -{ - template <> - struct hash - { - 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( - std::wstring name, DirectoryEntry* parent, OriginID originID); - - DirectoryEntry( - std::wstring name, DirectoryEntry* parent, OriginID originID, - boost::shared_ptr fileRegister, - boost::shared_ptr 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& archives, - const std::set& enabledArchives, - const std::vector& 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 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 getFiles() const; - - const SubDirectories& getSubDirectories() const - { - return m_SubDirectories; - } - - template - void forEachDirectory(F&& f) const - { - for (auto&& d : m_SubDirectories) { - if (!f(*d)) { - break; - } - } - } - - template - void forEachFile(F&& f) const - { - for (auto&& p : m_Files) { - if (auto file=m_FileRegister->getFile(p.second)) { - if (!f(*file)) { - break; - } - } - } - } - - template - 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& indices); - - void dump(const std::wstring& file) const; - -private: - using FilesMap = std::map; - using FilesLookup = std::unordered_map; - using SubDirectoriesLookup = std::unordered_map; - - boost::shared_ptr m_FileRegister; - boost::shared_ptr m_OriginConnection; - - std::wstring m_Name; - FilesMap m_Files; - FilesLookup m_FilesLookup; - SubDirectories m_SubDirectories; - SubDirectoriesLookup m_SubDirectoriesLookup; - - DirectoryEntry* m_Parent; - std::set 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& 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::result_type - hash::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 . +*/ + +#ifndef MO_REGISTER_DIRECTORYENTRY_INCLUDED +#define MO_REGISTER_DIRECTORYENTRY_INCLUDED + +#include "fileregister.h" +#include + +namespace env +{ + class DirectoryWalker; + struct Directory; + struct File; +} + +namespace std +{ + template <> + struct hash + { + 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( + std::wstring name, DirectoryEntry* parent, OriginID originID); + + DirectoryEntry( + std::wstring name, DirectoryEntry* parent, OriginID originID, + boost::shared_ptr fileRegister, + boost::shared_ptr 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& archives, + const std::set& enabledArchives, + const std::vector& 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 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 getFiles() const; + + const SubDirectories& getSubDirectories() const + { + return m_SubDirectories; + } + + template + void forEachDirectory(F&& f) const + { + for (auto&& d : m_SubDirectories) { + if (!f(*d)) { + break; + } + } + } + + template + void forEachFile(F&& f) const + { + for (auto&& p : m_Files) { + if (auto file=m_FileRegister->getFile(p.second)) { + if (!f(*file)) { + break; + } + } + } + } + + template + 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& indices); + + void dump(const std::wstring& file) const; + +private: + using FilesMap = std::map; + using FilesLookup = std::unordered_map; + using SubDirectoriesLookup = std::unordered_map; + + boost::shared_ptr m_FileRegister; + boost::shared_ptr m_OriginConnection; + + std::wstring m_Name; + FilesMap m_Files; + FilesLookup m_FilesLookup; + SubDirectories m_SubDirectories; + SubDirectoriesLookup m_SubDirectoriesLookup; + + DirectoryEntry* m_Parent; + std::set 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& 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::result_type + hash::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 . -*/ - -#include "util.h" -#include "windows_error.h" -#include "../mainwindow.h" -#include "../env.h" -#include -#include -#include - -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(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(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(text.c_str()), static_cast(text.size())); - return text; -} - -std::string ToLowerCopy(const std::string& text) -{ - std::string result(text); - CharLowerBuffA(const_cast(result.c_str()), static_cast(result.size())); - return result; -} - -std::wstring& ToLowerInPlace(std::wstring& text) -{ - CharLowerBuffW(const_cast(text.c_str()), static_cast(text.size())); - return text; -} - -std::wstring ToLowerCopy(const std::wstring& text) -{ - std::wstring result(text); - CharLowerBuffW(const_cast(result.c_str()), static_cast(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 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 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( - ::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( - 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() - 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; iisSeparator()) { - continue; - } - - const char shortcut1 = shortcutChar(action1); - if (shortcut1 == 0) { - continue; - } - - for (int j=i+1; jisSeparator()) { - 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(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 . +*/ + +#include "util.h" +#include "windows_error.h" +#include "../mainwindow.h" +#include "../env.h" +#include +#include +#include + +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(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(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(text.c_str()), static_cast(text.size())); + return text; +} + +std::string ToLowerCopy(const std::string& text) +{ + std::string result(text); + CharLowerBuffA(const_cast(result.c_str()), static_cast(result.size())); + return result; +} + +std::wstring& ToLowerInPlace(std::wstring& text) +{ + CharLowerBuffW(const_cast(text.c_str()), static_cast(text.size())); + return text; +} + +std::wstring ToLowerCopy(const std::wstring& text) +{ + std::wstring result(text); + CharLowerBuffW(const_cast(result.c_str()), static_cast(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 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 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( + ::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( + 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() - 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; iisSeparator()) { + continue; + } + + const char shortcut1 = shortcutChar(action1); + if (shortcut1 == 0) { + continue; + } + + for (int j=i+1; jisSeparator()) { + 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(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 . -*/ - -#ifndef UTIL_H -#define UTIL_H - -#include -#include -#include -#include - -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; -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 . +*/ + +#ifndef UTIL_H +#define UTIL_H + +#include +#include +#include +#include + +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; +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 . -*/ - -#include "windows_error.h" -#include - -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 . +*/ + +#include "windows_error.h" +#include + +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 . -*/ - -#ifndef WINDOWS_ERROR_H -#define WINDOWS_ERROR_H - - -#include -#define WIN32_LEAN_AND_MEAN -#include - -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 . +*/ + +#ifndef WINDOWS_ERROR_H +#define WINDOWS_ERROR_H + + +#include +#define WIN32_LEAN_AND_MEAN +#include + +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 . -*/ - -#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 -#include -#include -#include -#include -#include "shared/appconfig.h" -#include "shared/windows_error.h" -#include -#include -#include -#include -#include -#include - -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(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); - } else { - success = ::CreateProcess( - nullptr, const_cast(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 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(reinterpret_cast(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 . +*/ + +#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 +#include +#include +#include +#include +#include "shared/appconfig.h" +#include "shared/windows_error.h" +#include +#include +#include +#include +#include +#include + +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(wcommandLine.c_str()), nullptr, nullptr, + inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); + } else { + success = ::CreateProcess( + nullptr, const_cast(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 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(reinterpret_cast(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 . -*/ - -#ifndef SPAWN_H -#define SPAWN_H - -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include - -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 . +*/ + +#ifndef SPAWN_H +#define SPAWN_H + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +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 . -*/ - -#include "syncoverwritedialog.h" -#include "shared/directoryentry.h" -#include "shared/fileentry.h" -#include "shared/filesorigin.h" -#include "ui_syncoverwritedialog.h" - -#include -#include -#include - -#include -#include -#include -#include - - -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(""), -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("")); - 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(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 . +*/ + +#include "syncoverwritedialog.h" +#include "shared/directoryentry.h" +#include "shared/fileentry.h" +#include "shared/filesorigin.h" +#include "ui_syncoverwritedialog.h" + +#include +#include +#include + +#include +#include +#include +#include + + +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(""), -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("")); + 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(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 . -*/ - -#ifndef SYNCOVERWRITEDIALOG_H -#define SYNCOVERWRITEDIALOG_H - -#include "tutorabledialog.h" -#include "shared/fileregisterfwd.h" -#include - -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 . +*/ + +#ifndef SYNCOVERWRITEDIALOG_H +#define SYNCOVERWRITEDIALOG_H + +#include "tutorabledialog.h" +#include "shared/fileregisterfwd.h" +#include + +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 . -*/ - -#include "transfersavesdialog.h" - -#include "ui_transfersavesdialog.h" -#include "iplugingame.h" -#include "isavegame.h" -#include "savegameinfo.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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 &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 . +*/ + +#include "transfersavesdialog.h" + +#include "ui_transfersavesdialog.h" +#include "iplugingame.h" +#include "isavegame.h" +#include "savegameinfo.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 &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 . -*/ - -#ifndef TRANSFERSAVESDIALOG_H -#define TRANSFERSAVESDIALOG_H - -#include "tutorabledialog.h" -#include "profile.h" - -class QListWidget; -#include -class QPushButton; -#include -class QWidget; - -#include -#include -#include - -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; - using SaveList = std::vector; - using SaveCollection = std::map; - - 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 &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 . +*/ + +#ifndef TRANSFERSAVESDIALOG_H +#define TRANSFERSAVESDIALOG_H + +#include "tutorabledialog.h" +#include "profile.h" + +class QListWidget; +#include +class QPushButton; +#include +class QWidget; + +#include +#include +#include + +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; + using SaveList = std::vector; + using SaveCollection = std::map; + + 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 &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 -#include -#include - -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()) { - return data.value(); - } - 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(innerRect.height() - 3) / static_cast(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 +#include +#include + +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()) { + return data.value(); + } + 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(innerRect.height() - 3) / static_cast(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 -#include - - -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 +#include + + +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 -- cgit v1.3.1