diff options
71 files changed, 6285 insertions, 4451 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 98bbe05f..4b74137e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -132,16 +132,31 @@ add_filter(NAME src/modinfo/dialog GROUPS modinfodialogtab modinfodialogtextfiles ) + +add_filter(NAME src/modinfo/dialog/widgets GROUPS + modidlineedit +) + add_filter(NAME src/modlist GROUPS modlist + modlistdropinfo modlistsortproxy + modlistbypriorityproxy +) + +add_filter(NAME src/modlist/view GROUPS modlistview + modlistviewactions + modlistcontextmenu + modflagicondelegate + modconflicticondelegate ) add_filter(NAME src/plugins GROUPS pluginlist pluginlistsortproxy pluginlistview + pluginlistcontextmenu ) add_filter(NAME src/previews GROUPS @@ -216,13 +231,12 @@ add_filter(NAME src/widgets GROUPS lcdnumber loglist loghighlighter - modflagicondelegate - modconflicticondelegate - modidlineedit noeditdelegate qtgroupingproxy texteditor viewmarkingscrollbar + modelutils + copyeventfilter ) diff --git a/src/aboutdialog.cpp b/src/aboutdialog.cpp index 03663cf8..98743e05 100644 --- a/src/aboutdialog.cpp +++ b/src/aboutdialog.cpp @@ -120,5 +120,5 @@ void AboutDialog::on_creditsList_currentItemChanged(QListWidgetItem *current, QL void AboutDialog::on_sourceText_linkActivated(const QString &link)
{
- emit linkClicked(link);
+ MOBase::shell::Open(QUrl(link));
}
diff --git a/src/aboutdialog.h b/src/aboutdialog.h index 9b9b6102..02d840ec 100644 --- a/src/aboutdialog.h +++ b/src/aboutdialog.h @@ -40,10 +40,6 @@ public: explicit AboutDialog(const QString &version, QWidget *parent = 0);
~AboutDialog();
-signals:
-
- void linkClicked(QString link);
-
private:
enum Licenses {
diff --git a/src/copyeventfilter.cpp b/src/copyeventfilter.cpp new file mode 100644 index 00000000..223561b0 --- /dev/null +++ b/src/copyeventfilter.cpp @@ -0,0 +1,51 @@ +#include "copyeventfilter.h" + +#include <QClipboard> +#include <QGuiApplication> +#include <QKeyEvent> + +CopyEventFilter::CopyEventFilter(QAbstractItemView* view, int role) : + CopyEventFilter(view, [=](auto& index) { return index.data(role).toString(); }) +{ + +} + +CopyEventFilter::CopyEventFilter( + QAbstractItemView* view, std::function<QString(const QModelIndex&)> format) : + QObject(view), m_view(view), m_format(format) +{ + +} + +void CopyEventFilter::copySelection() const +{ + if (!m_view->selectionModel()->hasSelection()) { + return; + } + + // sort to reflect the visual order + QModelIndexList selectedRows = m_view->selectionModel()->selectedRows(); + std::sort(selectedRows.begin(), selectedRows.end(), [=](const auto& lidx, const auto& ridx) { + return m_view->visualRect(lidx).top() < m_view->visualRect(ridx).top(); + }); + + QStringList rows; + for (auto& idx : selectedRows) { + rows.append(m_format(idx)); + } + + QGuiApplication::clipboard()->setText(rows.join("\n")); +} + +bool CopyEventFilter::eventFilter(QObject* sender, QEvent* event) +{ + if (sender == m_view && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->modifiers() == Qt::ControlModifier + && keyEvent->key() == Qt::Key_C) { + copySelection(); + return true; + } + } + return QObject::eventFilter(sender, event); +} diff --git a/src/copyeventfilter.h b/src/copyeventfilter.h new file mode 100644 index 00000000..a9d0a1c1 --- /dev/null +++ b/src/copyeventfilter.h @@ -0,0 +1,43 @@ +#ifndef COPY_EVENT_FILTER_H +#define COPY_EVENT_FILTER_H + +#include <functional> + +#include <QAbstractItemView> +#include <QModelIndex> +#include <QObject> + +// this small class provides copy on Ctrl+C and also +// exposes a method to actual copy the selection +// +// the way the selection is copied can be customized by +// passing a functor to format each index, by default +// it only extracts the display role +// +// only works for view that selects whole row since it only +// considers the first cell in each row +// +class CopyEventFilter : public QObject +{ + Q_OBJECT + +public: + + CopyEventFilter(QAbstractItemView* view, int role = Qt::DisplayRole); + CopyEventFilter(QAbstractItemView* view, std::function<QString(const QModelIndex&)> format); + + // copy the selection of the view associated with this + // event filter into the clipboard + // + void copySelection() const; + + bool eventFilter(QObject* sender, QEvent* event) override; + +private: + + QAbstractItemView* m_view; + std::function<QString(const QModelIndex&)> m_format; + +}; + +#endif diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp index a5c284e6..93f8538c 100644 --- a/src/downloadlist.cpp +++ b/src/downloadlist.cpp @@ -19,6 +19,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "downloadlist.h"
#include "downloadmanager.h"
+#include "modlistdropinfo.h"
#include <utility.h>
#include <log.h>
#include <QEvent>
@@ -90,6 +91,18 @@ QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int }
}
+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::DOWNLOAD_TEXT);
+ return result;
+}
+
QVariant DownloadList::data(const QModelIndex &index, int role) const
{
bool pendingDownload = index.row() >= m_Manager->numTotalDownloads();
diff --git a/src/downloadlist.h b/src/downloadlist.h index 2171c013..65d03ab9 100644 --- a/src/downloadlist.h +++ b/src/downloadlist.h @@ -68,11 +68,12 @@ public: * @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;
diff --git a/src/filterlist.cpp b/src/filterlist.cpp index 2b72c152..4c1c6fff 100644 --- a/src/filterlist.cpp +++ b/src/filterlist.cpp @@ -12,7 +12,14 @@ using Criteria = ModListSortProxy::Criteria; class FilterList::CriteriaItem : public QTreeWidgetItem { + + static constexpr int IDRole = Qt::UserRole; + static constexpr int TypeRole = Qt::UserRole + 1; + public: + + static constexpr int StateRole = Qt::UserRole + 2; + enum States { FirstState = 0, @@ -58,27 +65,40 @@ public: void nextState() { - m_state = static_cast<States>(m_state + 1); - if (m_state > LastState) { - m_state = FirstState; + auto s = static_cast<States>(m_state + 1); + if (s > LastState) { + s = FirstState; } - - updateState(); + setState(s); } void previousState() { - m_state = static_cast<States>(m_state - 1); - if (m_state < FirstState) { - m_state = LastState; + auto s = static_cast<States>(m_state - 1); + if (s < FirstState) { + s = LastState; } + setState(s); + } - updateState(); + QVariant data(int column, int role) const + { + if (role == StateRole) { + return m_state; + } + return QTreeWidgetItem::data(column, role); + } + + void setData(int column, int role, const QVariant& value) { + if (role == StateRole) { + setState(static_cast<States>(value.toInt())); + } + else { + QTreeWidgetItem::setData(column, role, value); + } } private: - const int IDRole = Qt::UserRole; - const int TypeRole = Qt::UserRole + 1; FilterList* m_list; States m_state; @@ -181,8 +201,8 @@ private: }; -FilterList::FilterList(Ui::MainWindow* ui, OrganizerCore* organizer, CategoryFactory& factory) - : ui(ui), m_Organizer(organizer), m_factory(factory) +FilterList::FilterList(Ui::MainWindow* ui, OrganizerCore& core, CategoryFactory& factory) + : ui(ui), m_core(core), m_factory(factory) { auto* eventFilter = new CriteriaItemFilter( ui->filters, [&](auto* item, int dir){ return cycleItem(item, dir); }); @@ -212,10 +232,17 @@ FilterList::FilterList(Ui::MainWindow* ui, OrganizerCore* organizer, CategoryFac void FilterList::restoreState(const Settings& s) { s.widgets().restoreIndex(ui->filtersSeparators); + s.widgets().restoreChecked(ui->filtersAnd); + s.widgets().restoreChecked(ui->filtersOr); + s.widgets().restoreTreeCheckState(ui->filters, CriteriaItem::StateRole); + checkCriteria(); } void FilterList::saveState(Settings& s) const { + s.widgets().saveTreeCheckState(ui->filters, CriteriaItem::StateRole); + s.widgets().saveChecked(ui->filtersAnd); + s.widgets().saveChecked(ui->filtersOr); s.widgets().saveIndex(ui->filtersSeparators); } @@ -234,7 +261,7 @@ QTreeWidgetItem* FilterList::addCriteriaItem( void FilterList::addContentCriteria() { - m_Organizer->modDataContents().forEachContent([this](auto const& content) { + m_core.modDataContents().forEachContent([this](auto const& content) { addCriteriaItem( nullptr, QString("<%1>").arg(tr("Contains %1").arg(content.name())), content.id(), ModListSortProxy::TypeContent); diff --git a/src/filterlist.h b/src/filterlist.h index ba9dc71c..0788d224 100644 --- a/src/filterlist.h +++ b/src/filterlist.h @@ -14,7 +14,7 @@ class FilterList : public QObject Q_OBJECT; public: - FilterList(Ui::MainWindow* ui, OrganizerCore *organizer, CategoryFactory& factory); + FilterList(Ui::MainWindow* ui, OrganizerCore& organizer, CategoryFactory& factory); void restoreState(const Settings& s); void saveState(Settings& s) const; @@ -32,7 +32,7 @@ private: class CriteriaItem; Ui::MainWindow* ui; - OrganizerCore* m_Organizer; + OrganizerCore& m_core; CategoryFactory& m_factory; bool onClick(QMouseEvent* e); diff --git a/src/icondelegate.cpp b/src/icondelegate.cpp index 03964263..901b5731 100644 --- a/src/icondelegate.cpp +++ b/src/icondelegate.cpp @@ -27,7 +27,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase;
-IconDelegate::IconDelegate(QObject *parent)
+IconDelegate::IconDelegate(QObject* parent)
: QStyledItemDelegate(parent)
{
}
@@ -66,7 +66,12 @@ void IconDelegate::paintIcons( void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
- QStyledItemDelegate::paint(painter, option, index);
+ if (auto* w = qobject_cast<QAbstractItemView*>(parent())) {
+ w->itemDelegate()->paint(painter, option, index);
+ }
+ else {
+ QStyledItemDelegate::paint(painter, option, index);
+ }
paintIcons(painter, option, index, getIcons(index));
}
diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index 413f567a..ad7cdcd5 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -64,6 +64,12 @@ using namespace MOBase; using namespace MOShared; +InstallationResult::InstallationResult(IPluginInstaller::EInstallResult result) : + m_result(result), m_name(), m_iniTweaks(false), m_backup(false), m_merged(false), m_replaced(false) +{ +} + + template <typename T> static T resolveFunction(QLibrary &lib, const char *name) { @@ -353,8 +359,7 @@ IPluginInstaller::EInstallResult InstallationManager::installArchive(GuessedValu // in earlier versions the modName was copied here and the copy passed to install. I don't know why I did this and it causes // a problem if this is called by the bundle installer and the bundled installer adds additional names that then end up being used, // because the caller will then not have the right name. - bool iniTweaks; - return install(archiveName, modName, iniTweaks, modId); + return install(archiveName, modName, modId).result(); } @@ -374,10 +379,13 @@ QString InstallationManager::generateBackupName(const QString &directoryName) co } -IPluginInstaller::EInstallResult InstallationManager::testOverwrite(GuessedValue<QString> &modName, bool *merge) +InstallationResult InstallationManager::testOverwrite(GuessedValue<QString> &modName) { QString targetDirectory = QDir::fromNativeSeparators(m_ModsDirectory + "\\" + modName); + // this is only returned on success + InstallationResult result{ IPluginInstaller::RESULT_SUCCESS }; + while (QDir(targetDirectory).exists()) { Settings &settings(Settings::instance()); @@ -393,12 +401,14 @@ IPluginInstaller::EInstallResult InstallationManager::testOverwrite(GuessedValue QString backupDirectory = generateBackupName(targetDirectory); if (!copyDir(targetDirectory, backupDirectory, false)) { reportError(tr("Failed to create backup")); - return IPluginInstaller::RESULT_FAILED; + return { IPluginInstaller::RESULT_FAILED }; } } - if (merge != nullptr) { - *merge = (overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE); - } + + result.m_merged = overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE; + result.m_replaced = overwriteDialog.action() == QueryOverwriteDialog::ACT_REPLACE; + result.m_backup = overwriteDialog.backup(); + if (overwriteDialog.action() == QueryOverwriteDialog::ACT_RENAME) { bool ok = false; QString name = QInputDialog::getText(m_ParentWidget, tr("Mod Name"), tr("Name"), @@ -406,7 +416,7 @@ IPluginInstaller::EInstallResult InstallationManager::testOverwrite(GuessedValue if (ok && !name.isEmpty()) { modName.update(name, GUESS_USER); if (!ensureValidModName(modName)) { - return IPluginInstaller::RESULT_FAILED; + return { IPluginInstaller::RESULT_FAILED }; } targetDirectory = QDir::fromNativeSeparators(m_ModsDirectory) + "/" + modName; } @@ -441,20 +451,20 @@ IPluginInstaller::EInstallResult InstallationManager::testOverwrite(GuessedValue } else { log::error("failed to restore original settings: {}", metaFilename); } - return IPluginInstaller::RESULT_SUCCESS; + return result; } else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE) { - return IPluginInstaller::RESULT_SUCCESS; + return result; } else /* if (overwriteDialog.action() == QueryOverwriteDialog::ACT_NONE) */ { - return IPluginInstaller::RESULT_CANCELED; + return { IPluginInstaller::RESULT_CANCELED }; } } else { - return IPluginInstaller::RESULT_CANCELED; + return { IPluginInstaller::RESULT_CANCELED }; } } QDir().mkdir(targetDirectory); - return IPluginInstaller::RESULT_SUCCESS;; + return result; } @@ -473,27 +483,30 @@ bool InstallationManager::ensureValidModName(GuessedValue<QString> &name) const return true; } -IPluginInstaller::EInstallResult InstallationManager::doInstall(GuessedValue<QString> &modName, QString gameName, int modID, - const QString &version, const QString &newestVersion, - int categoryID, int fileCategoryID, const QString &repository) +InstallationResult InstallationManager::doInstall( + GuessedValue<QString> &modName, QString gameName, int modID, + const QString &version, const QString &newestVersion, + int categoryID, int fileCategoryID, const QString &repository) { if (!ensureValidModName(modName)) { - return IPluginInstaller::RESULT_FAILED; + return { IPluginInstaller::RESULT_FAILED }; } bool merge = false; // determine target directory - IPluginInstaller::EInstallResult result = testOverwrite(modName, &merge); - if (result != IPluginInstaller::RESULT_SUCCESS) { + InstallationResult result = testOverwrite(modName); + if (!result) { return result; } + result.m_name = modName; + QString targetDirectory = QDir(m_ModsDirectory + "/" + modName).canonicalPath(); QString targetDirectoryNative = QDir::toNativeSeparators(targetDirectory); log::debug("installing to \"{}\"", targetDirectoryNative); if (!extractFiles(targetDirectory, "", true, false)) { - return IPluginInstaller::RESULT_CANCELED; + return { IPluginInstaller::RESULT_CANCELED }; } // Copy the created files: @@ -547,7 +560,7 @@ IPluginInstaller::EInstallResult InstallationManager::doInstall(GuessedValue<QSt settingsFile.endGroup(); } - return IPluginInstaller::RESULT_SUCCESS; + return result; } @@ -601,10 +614,8 @@ void InstallationManager::postInstallCleanup() } } -IPluginInstaller::EInstallResult InstallationManager::install(const QString &fileName, - GuessedValue<QString> &modName, - bool &hasIniTweaks, - int modID) +InstallationResult InstallationManager::install( + const QString &fileName, GuessedValue<QString> &modName, int modID) { m_IsRunning = true; ON_BLOCK_EXIT([this]() { m_IsRunning = false; }); @@ -612,7 +623,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil QFileInfo fileInfo(fileName); if (!getSupportedExtensions().contains(fileInfo.suffix(), Qt::CaseInsensitive)) { reportError(tr("File format \"%1\" not supported").arg(fileInfo.suffix())); - return IPluginInstaller::RESULT_FAILED; + return InstallationResult(IPluginInstaller::RESULT_FAILED); } modName.setFilter(&fixDirectoryName); @@ -703,7 +714,6 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil std::shared_ptr<IFileTree> filesTree = archiveOpen ? ArchiveFileTree::makeTree(*m_ArchiveHandler) : nullptr; - IPluginInstaller::EInstallResult installResult = IPluginInstaller::RESULT_NOTATTEMPTED; auto installers = m_PluginContainer->plugins<IPluginInstaller>(); @@ -711,6 +721,8 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil return lhs->priority() > rhs->priority(); }); + InstallationResult installResult(IPluginInstaller::RESULT_NOTATTEMPTED); + for (IPluginInstaller *installer : installers) { // don't use inactive installers (installer can't be null here but vc static code analysis thinks it could) if ((installer == nullptr) || !m_PluginContainer->isEnabled(installer)) { @@ -718,11 +730,11 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil } // try only manual installers if that was requested - if (installResult == IPluginInstaller::RESULT_MANUALREQUESTED) { + if (installResult.result() == IPluginInstaller::RESULT_MANUALREQUESTED) { if (!installer->isManualInstaller()) { continue; } - } else if (installResult != IPluginInstaller::RESULT_NOTATTEMPTED) { + } else if (installResult.result() != IPluginInstaller::RESULT_NOTATTEMPTED) { break; } @@ -732,9 +744,9 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil = dynamic_cast<IPluginInstallerSimple *>(installer); if ((installerSimple != nullptr) && (filesTree != nullptr) && (installer->isArchiveSupported(filesTree))) { - installResult + installResult.m_result = installerSimple->install(modName, filesTree, version, modID); - if (installResult == IPluginInstaller::RESULT_SUCCESS) { + if (installResult) { // Downcast to an actual ArchiveFileTree and map to the archive. Test if // the tree is still an ArchiveFileTree, otherwize it means the installer @@ -755,12 +767,13 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil // the simple installer only prepares the installation, the rest // works the same for all installers - installResult = doInstall(modName, gameName, modID, version, newestVersion, categoryID, fileCategoryID, repository); + installResult = doInstall(modName, gameName, modID, version, + newestVersion, categoryID, fileCategoryID, repository); } } } - if (installResult != IPluginInstaller::RESULT_CANCELED) { // custom case + if (installResult.result() != IPluginInstaller::RESULT_CANCELED) { // custom case IPluginInstallerCustom *installerCustom = dynamic_cast<IPluginInstallerCustom *>(installer); if ((installerCustom != nullptr) @@ -771,7 +784,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil std::set<QString> installerExt = installerCustom->supportedExtensions(); if (installerExt.find(fileInfo.suffix()) != installerExt.end()) { - installResult + installResult.m_result = installerCustom->install(modName, gameName, fileName, version, modID); unsigned int idx = ModInfo::getIndex(modName); if (idx != UINT_MAX) { @@ -787,7 +800,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil // act upon the installation result. at this point the files have already been // extracted to the correct location - switch (installResult) { + switch (installResult.result()) { case IPluginInstaller::RESULT_FAILED: { QMessageBox::information(qApp->activeWindow(), tr("Installation failed"), tr("Something went wrong while installing this mod."), @@ -798,10 +811,11 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil case IPluginInstaller::RESULT_SUCCESSCANCEL: { if (filesTree != nullptr) { auto iniTweakEntry = filesTree->find("INI Tweaks", FileTreeEntry::DIRECTORY); - hasIniTweaks = iniTweakEntry != nullptr + installResult.m_iniTweaks = iniTweakEntry != nullptr && !iniTweakEntry->astree()->empty(); } - return IPluginInstaller::RESULT_SUCCESS; + installResult.m_result = IPluginInstaller::RESULT_SUCCESS; + return installResult; } break; case IPluginInstaller::RESULT_NOTATTEMPTED: case IPluginInstaller::RESULT_MANUALREQUESTED: { @@ -811,7 +825,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil return installResult; } } - if (installResult == IPluginInstaller::RESULT_NOTATTEMPTED) { + if (installResult.result() == IPluginInstaller::RESULT_NOTATTEMPTED) { reportError(tr("None of the available installer plugins were able to handle that archive.\n" "This is likely due to a corrupted or incompatible download or unrecognized archive format.")); } @@ -877,14 +891,12 @@ void InstallationManager::notifyInstallationStart(QString const& archive, bool r } } -void InstallationManager::notifyInstallationEnd( - MOBase::IPluginInstaller::EInstallResult result, - ModInfo::Ptr newMod) +void InstallationManager::notifyInstallationEnd(const InstallationResult& result, ModInfo::Ptr newMod) { auto& installers = m_PluginContainer->plugins<IPluginInstaller>(); for (auto* installer : installers) { if (m_PluginContainer->isEnabled(installer)) { - installer->onInstallationEnd(result, newMod.get()); + installer->onInstallationEnd(result.result(), newMod.get()); } } } diff --git a/src/installationmanager.h b/src/installationmanager.h index 277c276a..a5ec11d9 100644 --- a/src/installationmanager.h +++ b/src/installationmanager.h @@ -37,15 +37,48 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "modinfo.h" #include "plugincontainer.h" +// contains installation result from the manager, internal class +// for MO2 that is not forwarded to plugin +class InstallationResult { +public: + + // result status of the installation + // + auto result() const { return m_result; } + + // information about the installation, only valid for successful + // installation + // + QString name() const { return m_name; } + bool backupCreated() const { return m_backup; } + bool merged() const { return m_merged; } + bool replaced() const { return m_replaced; } + bool hasIniTweaks() const { return m_iniTweaks; } + bool mergedOrReplaced() const { return merged() || replaced(); } + + // check if the installation was a success + // + explicit operator bool() const { return result() == MOBase::IPluginInstaller::EInstallResult::RESULT_SUCCESS; } + +private: + + friend class InstallationManager; + + // create a failed result + InstallationResult(MOBase::IPluginInstaller::EInstallResult result = MOBase::IPluginInstaller::EInstallResult::RESULT_FAILED); + + MOBase::IPluginInstaller::EInstallResult m_result; + + QString m_name; + + bool m_iniTweaks; + bool m_backup; + bool m_merged; + bool m_replaced; + +}; + -/** - * @brief manages the installation of mod archives - * This currently supports two special kind of archives: - * - "simple" archives: properly packaged archives without options, so they can be extracted to the (virtual) data directory directly - * - "complex" bain archives: archives with options for the bain system. - * All other archives are managed through the manual "InstallDialog" - * @todo this may be a good place to support plugins - **/ class InstallationManager : public QObject, public MOBase::IInstallationManager { Q_OBJECT @@ -79,7 +112,7 @@ public: * @param result The result of the installation process. * @param currentMod The newly install mod, if result is SUCCESS, a null pointer otherwise. */ - void notifyInstallationEnd(MOBase::IPluginInstaller::EInstallResult result, ModInfo::Ptr newMod); + void notifyInstallationEnd(const InstallationResult& result, ModInfo::Ptr newMod); /** * @brief update the directory where mods are to be installed @@ -107,7 +140,7 @@ public: * @return true if the archive was installed, false if installation failed or was refused * @exception std::exception an exception may be thrown if the archive can't be opened (maybe the format is invalid or the file is damaged) **/ - MOBase::IPluginInstaller::EInstallResult install(const QString &fileName, MOBase::GuessedValue<QString> &modName, bool &hasIniTweaks, int modID = 0); + InstallationResult install(const QString &fileName, MOBase::GuessedValue<QString> &modName, int modID = 0); /** * @return true if the installation was canceled @@ -148,7 +181,7 @@ public: * @note The temporary file is automatically cleaned up after the installation. * @note This call can be very slow if the archive is large and "solid". */ - virtual QString extractFile(std::shared_ptr<const MOBase::FileTreeEntry> entry, bool silent = false) override; + QString extractFile(std::shared_ptr<const MOBase::FileTreeEntry> entry, bool silent = false) override; /** * @brief Extract the specified files from the currently opened archive to a temporary location. @@ -171,7 +204,7 @@ public: * IFileTree and thus to given a list of entries flattened (this was not possible with the * QStringList version since these were based on the name of the file inside the archive). */ - virtual QStringList extractFiles(std::vector<std::shared_ptr<const MOBase::FileTreeEntry>> const& entries, bool silent = false) override; + QStringList extractFiles(std::vector<std::shared_ptr<const MOBase::FileTreeEntry>> const& entries, bool silent = false) override; /** * @brief Create a new file on the disk corresponding to the given entry. @@ -184,7 +217,7 @@ public: * * @return the path to the created file. */ - virtual QString createFile(std::shared_ptr<const MOBase::FileTreeEntry> entry) override; + QString createFile(std::shared_ptr<const MOBase::FileTreeEntry> entry) override; /** * @brief Installs the given archive. @@ -195,22 +228,26 @@ public: * * @return the installation result. */ - virtual MOBase::IPluginInstaller::EInstallResult installArchive(MOBase::GuessedValue<QString> &modName, const QString &archiveName, int modId = 0) override; + MOBase::IPluginInstaller::EInstallResult installArchive(MOBase::GuessedValue<QString> &modName, const QString &archiveName, int modId = 0) override; /** - * @brief test if the specified mod name is free. If not, query the user how to proceed * @param modName current possible names for the mod - * @param merge if this value is not null, the value will be set to whether the use chose to merge or replace - * @return true if we can proceed with the installation, false if the user canceled or in case of an unrecoverable error + * + * @return an installation result containing information from the user. */ - virtual MOBase::IPluginInstaller::EInstallResult testOverwrite(MOBase::GuessedValue<QString> &modName, bool *merge = nullptr); + InstallationResult testOverwrite(MOBase::GuessedValue<QString>& modName); QString generateBackupName(const QString &directoryName) const; private: - MOBase::IPluginInstaller::EInstallResult doInstall(MOBase::GuessedValue<QString> &modName, QString gameName, - int modID, const QString &version, const QString &newestVersion, int categoryID, int fileCategoryID, const QString &repository); + // actually perform the installation (write files to the disk, etc.), returns the + // installation result + // + InstallationResult doInstall( + MOBase::GuessedValue<QString> &modName, QString gameName, + int modID, const QString &version, const QString &newestVersion, + int categoryID, int fileCategoryID, const QString &repository); /** * @brief Clean the list of created files by removing all entries that are not diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3d7526f6..f761a5f0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -30,7 +30,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "isavegameinfowidget.h" #include "nexusinterface.h" #include "organizercore.h" -#include "pluginlistsortproxy.h" #include "previewgenerator.h" #include "serverinfo.h" #include "savegameinfo.h" @@ -39,15 +38,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "instancemanager.h" #include "report.h" #include "modlist.h" -#include "modlistsortproxy.h" -#include "qtgroupingproxy.h" #include "profile.h" #include "pluginlist.h" #include "profilesdialog.h" #include "editexecutablesdialog.h" #include "categories.h" #include "categoriesdialog.h" -#include "modinfodialog.h" #include "overwriteinfodialog.h" #include "downloadlist.h" #include "downloadlistwidget.h" @@ -56,12 +52,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "motddialog.h" #include "filedialogmemory.h" #include "tutorialmanager.h" -#include "modflagicondelegate.h" -#include "modconflicticondelegate.h" -#include "genericicondelegate.h" #include "selectiondialog.h" -#include "csvbuilder.h" -#include "savetextasdialog.h" #include "problemsdialog.h" #include "previewdialog.h" #include "browserdialog.h" @@ -87,6 +78,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "listdialog.h" #include "envshortcut.h" #include "browserdialog.h" +#include "modlistviewactions.h" +#include "modlistcontextmenu.h" #include "directoryrefresher.h" #include "shared/directoryentry.h" @@ -94,7 +87,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "shared/filesorigin.h" #include <QAbstractItemDelegate> -#include <QAbstractProxyModel> #include <QAction> #include <QApplication> #include <QButtonGroup> @@ -207,7 +199,6 @@ QString UnmanagedModName() bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); - void setFilterShortcuts(QWidget* widget, QLineEdit* edit) { auto activate = [=] { @@ -242,7 +233,6 @@ void setFilterShortcuts(QWidget* widget, QLineEdit* edit) hookReset(edit); } - MainWindow::MainWindow(Settings &settings , OrganizerCore &organizerCore , PluginContainer &pluginContainer @@ -254,15 +244,10 @@ MainWindow::MainWindow(Settings &settings , m_linksSeparator(nullptr) , m_Tutorial(this, "MainWindow") , m_OldProfileIndex(-1) - , m_ModListSortProxy(nullptr) , m_OldExecutableIndex(-1) , m_CategoryFactory(CategoryFactory::instance()) - , m_ContextItem(nullptr) - , m_ContextAction(nullptr) - , m_ContextRow(-1) , m_OrganizerCore(organizerCore) , m_PluginContainer(pluginContainer) - , m_DidUpdateMasterList(false) , m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this)) , m_LinkToolbar(nullptr) , m_LinkDesktop(nullptr) @@ -318,16 +303,6 @@ MainWindow::MainWindow(Settings &settings ui->statusBar->setAPI(ni.getAPIStats(), ni.getAPIUserAccount()); } - m_Filters.reset(new FilterList(ui, &m_OrganizerCore, m_CategoryFactory)); - - connect( - m_Filters.get(), &FilterList::criteriaChanged, - [&](auto&& v) { onFiltersCriteria(v); }); - - connect( - m_Filters.get(), &FilterList::optionsChanged, - [&](auto&& mode, auto&& sep) { onFiltersOptions(mode, sep); }); - ui->logList->setCore(m_OrganizerCore); @@ -338,14 +313,7 @@ MainWindow::MainWindow(Settings &settings TaskProgressManager::instance().tryCreateTaskbar(); setupModList(); - - // set up plugin list - m_PluginListSortProxy = m_OrganizerCore.createPluginListProxyModel(); - - ui->espList->setModel(m_PluginListSortProxy); - ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder); - ui->espList->setItemDelegateForColumn(PluginList::COL_FLAGS, new GenericIconDelegate(ui->espList)); - ui->espList->installEventFilter(m_OrganizerCore.pluginList()); + ui->espList->setup(m_OrganizerCore, this, ui); ui->bsaList->setLocalMoveOnly(true); ui->bsaList->setHeaderHidden(true); @@ -397,10 +365,7 @@ MainWindow::MainWindow(Settings &settings m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); ui->linkButton->setMenu(linkMenu); - QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); - initModListContextMenu(listOptionsMenu); - ui->listOptionsBtn->setMenu(listOptionsMenu); - connect(ui->listOptionsBtn, SIGNAL(pressed()), this, SLOT(on_listOptionsBtn_pressed())); + ui->listOptionsBtn->setMenu(new ModListGlobalContextMenu(m_OrganizerCore, ui->modList, this)); ui->openFolderMenu->setMenu(openFolderMenu()); @@ -421,13 +386,6 @@ MainWindow::MainWindow(Settings &settings connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, SLOT(scheduleCheckForProblems())); - connect(m_ModListSortProxy, SIGNAL(filterActive(bool)), this, SLOT(modFilterActive(bool))); - connect(m_ModListSortProxy, SIGNAL(layoutChanged()), this, SLOT(updateModCount())); - connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString))); - - connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), m_PluginListSortProxy, SLOT(updateFilter(QString))); - connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString))); - connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed())); connect( m_OrganizerCore.directoryRefresher(), @@ -483,11 +441,6 @@ MainWindow::MainWindow(Settings &settings m_CheckBSATimer.setSingleShot(true); connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList())); - connect(ui->espList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(esplistSelectionsChanged(QItemSelection))); - - new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated())); - new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated())); - setFilterShortcuts(ui->modList, ui->modFilterEdit); setFilterShortcuts(ui->espList, ui->espFilterEdit); setFilterShortcuts(ui->downloadView, ui->downloadFilterEdit); @@ -510,6 +463,18 @@ MainWindow::MainWindow(Settings &settings m_Tutorial.expose("espList", m_OrganizerCore.pluginList()); m_OrganizerCore.setUserInterface(this); + connect(m_OrganizerCore.modList(), &ModList::showMessage, + [=](auto&& message) { showMessage(message); }); + connect(m_OrganizerCore.modList(), &ModList::modRenamed, + [=](auto&& oldName, auto&& newName) { modRenamed(oldName, newName); }); + connect(m_OrganizerCore.modList(), &ModList::modUninstalled, + [=](auto&& name) { modRemoved(name); }); + connect(m_OrganizerCore.modList(), &ModList::fileMoved, + [=](auto&& ...args) { fileMoved(args...); }); + connect(m_OrganizerCore.installationManager(), &InstallationManager::modReplaced, + [=](auto&& name) { modRemoved(name); }); + connect(m_OrganizerCore.downloadManager(), &DownloadManager::showMessage, + [=](auto&& message) { showMessage(message); }); for (const QString &fileName : m_PluginContainer.pluginFileNames()) { installTranslator(QFileInfo(fileName).baseName()); } @@ -538,94 +503,20 @@ MainWindow::MainWindow(Settings &settings refreshExecutablesList(); updatePinnedExecutables(); resetActionIcons(); - updatePluginCount(); - updateModCount(); processUpdates(); + ui->modList->updateModCount(); + ui->espList->updatePluginCount(); ui->statusBar->updateNormalMessage(m_OrganizerCore); } void MainWindow::setupModList() { - m_ModListSortProxy = m_OrganizerCore.createModListProxyModel(); - ui->modList->setModel(m_ModListSortProxy); - ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder); - - - connect( - ui->modList, SIGNAL(dropModeUpdate(bool)), - m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool))); - - connect( - ui->modList->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), - this, SLOT(modListSortIndicatorChanged(int,Qt::SortOrder))); - - connect( - ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - this, SLOT(modlistSelectionsChanged(QItemSelection))); - - connect( - ui->modList->header(), SIGNAL(sectionResized(int, int, int)), - this, SLOT(modListSectionResized(int, int, int))); - - - GenericIconDelegate *contentDelegate = new GenericIconDelegate( - ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150); - - connect( - ui->modList->header(), SIGNAL(sectionResized(int,int,int)), - contentDelegate, SLOT(columnResized(int,int,int))); - - - ModFlagIconDelegate *flagDelegate = new ModFlagIconDelegate( - ui->modList, ModList::COL_FLAGS, 120); - - connect( - ui->modList->header(), SIGNAL(sectionResized(int,int,int)), - flagDelegate, SLOT(columnResized(int,int,int))); - - - ModConflictIconDelegate* conflictFlagDelegate = new ModConflictIconDelegate( - ui->modList, ModList::COL_CONFLICTFLAGS, 80); - - connect( - ui->modList->header(), SIGNAL(sectionResized(int, int, int)), - conflictFlagDelegate, SLOT(columnResized(int, int, int))); - - - ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate); - ui->modList->setItemDelegateForColumn(ModList::COL_CONFLICTFLAGS, conflictFlagDelegate); - ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate); - ui->modList->header()->installEventFilter(m_OrganizerCore.modList()); - + ui->modList->setup(m_OrganizerCore, m_CategoryFactory, this, ui); - if (m_OrganizerCore.settings().geometry().restoreState(ui->modList->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 = ui->modList->header()->sectionSize(column); - ui->modList->header()->resizeSection(column, sectionSize + 1); - ui->modList->header()->resizeSection(column, sectionSize); - } - } else { - // hide these columns by default - ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true); - ui->modList->header()->setSectionHidden(ModList::COL_MODID, true); - ui->modList->header()->setSectionHidden(ModList::COL_GAME, true); - ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true); - ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true); - - // resize mod list to fit content - for (int i = 0; i < ui->modList->header()->count(); ++i) { - ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); - } - - ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); - } - - // prevent the name-column from being hidden - ui->modList->header()->setSectionHidden(ModList::COL_NAME, false); - - ui->modList->installEventFilter(m_OrganizerCore.modList()); + connect(&ui->modList->actions(), &ModListViewActions::overwriteCleared, [=]() { scheduleCheckForProblems(); }); + connect(&ui->modList->actions(), &ModListViewActions::originModified, this, &MainWindow::originModified); + connect(m_OrganizerCore.modList(), &ModList::modPrioritiesChanged, [&]() { m_ArchiveListWriter.write(); }); } void MainWindow::resetActionIcons() @@ -695,7 +586,6 @@ void MainWindow::resetActionIcons() updateProblemsButton(); } - MainWindow::~MainWindow() { try { @@ -731,13 +621,11 @@ void MainWindow::updateWindowTitle(const APIUserAccount& user) this->setWindowTitle(title); } - void MainWindow::onRequestsChanged(const APIStats& stats, const APIUserAccount& user) { ui->statusBar->setAPI(stats, user); } - void MainWindow::resizeLists(bool pluginListCustom) { // ensure the columns aren't so small you can't see them any more @@ -756,7 +644,6 @@ void MainWindow::resizeLists(bool pluginListCustom) } } - void MainWindow::allowListResize() { // allow resize on mod list @@ -783,25 +670,6 @@ void MainWindow::resizeEvent(QResizeEvent *event) QMainWindow::resizeEvent(event); } - -static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex idx) -{ - QModelIndex result = idx; - const QAbstractItemModel *model = idx.model(); - while (model != targetModel) { - if (model == nullptr) { - return QModelIndex(); - } - const QAbstractProxyModel *proxyModel = qobject_cast<const QAbstractProxyModel*>(model); - if (proxyModel == nullptr) { - return QModelIndex(); - } - result = proxyModel->mapToSource(result); - model = proxyModel->sourceModel(); - } - return result; -} - void MainWindow::setupToolbar() { setupActionMenu(ui->actionModPage); @@ -1076,7 +944,6 @@ void MainWindow::updateProblemsButton() } } - bool MainWindow::errorReported(QString &logFile) { QDir dir(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath())); @@ -1138,12 +1005,9 @@ void MainWindow::checkForProblemsImpl() void MainWindow::about() { - AboutDialog dialog(m_OrganizerCore.getVersion().displayString(3), this); - connect(&dialog, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); - dialog.exec(); + AboutDialog(m_OrganizerCore.getVersion().displayString(3), this).exec(); } - void MainWindow::createEndorseMenu() { auto* menu = ui->actionEndorseMO->menu(); @@ -1163,7 +1027,6 @@ void MainWindow::createEndorseMenu() menu->addAction(wontEndorseAction); } - void MainWindow::createHelpMenu() { auto* menu = ui->actionHelp->menu(); @@ -1234,48 +1097,6 @@ void MainWindow::createHelpMenu() menu->addAction(tr("About Qt"), qApp, SLOT(aboutQt())); } -void MainWindow::modFilterActive(bool filterActive) -{ - ui->clearFiltersButton->setVisible(filterActive); - if (filterActive) { -// m_OrganizerCore.modList()->setOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>()); - ui->modList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); - ui->activeModsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); - } else if (ui->groupCombo->currentIndex() != 0) { - ui->modList->setStyleSheet("QTreeView { border: 2px ridge #337733; }"); - ui->activeModsCounter->setStyleSheet(""); - } else { - ui->modList->setStyleSheet(""); - ui->activeModsCounter->setStyleSheet(""); - } -} - -void MainWindow::espFilterChanged(const QString &filter) -{ - if (!filter.isEmpty()) { - ui->espList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); - ui->activePluginsCounter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); - } else { - ui->espList->setStyleSheet(""); - ui->activePluginsCounter->setStyleSheet(""); - } - updatePluginCount(); -} - -void MainWindow::expandModList(const QModelIndex &index) -{ - QAbstractItemModel *model = ui->modList->model(); - - for (int i = 0; i < model->rowCount(); ++i) { - QModelIndex targetIdx = model->index(i, 0); - if (model->data(targetIdx).toString() == index.data().toString()) { - ui->modList->expand(targetIdx); - break; - } - } -} - - bool MainWindow::addProfile() { QComboBox *profileBox = findChild<QComboBox*>("profileBox"); @@ -1348,8 +1169,8 @@ void MainWindow::showEvent(QShowEvent *event) QMainWindow::showEvent(event); if (!m_WasVisible) { + ui->modList->refreshFilters(); readSettings(); - refreshFilters(); // this needs to be connected here instead of in the constructor because the // actual changing of the stylesheet is done by MOApplication, which @@ -1674,13 +1495,12 @@ void MainWindow::startExeAction() void MainWindow::activateSelectedProfile() { m_OrganizerCore.setCurrentProfile(ui->profileBox->currentText()); - - m_ModListSortProxy->setProfile(m_OrganizerCore.currentProfile()); + ui->modList->setProfile(m_OrganizerCore.currentProfile()); m_SavesTab->refreshSaveList(); m_OrganizerCore.refresh(); - updateModCount(); - updatePluginCount(); + ui->modList->updateModCount(); + ui->espList->updatePluginCount(); ui->statusBar->updateNormalMessage(m_OrganizerCore); } @@ -1813,7 +1633,6 @@ bool MainWindow::refreshProfiles(bool selectProfile) return profileBox->count() > 1; } - void MainWindow::refreshExecutablesList() { QAbstractItemModel *model = ui->executablesListBox->model(); @@ -2047,13 +1866,11 @@ void MainWindow::fixCategories() } } - void MainWindow::setupNetworkProxy(bool activate) { QNetworkProxyFactory::setUseSystemConfiguration(activate); } - void MainWindow::activateProxy(bool activate) { QProgressDialog busyDialog(tr("Activating Network Proxy"), QString(), 0, 0, parentWidget()); @@ -2105,10 +1922,9 @@ void MainWindow::readSettings() ui->executablesListBox->setCurrentIndex(*v); } - s.widgets().restoreIndex(ui->groupCombo); s.widgets().restoreIndex(ui->tabWidget); - m_Filters->restoreState(s); + ui->modList->restoreState(s); { s.geometry().restoreVisibility(ui->categoriesGroup, false); @@ -2179,14 +1995,12 @@ void MainWindow::storeSettings() s.geometry().saveState(ui->espList->header()); s.geometry().saveState(ui->downloadView->header()); - s.geometry().saveState(ui->modList->header()); - s.widgets().saveIndex(ui->groupCombo); s.widgets().saveIndex(ui->executablesListBox); s.widgets().saveIndex(ui->tabWidget); - m_Filters->saveState(s); m_DataTab->saveState(s); + ui->modList->saveState(s); s.interface().setFilterOptions(FilterWidget::options()); } @@ -2210,30 +2024,6 @@ void MainWindow::on_tabWidget_currentChanged(int index) } } - -void MainWindow::installMod(QString fileName) -{ - try { - if (fileName.isEmpty()) { - QStringList extensions = m_OrganizerCore.installationManager()->getSupportedExtensions(); - for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) { - *iter = "*." + *iter; - } - - fileName = FileDialogMemory::getOpenFileName("installMod", this, tr("Choose Mod"), QString(), - tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" ")))); - } - - if (fileName.isEmpty()) { - return; - } else { - m_OrganizerCore.installMod(fileName, false, nullptr, QString()); - } - } catch (const std::exception &e) { - reportError(e.what()); - } -} - void MainWindow::on_startButton_clicked() { const Executable* selectedExecutable = getSelectedExecutable(); @@ -2323,10 +2113,9 @@ void MainWindow::tutorialTriggered() } } - void MainWindow::on_actionInstallMod_triggered() { - installMod(); + ui->modList->actions().installMod(); } void MainWindow::on_action_Refresh_triggered() @@ -2381,33 +2170,6 @@ void MainWindow::on_actionModify_Executables_triggered() } } - -void MainWindow::setModListSorting(int index) -{ - Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder; - int column = index >> 1; - ui->modList->header()->setSortIndicator(column, order); -} - - -void MainWindow::setESPListSorting(int index) -{ - switch (index) { - case 0: { - ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder); - } break; - case 1: { - ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder); - } break; - case 2: { - ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder); - } break; - case 3: { - ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder); - } break; - } -} - void MainWindow::refresherProgress(const DirectoryRefreshProgress* p) { if (p->finished()) { @@ -2430,74 +2192,16 @@ void MainWindow::directory_refreshed() } } -void MainWindow::esplist_changed() -{ - updatePluginCount(); -} - -void MainWindow::modorder_changed() -{ - for (unsigned int i = 0; i < m_OrganizerCore.currentProfile()->numMods(); ++i) { - int priority = m_OrganizerCore.currentProfile()->getModPriority(i); - if (m_OrganizerCore.currentProfile()->modEnabled(i)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - // priorities in the directory structure are one higher because data is 0 - m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->internalName())).setPriority(priority + 1); - } - } - m_OrganizerCore.refreshBSAList(); - m_OrganizerCore.currentProfile()->writeModlist(); - m_ArchiveListWriter.write(); - m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins(); - - { // refresh selection - QModelIndex current = ui->modList->currentIndex(); - if (current.isValid()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(current.data(Qt::UserRole + 1).toInt()); - // clear caches on all mods conflicting with the moved mod - for (int i : modInfo->getModOverwrite()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModOverwritten()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveOverwrite()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveOverwritten()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveLooseOverwrite()) { - ModInfo::getByIndex(i)->clearCaches(); - } - for (int i : modInfo->getModArchiveLooseOverwritten()) { - ModInfo::getByIndex(i)->clearCaches(); - } - // update conflict check on the moved mod - modInfo->doConflictCheck(); - m_OrganizerCore.modList()->setOverwriteMarkers(modInfo->getModOverwrite(), modInfo->getModOverwritten()); - m_OrganizerCore.modList()->setArchiveOverwriteMarkers(modInfo->getModArchiveOverwrite(), modInfo->getModArchiveOverwritten()); - m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(modInfo->getModArchiveLooseOverwrite(), modInfo->getModArchiveLooseOverwritten()); - if (m_ModListSortProxy != nullptr) - m_ModListSortProxy->invalidate(); - ui->modList->verticalScrollBar()->repaint(); - } - } -} - void MainWindow::modInstalled(const QString &modName) { - QModelIndexList posList = - m_OrganizerCore.modList()->match(m_OrganizerCore.modList()->index(0, 0), Qt::DisplayRole, modName); - if (posList.count() == 1) { - ui->modList->scrollTo(posList.at(0)); + unsigned int index = ModInfo::getIndex(modName); + + if (index == UINT_MAX) { + return; } // force an update to happen - std::multimap<QString, int> IDs; - ModInfo::Ptr info = ModInfo::getByIndex(ModInfo::getIndex(modName)); - IDs.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId())); - modUpdateCheck(IDs); + ui->modList->actions().checkModsForUpdates({ m_OrganizerCore.modList()->index(index, 0) }); } void MainWindow::showMessage(const QString &message) @@ -2510,11 +2214,6 @@ void MainWindow::showError(const QString &message) reportError(message); } -void MainWindow::installMod_clicked() -{ - installMod(); -} - void MainWindow::modRenamed(const QString &oldName, const QString &newName) { Profile::renameModInAllProfiles(oldName, newName); @@ -2562,144 +2261,6 @@ void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName } } - -void MainWindow::renameMod_clicked() -{ - try { - ui->modList->edit(ui->modList->currentIndex()); - } catch (const std::exception &e) { - reportError(tr("failed to rename mod: %1").arg(e.what())); - } -} - - -void MainWindow::restoreBackup_clicked() -{ - QRegExp backupRegEx("(.*)_backup[0-9]*$"); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - if (backupRegEx.indexIn(modInfo->name()) != -1) { - QString regName = backupRegEx.cap(1); - QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().paths().mods())); - if (!modDir.exists(regName) || - (QMessageBox::question(this, tr("Overwrite?"), - tr("This will replace the existing mod \"%1\". Continue?").arg(regName), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) { - reportError(tr("failed to remove mod \"%1\"").arg(regName)); - } else { - QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().paths().mods()) + "/" + regName; - if (!modDir.rename(modInfo->absolutePath(), destinationPath)) { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath)); - } - - m_OrganizerCore.refresh(); - updateModCount(); - } - } - } -} - -void MainWindow::modlistChanged(const QModelIndex&, int) -{ - m_OrganizerCore.currentProfile()->writeModlist(); - updateModCount(); -} - -void MainWindow::modlistChanged(const QModelIndexList&, int) -{ - m_OrganizerCore.currentProfile()->writeModlist(); - updateModCount(); -} - -void MainWindow::modlistSelectionsChanged(const QItemSelection &selected) -{ - if (selected.count()) { - auto selection = selected.last(); - auto index = selection.indexes().last(); - ModInfo::Ptr selectedMod = ModInfo::getByIndex(index.data(Qt::UserRole + 1).toInt()); - m_OrganizerCore.modList()->setOverwriteMarkers(selectedMod->getModOverwrite(), selectedMod->getModOverwritten()); - m_OrganizerCore.modList()->setArchiveOverwriteMarkers(selectedMod->getModArchiveOverwrite(), selectedMod->getModArchiveOverwritten()); - m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(selectedMod->getModArchiveLooseOverwrite(), selectedMod->getModArchiveLooseOverwritten()); - } else { - m_OrganizerCore.modList()->setOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>()); - m_OrganizerCore.modList()->setArchiveOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>()); - m_OrganizerCore.modList()->setArchiveLooseOverwriteMarkers(std::set<unsigned int>(), std::set<unsigned int>()); - } - ui->modList->verticalScrollBar()->repaint(); - - m_OrganizerCore.pluginList()->highlightPlugins(ui->modList->selectionModel(), *m_OrganizerCore.directoryStructure(), *m_OrganizerCore.currentProfile()); - ui->espList->verticalScrollBar()->repaint(); -} - -void MainWindow::esplistSelectionsChanged(const QItemSelection &selected) -{ - m_OrganizerCore.modList()->highlightMods(ui->espList->selectionModel(), *m_OrganizerCore.directoryStructure()); - ui->modList->verticalScrollBar()->repaint(); -} - -void MainWindow::modListSortIndicatorChanged(int, Qt::SortOrder) -{ - ui->modList->verticalScrollBar()->repaint(); -} - -void MainWindow::modListSectionResized(int logicalIndex, int oldSize, int newSize) -{ - bool enabled = (newSize != 0); - qobject_cast<ModListSortProxy *>(ui->modList->model())->setColumnVisible(logicalIndex, enabled); -} - -void MainWindow::removeMod_clicked() -{ - const int max_items = 20; - - try { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - QString mods; - QStringList modNames; - - int i = 0; - for (QModelIndex idx : selection->selectedRows()) { - QString name = idx.data().toString(); - if (!ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->isRegular()) { - continue; - } - - // adds an item for the mod name until `i` reaches `max_items`, which - // adds one "..." item; subsequent mods are not shown on the list but - // are still added to `modNames` below so they can be removed correctly - - if (i < max_items) { - mods += "<li>" + name + "</li>"; - } - else if (i == max_items) { - mods += "<li>...</li>"; - } - - modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name()); - ++i; - } - if (QMessageBox::question(this, tr("Confirm"), - tr("Remove the following mods?<br><ul>%1</ul>").arg(mods), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - // use mod names instead of indexes because those become invalid during the removal - DownloadManager::startDisableDirWatcher(); - for (QString name : modNames) { - m_OrganizerCore.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex()); - } - DownloadManager::endDisableDirWatcher(); - } - } else { - m_OrganizerCore.modList()->removeRow(m_ContextRow, QModelIndex()); - } - updateModCount(); - updatePluginCount(); - } catch (const std::exception &e) { - reportError(tr("failed to remove mod: %1").arg(e.what())); - } -} - - void MainWindow::modRemoved(const QString &fileName) { if (!fileName.isEmpty() && !QFileInfo(fileName).isAbsolute()) { @@ -2707,216 +2268,14 @@ void MainWindow::modRemoved(const QString &fileName) } } - -void MainWindow::reinstallMod_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QString installationFile = modInfo->installationFile(); - if (installationFile.length() != 0) { - QString fullInstallationFile; - QFileInfo fileInfo(installationFile); - if (fileInfo.isAbsolute()) { - if (fileInfo.exists()) { - fullInstallationFile = installationFile; - } else { - fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName(); - } - } else { - fullInstallationFile = m_OrganizerCore.downloadManager()->getOutputDirectory() + "/" + installationFile; - } - if (QFile::exists(fullInstallationFile)) { - m_OrganizerCore.installMod(fullInstallationFile, true, modInfo, modInfo->name()); - } else { - QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists")); - } - } else { - QMessageBox::information(this, tr("Failed"), - tr("Mods installed with old versions of MO can't be reinstalled in this way.")); - } -} - -void MainWindow::backupMod_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QString backupDirectory = m_OrganizerCore.installationManager()->generateBackupName(modInfo->absolutePath()); - if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) { - QMessageBox::information(this, tr("Failed"), - tr("Failed to create backup.")); - } - - m_OrganizerCore.refresh(); - updateModCount(); -} - - -void MainWindow::endorseMod(ModInfo::Ptr mod) -{ - m_OrganizerCore.loggedInAction(this, [this, mod] { - mod->endorse(true); - }); -} - - -void MainWindow::endorse_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - - m_OrganizerCore.loggedInAction(this, [this] { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), this); - } - - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(true); - } - }); -} - -void MainWindow::dontendorse_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->setNeverEndorse(); - } - } - else { - ModInfo::getByIndex(m_ContextRow)->setNeverEndorse(); - } -} - - -void MainWindow::unendorseMod(ModInfo::Ptr mod) -{ - m_OrganizerCore.loggedInAction(this, [mod] { - mod->endorse(false); - }); -} - - -void MainWindow::unendorse_clicked() -{ - m_OrganizerCore.loggedInAction(this, [this] { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - MessageDialog::showMessage(tr("Unendorsing multiple mods will take a while. Please wait..."), this); - } - - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->endorse(false); - } - }); -} - - -void MainWindow::trackMod(ModInfo::Ptr mod, bool doTrack) -{ - m_OrganizerCore.loggedInAction(this, [mod, doTrack] { - mod->track(doTrack); - }); -} - - -void MainWindow::track_clicked() -{ - m_OrganizerCore.loggedInAction(this, [this] { - QItemSelectionModel *selection = ui->modList->selectionModel(); - for (auto idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(true); - } - }); -} - -void MainWindow::untrack_clicked() -{ - m_OrganizerCore.loggedInAction(this, [this] { - QItemSelectionModel *selection = ui->modList->selectionModel(); - for (auto idx : selection->selectedRows()) { - ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->track(false); - } - }); -} - void MainWindow::windowTutorialFinished(const QString &windowName) { m_OrganizerCore.settings().interface().setTutorialCompleted(windowName); } -void MainWindow::overwriteClosed(int) -{ - OverwriteInfoDialog *dialog = this->findChild<OverwriteInfoDialog*>("__overwriteDialog"); - if (dialog != nullptr) { - m_OrganizerCore.modList()->modInfoChanged(dialog->modInfo()); - dialog->deleteLater(); - } - m_OrganizerCore.refreshDirectoryStructure(); -} - - -void MainWindow::displayModInformation( - ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) +void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) { - if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) { - log::debug("A different mod information dialog is open. If this is incorrect, please restart MO"); - return; - } - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { - QDialog *dialog = this->findChild<QDialog*>("__overwriteDialog"); - try { - if (dialog == nullptr) { - dialog = new OverwriteInfoDialog(modInfo, this); - dialog->setObjectName("__overwriteDialog"); - } else { - qobject_cast<OverwriteInfoDialog*>(dialog)->setModInfo(modInfo); - } - - dialog->show(); - dialog->raise(); - dialog->activateWindow(); - connect(dialog, SIGNAL(finished(int)), this, SLOT(overwriteClosed(int))); - } catch (const std::exception &e) { - reportError(tr("Failed to display overwrite dialog: %1").arg(e.what())); - } - } else { - modInfo->saveMeta(); - - ModInfoDialog dialog(this, &m_OrganizerCore, &m_PluginContainer, modInfo); - connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); - - //Open the tab first if we want to use the standard indexes of the tabs. - if (tabID != ModInfoTabIDs::None) { - dialog.selectTab(tabID); - } - - dialog.exec(); - - modInfo->saveMeta(); - emit modInfoDisplayed(); - m_OrganizerCore.modList()->modInfoChanged(modInfo); - } - - if (m_OrganizerCore.currentProfile()->modEnabled(modIndex) - && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { - FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - - if (m_OrganizerCore.directoryStructure()->originExists(ToWString(modInfo->name()))) { - FilesOrigin& origin = m_OrganizerCore.directoryStructure()->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - - m_OrganizerCore.directoryRefresher()->addModToStructure(m_OrganizerCore.directoryStructure() - , modInfo->name() - , m_OrganizerCore.currentProfile()->getModPriority(modIndex) - , modInfo->absolutePath() - , modInfo->stealFiles() - , modInfo->archives()); - DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure()); - m_OrganizerCore.directoryStructure()->getFileRegister()->sortOrigins(); - m_OrganizerCore.refreshLists(); - } - } + ui->modList->actions().displayModInformation(modInfo, modIndex, tabID); } bool MainWindow::closeWindow() @@ -2929,1172 +2288,11 @@ void MainWindow::setWindowEnabled(bool enabled) setEnabled(enabled); } - -ModInfo::Ptr MainWindow::nextModInList() -{ - const QModelIndex start = m_ModListSortProxy->mapFromSource( - m_OrganizerCore.modList()->index(m_ContextRow, 0)); - - auto index = start; - - for (;;) { - index = m_ModListSortProxy->index((index.row() + 1) % m_ModListSortProxy->rowCount(), 0); - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - - if (index == start || !index.isValid()) { - // wrapped around, give up - break; - } - - ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - - // skip overwrite and backups and separators - if (mod->hasFlag(ModInfo::FLAG_OVERWRITE) || - mod->hasFlag(ModInfo::FLAG_BACKUP) || - mod->hasFlag(ModInfo::FLAG_SEPARATOR)) { - continue; - } - - return mod; - } - - return {}; -} - -ModInfo::Ptr MainWindow::previousModInList() -{ - const QModelIndex start = m_ModListSortProxy->mapFromSource( - m_OrganizerCore.modList()->index(m_ContextRow, 0)); - - auto index = start; - - for (;;) { - int row = index.row() - 1; - if (row == -1) { - row = m_ModListSortProxy->rowCount() - 1; - } - - index = m_ModListSortProxy->index(row, 0); - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - - if (index == start || !index.isValid()) { - // wrapped around, give up - break; - } - - // skip overwrite and backups and separators - ModInfo::Ptr mod = ModInfo::getByIndex(m_ContextRow); - - if (mod->hasFlag(ModInfo::FLAG_OVERWRITE) || - mod->hasFlag(ModInfo::FLAG_BACKUP) || - mod->hasFlag(ModInfo::FLAG_SEPARATOR)) { - continue; - } - - return mod; - } - - return {}; -} - -void MainWindow::displayModInformation(const QString &modName, ModInfoTabIDs tabID) -{ - unsigned int index = ModInfo::getIndex(modName); - if (index == UINT_MAX) { - log::error("failed to resolve mod name {}", modName); - return; - } - - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - displayModInformation(modInfo, index, tabID); -} - - -void MainWindow::displayModInformation(int row, ModInfoTabIDs tabID) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - displayModInformation(modInfo, row, tabID); -} - - -void MainWindow::ignoreMissingData_clicked() -{ - const auto rows = ui->modList->selectionModel()->selectedRows(); - - if (rows.count() > 1) { - std::vector<ModInfo::Ptr> changed; - - for (QModelIndex idx : rows) { - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - ModInfo::Ptr info = ModInfo::getByIndex(row_idx); - info->markValidated(true); - changed.push_back(info); - } - - for (auto&& m : changed) { - int row_idx = ModInfo::getIndex(m->internalName()); - m_OrganizerCore.modList()->notifyChange(row_idx); - } - } else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->markValidated(true); - m_OrganizerCore.modList()->notifyChange(m_ContextRow); - } -} - -void MainWindow::markConverted_clicked() -{ - const auto rows = ui->modList->selectionModel()->selectedRows(); - - if (rows.count() > 1) { - std::vector<ModInfo::Ptr> changed; - - for (QModelIndex idx : rows) { - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - ModInfo::Ptr info = ModInfo::getByIndex(row_idx); - info->markConverted(true); - changed.push_back(info); - } - - for (auto&& m : changed) { - int row_idx = ModInfo::getIndex(m->internalName()); - m_OrganizerCore.modList()->notifyChange(row_idx); - } - } else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->markConverted(true); - m_OrganizerCore.modList()->notifyChange(m_ContextRow); - } -} - - -void MainWindow::restoreHiddenFiles_clicked() -{ - const int max_items = 20; - QItemSelectionModel* selection = ui->modList->selectionModel(); - - QFlags<FileRenamer::RenameFlags> flags = FileRenamer::UNHIDE; - flags |= FileRenamer::MULTIPLE; - - FileRenamer renamer(this, flags); - - FileRenamer::RenameResults result = FileRenamer::RESULT_OK; - - // multi selection - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - QString mods; - QStringList modNames; - int i = 0; - - for (QModelIndex idx : selection->selectedRows()) { - - QString name = idx.data().toString(); - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(row_idx); - const auto flags = modInfo->getFlags(); - - if (!modInfo->isRegular() || - std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) == flags.end()) { - continue; - } - - // adds an item for the mod name until `i` reaches `max_items`, which - // adds one "..." item; subsequent mods are not shown on the list but - // are still added to `modNames` below so they can be removed correctly - if (i < max_items) { - mods += "<li>" + name + "</li>"; - } - else if (i == max_items) { - mods += "<li>...</li>"; - } - - modNames.append(ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt())->name()); - ++i; - } - if (QMessageBox::question(this, tr("Confirm"), - tr("Restore all hidden files in the following mods?<br><ul>%1</ul>").arg(mods), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - - for (QModelIndex idx : selection->selectedRows()) { - - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(row_idx); - - const auto flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) != flags.end()) { - const QString modDir = modInfo->absolutePath(); - - auto partialResult = restoreHiddenFilesRecursive(renamer, modDir); - - if (partialResult == FileRenamer::RESULT_CANCEL) { - result = FileRenamer::RESULT_CANCEL; - break; - } - originModified((m_OrganizerCore.directoryStructure()->getOriginByName( - ToWString(modInfo->internalName()))).getID()); - } - } - } - } - else { - //single selection - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - const QString modDir = modInfo->absolutePath(); - - if (QMessageBox::question(this, tr("Are you sure?"), - tr("About to restore all hidden files in:\n") + modInfo->name(), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { - - result = restoreHiddenFilesRecursive(renamer, modDir); - - originModified((m_OrganizerCore.directoryStructure()->getOriginByName( - ToWString(modInfo->internalName()))).getID()); - } - } - - if (result == FileRenamer::RESULT_CANCEL){ - log::debug("Restoring hidden files operation cancelled"); - } - else { - log::debug("Finished restoring hidden files"); - } -} - - -void MainWindow::visitOnNexus_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - int count = selection->selectedRows().count(); - if (count > 10) { - if (QMessageBox::question(this, tr("Opening Nexus Links"), - tr("You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?").arg(count), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - int row_idx; - ModInfo::Ptr info; - QString gameName; - - for (QModelIndex idx : selection->selectedRows()) { - row_idx = idx.data(Qt::UserRole + 1).toInt(); - info = ModInfo::getByIndex(row_idx); - int modID = info->nexusId(); - gameName = info->gameName(); - if (modID > 0) { - linkClicked(NexusInterface::instance().getModURL(modID, gameName)); - } else { - log::error("mod '{}' has no nexus id", info->name()); - } - } - } - else { - int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt(); - QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString(); - if (modID > 0) { - linkClicked(NexusInterface::instance().getModURL(modID, gameName)); - } else { - MessageDialog::showMessage(tr("Nexus ID for this mod is unknown"), this); - } - } -} - -void MainWindow::visitWebPage_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - int count = selection->selectedRows().count(); - if (count > 10) { - if (QMessageBox::question(this, tr("Opening Web Pages"), - tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(count), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - int row_idx; - ModInfo::Ptr info; - QString gameName; - for (QModelIndex idx : selection->selectedRows()) { - row_idx = idx.data(Qt::UserRole + 1).toInt(); - info = ModInfo::getByIndex(row_idx); - - const auto url = info->parseCustomURL(); - if (url.isValid()) { - linkClicked(url.toString()); - } - } - } - else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - - const auto url = info->parseCustomURL(); - if (url.isValid()) { - linkClicked(url.toString()); - } - } -} - -void MainWindow::visitNexusOrWebPage(const QModelIndex& idx) -{ - int row_idx = idx.data(Qt::UserRole + 1).toInt(); - - ModInfo::Ptr info = ModInfo::getByIndex(row_idx); - if (!info) { - log::error("mod {} not found", row_idx); - return; - } - - int modID = info->nexusId(); - QString gameName = info->gameName(); - const auto url = info->parseCustomURL(); - - if (modID > 0) { - linkClicked(NexusInterface::instance().getModURL(modID, gameName)); - } else if (url.isValid()) { - linkClicked(url.toString()); - } else { - log::error("mod '{}' has no valid link", info->name()); - } -} - -void MainWindow::visitNexusOrWebPage_clicked() { - QItemSelectionModel* selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - int count = selection->selectedRows().count(); - if (count > 10) { - if (QMessageBox::question(this, tr("Opening Web Pages"), - tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(count), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - - for (QModelIndex idx : selection->selectedRows()) { - visitNexusOrWebPage(idx); - } - } - else { - QModelIndex idx = m_OrganizerCore.modList()->index(m_ContextRow, 0); - visitNexusOrWebPage(idx); - } -} - -void MainWindow::openExplorer_clicked() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - shell::Explore(info->absolutePath()); - } - } - else { - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - shell::Explore(modInfo->absolutePath()); - } -} - -void MainWindow::openPluginOriginExplorer_clicked() -{ - QItemSelectionModel *selection = ui->espList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 0) { - for (QModelIndex idx : selection->selectedRows()) { - QString fileName = idx.data().toString(); - unsigned int modIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)); - if (modIndex == UINT_MAX) { - continue; - } - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - shell::Explore(modInfo->absolutePath()); - } - } - else { - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - shell::Explore(modInfo->absolutePath()); - } -} - -void MainWindow::openExplorer_activated() -{ - if (ui->modList->hasFocus()) { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() == 1 ) { - - QModelIndex idx = selection->currentIndex(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - shell::Explore(modInfo->absolutePath()); - } - - } - } - - if (ui->espList->hasFocus()) { - QItemSelectionModel *selection = ui->espList->selectionModel(); - - if (selection->hasSelection() && selection->selectedRows().count() == 1) { - - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - - - unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)); - if (modInfoIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - shell::Explore(modInfo->absolutePath()); - } - } - } - } -} - void MainWindow::refreshProfile_activated() { m_OrganizerCore.profileRefresh(); } -void MainWindow::updateModCount() -{ - TimeThis tt("updateModCount"); - - int activeCount = 0; - int visActiveCount = 0; - int backupCount = 0; - int visBackupCount = 0; - int foreignCount = 0; - int visForeignCount = 0; - int separatorCount = 0; - int visSeparatorCount = 0; - int regularCount = 0; - int visRegularCount = 0; - - QStringList allMods = m_OrganizerCore.modList()->allMods(); - - auto hasFlag = [](std::vector<ModInfo::EFlag> flags, ModInfo::EFlag filter) { - return std::find(flags.begin(), flags.end(), filter) != flags.end(); - }; - - bool isEnabled; - bool isVisible; - for (QString mod : allMods) { - int modIndex = ModInfo::getIndex(mod); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - std::vector<ModInfo::EFlag> modFlags = modInfo->getFlags(); - isEnabled = m_OrganizerCore.currentProfile()->modEnabled(modIndex); - isVisible = m_ModListSortProxy->filterMatchesMod(modInfo, isEnabled); - - for (auto flag : modFlags) { - switch (flag) { - case ModInfo::FLAG_BACKUP: backupCount++; - if (isVisible) - visBackupCount++; - break; - case ModInfo::FLAG_FOREIGN: foreignCount++; - if (isVisible) - visForeignCount++; - break; - case ModInfo::FLAG_SEPARATOR: separatorCount++; - if (isVisible) - visSeparatorCount++; - break; - } - } - - if (!hasFlag(modFlags, ModInfo::FLAG_BACKUP) && - !hasFlag(modFlags, ModInfo::FLAG_FOREIGN) && - !hasFlag(modFlags, ModInfo::FLAG_SEPARATOR) && - !hasFlag(modFlags, ModInfo::FLAG_OVERWRITE)) { - if (isEnabled) { - activeCount++; - if (isVisible) - visActiveCount++; - } - if (isVisible) - visRegularCount++; - regularCount++; - } - } - - ui->activeModsCounter->display(visActiveCount); - ui->activeModsCounter->setToolTip(tr("<table cellspacing=\"5\">" - "<tr><th>Type</th><th>All</th><th>Visible</th>" - "<tr><td>Enabled mods: </td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr>" - "<tr><td>Unmanaged/DLCs: </td><td align=right>%5</td><td align=right>%6</td></tr>" - "<tr><td>Mod backups: </td><td align=right>%7</td><td align=right>%8</td></tr>" - "<tr><td>Separators: </td><td align=right>%9</td><td align=right>%10</td></tr>" - "</table>") - .arg(activeCount) - .arg(regularCount) - .arg(visActiveCount) - .arg(visRegularCount) - .arg(foreignCount) - .arg(visForeignCount) - .arg(backupCount) - .arg(visBackupCount) - .arg(separatorCount) - .arg(visSeparatorCount) - ); -} - -void MainWindow::updatePluginCount() -{ - int activeMasterCount = 0; - int activeLightMasterCount = 0; - int activeRegularCount = 0; - int masterCount = 0; - int lightMasterCount = 0; - int regularCount = 0; - int activeVisibleCount = 0; - - PluginList *list = m_OrganizerCore.pluginList(); - QString filter = ui->espFilterEdit->text(); - - for (QString plugin : list->pluginNames()) { - bool active = list->isEnabled(plugin); - bool visible = m_PluginListSortProxy->filterMatchesPlugin(plugin); - if (list->isLight(plugin) || list->isLightFlagged(plugin)) { - lightMasterCount++; - activeLightMasterCount += active; - activeVisibleCount += visible && active; - } else if (list->isMaster(plugin)) { - masterCount++; - activeMasterCount += active; - activeVisibleCount += visible && active; - } else { - regularCount++; - activeRegularCount += active; - activeVisibleCount += visible && active; - } - } - - int activeCount = activeMasterCount + activeLightMasterCount + activeRegularCount; - int totalCount = masterCount + lightMasterCount + regularCount; - - ui->activePluginsCounter->display(activeVisibleCount); - ui->activePluginsCounter->setToolTip(tr("<table cellspacing=\"6\">" - "<tr><th>Type</th><th>Active </th><th>Total</th></tr>" - "<tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr>" - "<tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr>" - "<tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr>" - "<tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr>" - "<tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr>" - "</table>") - .arg(activeCount).arg(totalCount) - .arg(activeMasterCount).arg(masterCount) - .arg(activeLightMasterCount).arg(lightMasterCount) - .arg(activeRegularCount).arg(regularCount) - .arg(activeMasterCount+activeRegularCount).arg(masterCount+regularCount) - ); -} - -void MainWindow::information_clicked() -{ - try { - displayModInformation(m_ContextRow); - } catch (const std::exception &e) { - reportError(e.what()); - } -} - -void MainWindow::createEmptyMod_clicked() -{ - GuessedValue<QString> name; - name.setFilter(&fixDirectoryName); - - while (name->isEmpty()) { - bool ok; - name.update(QInputDialog::getText(this, tr("Create Mod..."), - tr("This will create an empty mod.\n" - "Please enter a name:"), QLineEdit::Normal, "", &ok), - GUESS_USER); - if (!ok) { - return; - } - } - - if (m_OrganizerCore.modList()->getMod(name) != nullptr) { - reportError(tr("A mod with this name already exists")); - return; - } - - int newPriority = -1; - if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) { - newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); - } - - IModInterface *newMod = m_OrganizerCore.createMod(name); - if (newMod == nullptr) { - return; - } - - m_OrganizerCore.refresh(); - - if (newPriority >= 0) { - m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); - } -} - -void MainWindow::createSeparator_clicked() -{ - GuessedValue<QString> name; - name.setFilter(&fixDirectoryName); - while (name->isEmpty()) - { - bool ok; - name.update(QInputDialog::getText(this, tr("Create Separator..."), - tr("This will create a new separator.\n" - "Please enter a name:"), QLineEdit::Normal, "", &ok), - GUESS_USER); - if (!ok) { return; } - } - if (m_OrganizerCore.modList()->getMod(name) != nullptr) - { - reportError(tr("A separator with this name already exists")); - return; - } - name->append("_separator"); - if (m_OrganizerCore.modList()->getMod(name) != nullptr) - { - return; - } - - int newPriority = -1; - if (m_ContextRow >= 0 && m_ModListSortProxy->sortColumn() == ModList::COL_PRIORITY) - { - newPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); - } - - if (m_OrganizerCore.createMod(name) == nullptr) { return; } - m_OrganizerCore.refresh(); - - if (newPriority >= 0) - { - m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); - } - - if (auto c=m_OrganizerCore.settings().colors().previousSeparatorColor()) { - ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(*c); - } -} - -void MainWindow::setColor_clicked() -{ - auto& settings = m_OrganizerCore.settings(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - - QColorDialog dialog(this); - dialog.setOption(QColorDialog::ShowAlphaChannel); - - QColor currentColor = modInfo->color(); - if (currentColor.isValid()) { - dialog.setCurrentColor(currentColor); - } - else if (auto c=settings.colors().previousSeparatorColor()) { - dialog.setCurrentColor(*c); - } - - if (!dialog.exec()) - return; - - currentColor = dialog.currentColor(); - if (!currentColor.isValid()) - return; - - settings.colors().setPreviousSeparatorColor(currentColor); - - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - info->setColor(currentColor); - } - } - else { - modInfo->setColor(currentColor); - } -} - -void MainWindow::resetColor_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QColor color = QColor(); - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - info->setColor(color); - } - } - else { - modInfo->setColor(color); - } - - m_OrganizerCore.settings().colors().removePreviousSeparatorColor(); -} - -void MainWindow::createModFromOverwrite() -{ - GuessedValue<QString> name; - name.setFilter(&fixDirectoryName); - - while (name->isEmpty()) { - bool ok; - name.update(QInputDialog::getText(this, tr("Create Mod..."), - tr("This will move all files from overwrite into a new, regular mod.\n" - "Please enter a name:"), QLineEdit::Normal, "", &ok), - GUESS_USER); - if (!ok) { - return; - } - } - - if (m_OrganizerCore.modList()->getMod(name) != nullptr) { - reportError(tr("A mod with this name already exists")); - return; - } - - const IModInterface *newMod = m_OrganizerCore.createMod(name); - if (newMod == nullptr) { - return; - } - - doMoveOverwriteContentToMod(newMod->absolutePath()); -} - -void MainWindow::moveOverwriteContentToExistingMod() -{ - QStringList mods; - auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); - for (auto & iter : indexesByPriority) { - if ((iter.second != UINT_MAX)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second); - if (!modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && !modInfo->hasFlag(ModInfo::FLAG_FOREIGN) && !modInfo->hasFlag(ModInfo::FLAG_OVERWRITE)) { - mods << modInfo->name(); - } - } - } - - ListDialog dialog(this); - dialog.setWindowTitle("Select a mod..."); - dialog.setChoices(mods); - - if (dialog.exec() == QDialog::Accepted) { - QString result = dialog.getChoice(); - if (!result.isEmpty()) { - - QString modAbsolutePath; - - for (const auto& mod : m_OrganizerCore.modsSortedByProfilePriority(m_OrganizerCore.currentProfile())) { - if (result.compare(mod) == 0) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod)); - modAbsolutePath = modInfo->absolutePath(); - break; - } - } - - if (modAbsolutePath.isNull()) { - log::warn("Mod {} has not been found, for some reason", result); - return; - } - - doMoveOverwriteContentToMod(modAbsolutePath); - } - } -} - -void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath) -{ - unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { - std::vector<ModInfo::EFlag> flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); - - ModInfo::Ptr overwriteInfo = ModInfo::getByIndex(overwriteIndex); - bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"), - (QDir::toNativeSeparators(modAbsolutePath)), false, this); - - if (successful) { - MessageDialog::showMessage(tr("Move successful."), this); - } - else { - const auto e = GetLastError(); - log::error("Move operation failed: {}", formatSystemMessage(e)); - } - - m_OrganizerCore.refresh(); -} - -void MainWindow::clearOverwrite() -{ - unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { - std::vector<ModInfo::EFlag> flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) - != flags.end(); - }); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); - if (modInfo) - { - QDir overwriteDir(modInfo->absolutePath()); - if (QMessageBox::question(this, tr("Are you sure?"), - tr("About to recursively delete:\n") + overwriteDir.absolutePath(), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) - { - QStringList delList; - for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot)) - delList.push_back(overwriteDir.absoluteFilePath(f)); - if (shellDelete(delList, true)) { - scheduleCheckForProblems(); - m_OrganizerCore.refresh(); - } else { - const auto e = GetLastError(); - log::error("Delete operation failed: {}", formatSystemMessage(e)); - } - } - } -} - -void MainWindow::cancelModListEditor() -{ - ui->modList->setEnabled(false); - ui->modList->setEnabled(true); -} - -void MainWindow::on_modList_doubleClicked(const QModelIndex &index) -{ - if (!index.isValid()) { - return; - } - - if (m_OrganizerCore.modList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { - // don't interpret double click if we only just checked a mod - return; - } - - QModelIndex sourceIdx = mapToModel(m_OrganizerCore.modList(), index); - if (!sourceIdx.isValid()) { - return; - } - - Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); - if (modifiers.testFlag(Qt::ControlModifier)) { - try { - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - shell::Explore(modInfo->absolutePath()); - - // workaround to cancel the editor that might have opened because of - // selection-click - ui->modList->closePersistentEditor(index); - } - catch (const std::exception &e) { - reportError(e.what()); - } - } - else if (modifiers.testFlag(Qt::ShiftModifier)) { - try { - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - QModelIndex idx = m_OrganizerCore.modList()->index(m_ContextRow, 0); - visitNexusOrWebPage(idx); - ui->modList->closePersistentEditor(index); - } - catch (const std::exception & e) { - reportError(e.what()); - } - } - else{ - try { - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - sourceIdx.column(); - - auto tab = ModInfoTabIDs::None; - - switch (sourceIdx.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; - } - - displayModInformation(sourceIdx.row(), tab); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->modList->closePersistentEditor(index); - } - catch (const std::exception &e) { - reportError(e.what()); - } - } -} - -void MainWindow::on_listOptionsBtn_pressed() -{ - m_ContextRow = -1; -} - -void MainWindow::openOriginInformation_clicked() -{ - try { - QItemSelectionModel *selection = ui->espList->selectionModel(); - //we don't want to open multiple modinfodialogs. - /*if (selection->hasSelection() && selection->selectedRows().count() > 0) { - - for (QModelIndex idx : selection->selectedRows()) { - QString fileName = idx.data().toString(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - } - } - } - else {}*/ - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - } - } - catch (const std::exception &e) { - reportError(e.what()); - } -} - -void MainWindow::on_espList_doubleClicked(const QModelIndex &index) -{ - if (!index.isValid()) { - return; - } - - if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { - // don't interpret double click if we only just checked a plugin - return; - } - - QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index); - if (!sourceIdx.isValid()) { - return; - } - try { - - QItemSelectionModel *selection = ui->espList->selectionModel(); - - if (selection->hasSelection() && selection->selectedRows().count() == 1) { - - QModelIndex idx = selection->currentIndex(); - QString fileName = idx.data().toString(); - - if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX) - return; - - ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { - - Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); - if (modifiers.testFlag(Qt::ControlModifier)) { - openExplorer_activated(); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->espList->closePersistentEditor(index); - } - else { - - displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->espList->closePersistentEditor(index); - } - } - } - } - catch (const std::exception &e) { - reportError(e.what()); - } -} - -bool MainWindow::populateMenuCategories(QMenu *menu, int targetID) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - const std::set<int> &categories = modInfo->getCategories(); - - bool childEnabled = false; - - for (unsigned int i = 1; i < m_CategoryFactory.numCategories(); ++i) { - if (m_CategoryFactory.getParentID(i) == targetID) { - QMenu *targetMenu = menu; - if (m_CategoryFactory.hasChildren(i)) { - targetMenu = menu->addMenu(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); - } - - int id = m_CategoryFactory.getCategoryID(i); - QScopedPointer<QCheckBox> checkBox(new QCheckBox(targetMenu)); - bool enabled = categories.find(id) != categories.end(); - checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); - if (enabled) { - childEnabled = true; - } - checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); - - QScopedPointer<QWidgetAction> checkableAction(new QWidgetAction(targetMenu)); - checkableAction->setDefaultWidget(checkBox.take()); - checkableAction->setData(id); - targetMenu->addAction(checkableAction.take()); - - if (m_CategoryFactory.hasChildren(i)) { - if (populateMenuCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) { - targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); - } - } - } - } - return childEnabled; -} - -void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow); - for (QAction* action : menu->actions()) { - if (action->menu() != nullptr) { - replaceCategoriesFromMenu(action->menu(), modRow); - } else { - QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action); - if (widgetAction != nullptr) { - QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget()); - modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked()); - } - } - } -} - -void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow) -{ - if (referenceRow != -1 && referenceRow != modRow) { - ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow); - for (QAction* action : menu->actions()) { - if (action->menu() != nullptr) { - addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow); - } else { - QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action); - if (widgetAction != nullptr) { - QCheckBox *checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget()); - int categoryId = widgetAction->data().toInt(); - bool checkedBefore = editedModInfo->categorySet(categoryId); - bool checkedAfter = checkbox->isChecked(); - - if (checkedBefore != checkedAfter) { // only update if the category was changed on the edited mod - ModInfo::Ptr currentModInfo = ModInfo::getByIndex(modRow); - currentModInfo->setCategory(categoryId, checkedAfter); - } - } - } - } - } else { - replaceCategoriesFromMenu(menu, modRow); - } -} - -void MainWindow::addRemoveCategories_MenuHandler() { - QMenu *menu = qobject_cast<QMenu*>(sender()); - if (menu == nullptr) { - log::error("not a menu?"); - return; - } - - QList<QPersistentModelIndex> selected; - for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { - selected.append(QPersistentModelIndex(idx)); - } - - if (selected.size() > 0) { - int minRow = INT_MAX; - int maxRow = -1; - - for (const QPersistentModelIndex &idx : selected) { - log::debug("change categories on: {}", idx.data().toString()); - QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx); - if (modIdx.row() != m_ContextIdx.row()) { - addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row()); - } - if (idx.row() < minRow) minRow = idx.row(); - if (idx.row() > maxRow) maxRow = idx.row(); - } - replaceCategoriesFromMenu(menu, m_ContextIdx.row()); - - m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); - - for (const QPersistentModelIndex &idx : selected) { - ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); - } - } else { - //For single mod selections, just do a replace - replaceCategoriesFromMenu(menu, m_ContextRow); - m_OrganizerCore.modList()->notifyChange(m_ContextRow); - } - - refreshFilters(); -} - -void MainWindow::replaceCategories_MenuHandler() { - QMenu *menu = qobject_cast<QMenu*>(sender()); - if (menu == nullptr) { - log::error("not a menu?"); - return; - } - - QList<QPersistentModelIndex> selected; - for (const QModelIndex &idx : ui->modList->selectionModel()->selectedRows()) { - selected.append(QPersistentModelIndex(idx)); - } - - if (selected.size() > 0) { - QStringList selectedMods; - int minRow = INT_MAX; - int maxRow = -1; - for (int i = 0; i < selected.size(); ++i) { - QModelIndex temp = mapToModel(m_OrganizerCore.modList(), selected.at(i)); - selectedMods.append(temp.data().toString()); - replaceCategoriesFromMenu(menu, mapToModel(m_OrganizerCore.modList(), selected.at(i)).row()); - if (temp.row() < minRow) minRow = temp.row(); - if (temp.row() > maxRow) maxRow = temp.row(); - } - - m_OrganizerCore.modList()->notifyChange(minRow, maxRow + 1); - - // find mods by their name because indices are invalidated - QAbstractItemModel *model = ui->modList->model(); - for (const QString &mod : selectedMods) { - QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1, - Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive); - if (matches.size() > 0) { - ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows); - } - } - } else { - //For single mod selections, just do a replace - replaceCategoriesFromMenu(menu, m_ContextRow); - m_OrganizerCore.modList()->notifyChange(m_ContextRow); - } - - refreshFilters(); -} - void MainWindow::saveArchiveList() { if (m_OrganizerCore.isArchivesInit()) { @@ -4114,180 +2312,6 @@ void MainWindow::saveArchiveList() } } -void MainWindow::checkModsForUpdates() -{ - bool checkingModsForUpdate = false; - if (NexusInterface::instance().getAccessManager()->validated()) { - checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); - NexusInterface::instance().requestEndorsementInfo(this, QVariant(), QString()); - NexusInterface::instance().requestTrackingInfo(this, QVariant(), QString()); - } else { - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); - NexusInterface::instance().getAccessManager()->apiCheck(apiKey); - } else { - log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.")); - } - } - - bool updatesAvailable = false; - for (auto mod : m_OrganizerCore.modList()->allMods()) { - ModInfo::Ptr modInfo = ModInfo::getByName(mod); - if (modInfo->updateAvailable()) { - updatesAvailable = true; - break; - } - } - - if (updatesAvailable || checkingModsForUpdate) { - m_ModListSortProxy->setCriteria({{ - ModListSortProxy::TypeSpecial, - CategoryFactory::UpdateAvailable, - false} - }); - - m_Filters->setSelection({{ - ModListSortProxy::TypeSpecial, - CategoryFactory::UpdateAvailable, - false - }}); - } -} - -void MainWindow::changeVersioningScheme() { - if (QMessageBox::question(this, tr("Continue?"), - tr("The versioning scheme decides which version is considered newer than another.\n" - "This function will guess the versioning scheme under the assumption that the installed version is outdated."), - QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) { - - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - - bool success = false; - - static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS }; - - for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) { - VersionInfo verOld(info->version().canonicalString(), schemes[i]); - VersionInfo verNew(info->newestVersion().canonicalString(), schemes[i]); - if (verOld < verNew) { - info->setVersion(verOld); - info->setNewestVersion(verNew); - success = true; - } - } - if (!success) { - QMessageBox::information(this, tr("Sorry"), - tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->newestVersion().canonicalString()).arg(info->version().canonicalString()), - QMessageBox::Ok); - } - } -} - -void MainWindow::ignoreUpdate() { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - info->ignoreUpdate(true); - } - } - else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->ignoreUpdate(true); - } - if (m_ModListSortProxy != nullptr) - m_ModListSortProxy->invalidate(); -} - -void MainWindow::checkModUpdates_clicked() -{ - std::multimap<QString, int> IDs; - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - IDs.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId())); - } - } else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - IDs.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId())); - } - modUpdateCheck(IDs); -} - -void MainWindow::unignoreUpdate() -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - for (QModelIndex idx : selection->selectedRows()) { - ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); - info->ignoreUpdate(false); - } - } - else { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->ignoreUpdate(false); - } - if (m_ModListSortProxy != nullptr) - m_ModListSortProxy->invalidate(); -} - -void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, - ModInfo::Ptr info) { - const std::set<int> &categories = info->getCategories(); - for (int categoryID : categories) { - int catIdx = m_CategoryFactory.getCategoryIndex(categoryID); - QWidgetAction *action = new QWidgetAction(primaryCategoryMenu); - try { - QRadioButton *categoryBox = new QRadioButton( - m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"), - primaryCategoryMenu); - connect(categoryBox, &QRadioButton::toggled, [info, categoryID](bool enable) { - if (enable) { - info->setPrimaryCategory(categoryID); - } - }); - categoryBox->setChecked(categoryID == info->primaryCategory()); - action->setDefaultWidget(categoryBox); - } catch (const std::exception &e) { - log::error("failed to create category checkbox: {}", e.what()); - } - - action->setData(categoryID); - primaryCategoryMenu->addAction(action); - } -} - -void MainWindow::addPrimaryCategoryCandidates() -{ - QMenu *menu = qobject_cast<QMenu*>(sender()); - if (menu == nullptr) { - log::error("not a menu?"); - return; - } - menu->clear(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - - addPrimaryCategoryCandidates(menu, modInfo); -} - -void MainWindow::enableVisibleMods() -{ - if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all visible mods?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_ModListSortProxy->enableAllVisible(); - } -} - -void MainWindow::disableVisibleMods() -{ - if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all visible mods?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_ModListSortProxy->disableAllVisible(); - } -} - void MainWindow::openInstanceFolder() { QString dataPath = qApp->property("dataPath").toString(); @@ -4353,184 +2377,6 @@ void MainWindow::openMyGamesFolder() shell::Explore(m_OrganizerCore.managedGame()->documentsDirectory()); } - -void MainWindow::exportModListCSV() -{ - //SelectionDialog selection(tr("Choose what to export")); - - //selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0); - //selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1); - //selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2); - - QDialog selection(this); - QGridLayout *grid = new QGridLayout; - selection.setWindowTitle(tr("Export to csv")); - - QLabel *csvDescription = new QLabel(); - csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead.")); - grid->addWidget(csvDescription); - - QGroupBox *groupBoxRows = new QGroupBox(tr("Select what mods you want export:")); - QRadioButton *all = new QRadioButton(tr("All installed mods")); - QRadioButton *active = new QRadioButton(tr("Only active (checked) mods from your current profile")); - QRadioButton *visible = new QRadioButton(tr("All currently visible mods in the mod list")); - - QVBoxLayout *vbox = new QVBoxLayout; - vbox->addWidget(all); - vbox->addWidget(active); - vbox->addWidget(visible); - vbox->addStretch(1); - groupBoxRows->setLayout(vbox); - - - - grid->addWidget(groupBoxRows); - - QButtonGroup *buttonGroupRows = new QButtonGroup(); - buttonGroupRows->addButton(all, 0); - buttonGroupRows->addButton(active, 1); - buttonGroupRows->addButton(visible, 2); - buttonGroupRows->button(0)->setChecked(true); - - - - QGroupBox *groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:")); - groupBoxColumns->setFlat(true); - - QCheckBox *mod_Priority = new QCheckBox(tr("Mod_Priority")); - mod_Priority->setChecked(true); - QCheckBox *mod_Name = new QCheckBox(tr("Mod_Name")); - mod_Name->setChecked(true); - QCheckBox *mod_Note = new QCheckBox(tr("Notes_column")); - QCheckBox *mod_Status = new QCheckBox(tr("Mod_Status")); - mod_Status->setChecked(true); - QCheckBox *primary_Category = new QCheckBox(tr("Primary_Category")); - QCheckBox *nexus_ID = new QCheckBox(tr("Nexus_ID")); - QCheckBox *mod_Nexus_URL = new QCheckBox(tr("Mod_Nexus_URL")); - QCheckBox *mod_Version = new QCheckBox(tr("Mod_Version")); - QCheckBox *install_Date = new QCheckBox(tr("Install_Date")); - QCheckBox *download_File_Name = new QCheckBox(tr("Download_File_Name")); - - QVBoxLayout *vbox1 = new QVBoxLayout; - vbox1->addWidget(mod_Priority); - vbox1->addWidget(mod_Name); - vbox1->addWidget(mod_Status); - vbox1->addWidget(mod_Note); - vbox1->addWidget(primary_Category); - vbox1->addWidget(nexus_ID); - vbox1->addWidget(mod_Nexus_URL); - vbox1->addWidget(mod_Version); - vbox1->addWidget(install_Date); - vbox1->addWidget(download_File_Name); - groupBoxColumns->setLayout(vbox1); - - grid->addWidget(groupBoxColumns); - - QPushButton *ok = new QPushButton("Ok"); - QPushButton *cancel = new QPushButton("Cancel"); - QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - - connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject())); - - grid->addWidget(buttons); - - selection.setLayout(grid); - - - if (selection.exec() == QDialog::Accepted) { - - unsigned int numMods = ModInfo::getNumMods(); - int selectedRowID = buttonGroupRows->checkedId(); - - try { - QBuffer buffer; - buffer.open(QIODevice::ReadWrite); - CSVBuilder builder(&buffer); - builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS); - std::vector<std::pair<QString, CSVBuilder::EFieldType> > fields; - if (mod_Priority->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Priority"), CSVBuilder::TYPE_STRING)); - if (mod_Status->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING)); - if (mod_Name->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Name"), CSVBuilder::TYPE_STRING)); - if (mod_Note->isChecked()) - fields.push_back(std::make_pair(QString("#Note"), CSVBuilder::TYPE_STRING)); - if (primary_Category->isChecked()) - fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING)); - if (nexus_ID->isChecked()) - fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER)); - if (mod_Nexus_URL->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING)); - if (mod_Version->isChecked()) - fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING)); - if (install_Date->isChecked()) - fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING)); - if (download_File_Name->isChecked()) - fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING)); - - builder.setFields(fields); - - builder.writeHeader(); - - auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); - for (auto& iter : indexesByPriority) { - ModInfo::Ptr info = ModInfo::getByIndex(iter.second); - bool enabled = m_OrganizerCore.currentProfile()->modEnabled(iter.second); - if ((selectedRowID == 1) && !enabled) { - continue; - } - else if ((selectedRowID == 2) && !m_ModListSortProxy->filterMatchesMod(info, enabled)) { - continue; - } - std::vector<ModInfo::EFlag> flags = info->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) && - (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) { - if (mod_Priority->isChecked()) - builder.setRowField("#Mod_Priority", QString("%1").arg(iter.first, 4, 10, QChar('0'))); - if (mod_Status->isChecked()) - builder.setRowField("#Mod_Status", (enabled) ? "+" : "-"); - if (mod_Name->isChecked()) - builder.setRowField("#Mod_Name", info->name()); - if (mod_Note->isChecked()) - builder.setRowField("#Note", QString("%1").arg(info->comments().remove(','))); - if (primary_Category->isChecked()) - builder.setRowField("#Primary_Category", (m_CategoryFactory.categoryExists(info->primaryCategory())) ? m_CategoryFactory.getCategoryNameByID(info->primaryCategory()) : ""); - if (nexus_ID->isChecked()) - builder.setRowField("#Nexus_ID", info->nexusId()); - if (mod_Nexus_URL->isChecked()) - builder.setRowField("#Mod_Nexus_URL",(info->nexusId()>0)? NexusInterface::instance().getModURL(info->nexusId(), info->gameName()) : ""); - if (mod_Version->isChecked()) - builder.setRowField("#Mod_Version", info->version().canonicalString()); - if (install_Date->isChecked()) - builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss")); - if (download_File_Name->isChecked()) - builder.setRowField("#Download_File_Name", info->installationFile()); - - builder.writeRow(); - } - } - - SaveTextAsDialog saveDialog(this); - saveDialog.setText(buffer.data()); - saveDialog.exec(); - } - catch (const std::exception &e) { - reportError(tr("export failed: %1").arg(e.what())); - } - } -} - -static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu) -{ - QPushButton *pushBtn = new QPushButton(subMenu->title()); - pushBtn->setMenu(subMenu); - QWidgetAction *action = new QWidgetAction(menu); - action->setDefaultWidget(pushBtn); - menu->addAction(action); -} - QMenu *MainWindow::openFolderMenu() { QMenu *FolderMenu = new QMenu(this); @@ -4560,268 +2406,6 @@ QMenu *MainWindow::openFolderMenu() return FolderMenu; } -void MainWindow::initModListContextMenu(QMenu *menu) -{ - menu->addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); - menu->addAction(tr("Create empty mod"), this, SLOT(createEmptyMod_clicked())); - menu->addAction(tr("Create Separator"), this, SLOT(createSeparator_clicked())); - - menu->addSeparator(); - - menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); - menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - menu->addAction(tr("Check for updates"), this, SLOT(checkModsForUpdates())); - menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); - menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); -} - -void MainWindow::addModSendToContextMenu(QMenu *menu) -{ - if (m_ModListSortProxy->sortColumn() != ModList::COL_PRIORITY) - return; - - QMenu *sub_menu = new QMenu(menu); - sub_menu->setTitle(tr("Send to")); - sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedModsToTop_clicked())); - sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedModsToBottom_clicked())); - sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedModsToPriority_clicked())); - sub_menu->addAction(tr("Separator..."), this, SLOT(sendSelectedModsToSeparator_clicked())); - - menu->addMenu(sub_menu); - menu->addSeparator(); -} - -void MainWindow::addPluginSendToContextMenu(QMenu *menu) -{ - if (m_PluginListSortProxy->sortColumn() != PluginList::COL_PRIORITY) - return; - - QMenu *sub_menu = new QMenu(this); - sub_menu->setTitle(tr("Send to")); - sub_menu->addAction(tr("Top"), this, SLOT(sendSelectedPluginsToTop_clicked())); - sub_menu->addAction(tr("Bottom"), this, SLOT(sendSelectedPluginsToBottom_clicked())); - sub_menu->addAction(tr("Priority..."), this, SLOT(sendSelectedPluginsToPriority_clicked())); - - menu->addMenu(sub_menu); - menu->addSeparator(); -} - -void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) -{ - try { - QTreeView *modList = findChild<QTreeView*>("modList"); - - m_ContextIdx = mapToModel(m_OrganizerCore.modList(), modList->indexAt(pos)); - m_ContextRow = m_ContextIdx.row(); - int contextColumn = m_ContextIdx.column(); - - if (m_ContextRow == -1) { - // no selection - QMenu menu(this); - initModListContextMenu(&menu); - menu.exec(modList->viewport()->mapToGlobal(pos)); - } - else { - QMenu menu(this); - - QMenu *allMods = new QMenu(&menu); - initModListContextMenu(allMods); - allMods->setTitle(tr("All Mods")); - menu.addMenu(allMods); - menu.addSeparator(); - - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - std::vector<ModInfo::EFlag> flags = info->getFlags(); - // Context menu for overwrites - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { - if (QDir(info->absolutePath()).count() > 2) { - menu.addAction(tr("Sync to Mods..."), &m_OrganizerCore, SLOT(syncOverwrite())); - menu.addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite())); - menu.addAction(tr("Move content to Mod..."), this, SLOT(moveOverwriteContentToExistingMod())); - menu.addAction(tr("Clear Overwrite..."), this, SLOT(clearOverwrite())); - } - menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); - } - // Context menu for mod backups - else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) { - menu.addAction(tr("Restore Backup"), this, SLOT(restoreBackup_clicked())); - menu.addAction(tr("Remove Backup..."), this, SLOT(removeMod_clicked())); - menu.addSeparator(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { - menu.addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked())); - } - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) { - menu.addAction(tr("Mark as converted/working"), this, SLOT(markConverted_clicked())); - } - menu.addSeparator(); - if (info->nexusId() > 0) { - menu.addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); - } - - const auto url = info->parseCustomURL(); - if (url.isValid()) { - menu.addAction( - tr("Visit on %1").arg(url.host()), - this, SLOT(visitWebPage_clicked())); - } - - menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); - } - else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()){ - menu.addSeparator(); - QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu); - populateMenuCategories(addRemoveCategoriesMenu, 0); - connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); - addMenuAsPushButton(&menu, addRemoveCategoriesMenu); - QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu); - connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); - addMenuAsPushButton(&menu, primaryCategoryMenu); - menu.addSeparator(); - menu.addAction(tr("Rename Separator..."), this, SLOT(renameMod_clicked())); - menu.addAction(tr("Remove Separator..."), this, SLOT(removeMod_clicked())); - menu.addSeparator(); - addModSendToContextMenu(&menu); - menu.addAction(tr("Select Color..."), this, SLOT(setColor_clicked())); - - if(info->color().isValid()) - menu.addAction(tr("Reset Color"), this, SLOT(resetColor_clicked())); - - menu.addSeparator(); - } - else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) != flags.end()) { - addModSendToContextMenu(&menu); - } - else { - QMenu *addRemoveCategoriesMenu = new QMenu(tr("Change Categories"), &menu); - populateMenuCategories(addRemoveCategoriesMenu, 0); - connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); - addMenuAsPushButton(&menu, addRemoveCategoriesMenu); - - QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category"), &menu); - connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); - addMenuAsPushButton(&menu, primaryCategoryMenu); - - menu.addSeparator(); - - if (info->downgradeAvailable()) { - menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); - } - - if (info->nexusId() > 0) - menu.addAction(tr("Force-check updates"), this, SLOT(checkModUpdates_clicked())); - if (info->updateIgnored()) { - menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); - } else { - if (info->updateAvailable() || info->downgradeAvailable()) { - menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); - } - } - menu.addSeparator(); - - menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedMods_clicked())); - menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedMods_clicked())); - - menu.addSeparator(); - - addModSendToContextMenu(&menu); - - menu.addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked())); - menu.addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked())); - menu.addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked())); - menu.addAction(tr("Create Backup"), this, SLOT(backupMod_clicked())); - - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) != flags.end()) { - menu.addAction(tr("Restore hidden files"), this, SLOT(restoreHiddenFiles_clicked())); - } - - menu.addSeparator(); - - if (contextColumn == ModList::COL_NOTES) { - menu.addAction(tr("Select Color..."), this, SLOT(setColor_clicked())); - - if (info->color().isValid()) - menu.addAction(tr("Reset Color"), this, SLOT(resetColor_clicked())); - - menu.addSeparator(); - } - - if (info->nexusId() > 0 && Settings::instance().nexus().endorsementIntegration()) { - switch (info->endorsedState()) { - case EndorsedState::ENDORSED_TRUE: { - menu.addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked())); - } break; - case EndorsedState::ENDORSED_FALSE: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); - menu.addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked())); - } break; - case EndorsedState::ENDORSED_NEVER: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); - } break; - default: { - QAction *action = new QAction(tr("Endorsement state unknown"), &menu); - action->setEnabled(false); - menu.addAction(action); - } break; - } - } - - if (info->nexusId() > 0 && Settings::instance().nexus().trackedIntegration()) { - switch (info->trackedState()) { - case TrackedState::TRACKED_FALSE: { - menu.addAction(tr("Start tracking"), this, SLOT(track_clicked())); - } break; - case TrackedState::TRACKED_TRUE: { - menu.addAction(tr("Stop tracking"), this, SLOT(untrack_clicked())); - } break; - default: { - QAction *action = new QAction(tr("Tracked state unknown"), &menu); - action->setEnabled(false); - menu.addAction(action); - } break; - } - } - - menu.addSeparator(); - - std::vector<ModInfo::EFlag> flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { - menu.addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked())); - } - - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) { - menu.addAction(tr("Mark as converted/working"), this, SLOT(markConverted_clicked())); - } - - menu.addSeparator(); - - if (info->nexusId() > 0) { - menu.addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); - } - - const auto url = info->parseCustomURL(); - if (url.isValid()) { - menu.addAction( - tr("Visit on %1").arg(url.host()), - this, SLOT(visitWebPage_clicked())); - } - - menu.addAction(tr("Open in Explorer"), this, SLOT(openExplorer_clicked())); - } - - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) { - QAction *infoAction = menu.addAction(tr("Information..."), this, SLOT(information_clicked())); - menu.setDefaultAction(infoAction); - } - - menu.exec(modList->viewport()->mapToGlobal(pos)); - } - } catch (const std::exception &e) { - reportError(tr("Exception: ").arg(e.what())); - } catch (...) { - reportError(tr("Unknown exception")); - } -} - void MainWindow::linkToolbar() { Executable* exe = getSelectedExecutable(); @@ -4916,7 +2500,8 @@ void MainWindow::on_actionSettings_triggered() scheduleCheckForProblems(); fixCategories(); - refreshFilters(); + ui->modList->refreshFilters(); + ui->modList->refresh(); if (settings.paths().profiles() != oldProfilesDirectory) { refreshProfiles(); @@ -4999,13 +2584,6 @@ void MainWindow::on_actionNexus_triggered() shell::Open(QUrl(NexusInterface::instance().getGameURL(gameName))); } - -void MainWindow::linkClicked(const QString &url) -{ - shell::Open(QUrl(url)); -} - - void MainWindow::installTranslator(const QString &name) { QTranslator *translator = new QTranslator(this); @@ -5020,7 +2598,6 @@ void MainWindow::installTranslator(const QString &name) m_Translators.push_back(translator); } - void MainWindow::languageChange(const QString &newLanguage) { for (QTranslator *trans : m_Translators) { @@ -5047,10 +2624,7 @@ void MainWindow::languageChange(const QString &newLanguage) m_DownloadsTab->update(); } - QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn); - initModListContextMenu(listOptionsMenu); - ui->listOptionsBtn->setMenu(listOptionsMenu); - + ui->listOptionsBtn->setMenu(new ModListGlobalContextMenu(m_OrganizerCore, ui->modList, this)); ui->openFolderMenu->setMenu(openFolderMenu()); } @@ -5066,57 +2640,6 @@ void MainWindow::originModified(int originID) DirectoryRefresher::cleanStructure(m_OrganizerCore.directoryStructure()); } - -void MainWindow::enableSelectedPlugins_clicked() -{ - m_OrganizerCore.pluginList()->enableSelected(ui->espList->selectionModel()); -} - - -void MainWindow::disableSelectedPlugins_clicked() -{ - m_OrganizerCore.pluginList()->disableSelected(ui->espList->selectionModel()); -} - -void MainWindow::sendSelectedPluginsToTop_clicked() -{ - m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), 0); -} - -void MainWindow::sendSelectedPluginsToBottom_clicked() -{ - m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), INT_MAX); -} - -void MainWindow::sendSelectedPluginsToPriority_clicked() -{ - bool ok; - int newPriority = QInputDialog::getInt(this, - tr("Set Priority"), tr("Set the priority of the selected plugins"), - 0, 0, INT_MAX, 1, &ok); - if (!ok) return; - - m_OrganizerCore.pluginList()->sendToPriority(ui->espList->selectionModel(), newPriority); -} - - -void MainWindow::enableSelectedMods_clicked() -{ - m_OrganizerCore.modList()->enableSelected(ui->modList->selectionModel()); - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } -} - - -void MainWindow::disableSelectedMods_clicked() -{ - m_OrganizerCore.modList()->disableSelected(ui->modList->selectionModel()); - if (m_ModListSortProxy != nullptr) { - m_ModListSortProxy->invalidate(); - } -} - void MainWindow::updateAvailable() { ui->actionUpdate->setEnabled(true); @@ -5124,7 +2647,6 @@ void MainWindow::updateAvailable() ui->statusBar->setUpdateAvailable(true); } - void MainWindow::motdReceived(const QString &motd) { // don't show motd after 5 seconds, may be annoying. Hopefully the user's @@ -5180,24 +2702,6 @@ void MainWindow::actionWontEndorseMO() } } -void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs) -{ - if (m_OrganizerCore.settings().network().offlineMode()) { - return; - } - - if (NexusInterface::instance().getAccessManager()->validated()) { - ModInfo::manualUpdateCheck(this, IDs); - } else { - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); - NexusInterface::instance().getAccessManager()->apiCheck(apiKey); - } else - log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.")); - } -} - void MainWindow::toggleMO2EndorseState() { const auto& s = m_OrganizerCore.settings(); @@ -5318,8 +2822,6 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa return std::make_pair(gameNameReal, ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true)); }); watcher->setFuture(future); - if (m_ModListSortProxy != nullptr) - m_ModListSortProxy->invalidate(); } void MainWindow::finishUpdateInfo() @@ -5338,6 +2840,7 @@ void MainWindow::finishUpdateInfo() if (mod->canBeUpdated()) { organizedGames.insert(std::make_pair<QString, int>(mod->gameName().toLower(), mod->nexusId())); } + m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name())); } if (!finalMods.empty() && organizedGames.empty()) @@ -5422,8 +2925,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (foundUpdate) { // Just get the standard data updates for endorsements and descriptions mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); - if (m_ModListSortProxy != nullptr) - m_ModListSortProxy->invalidate(); + m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name())); } else { // Scrape mod data here so we can use the mod version if no file update was located requiresInfo = true; @@ -5437,7 +2939,6 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { QVariantMap result = resultData.toMap(); - bool foundUpdate = false; QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins<IPluginGame>()) { if (game->gameNexusName() == gameName) { @@ -5447,6 +2948,7 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD } std::vector<ModInfo::Ptr> modsList = ModInfo::getByModID(gameNameReal, modID); for (auto mod : modsList) { + bool foundUpdate = false; QDateTime now = QDateTime::currentDateTimeUtc(); QDateTime updateTarget = mod->getExpires(); if (now >= updateTarget) { @@ -5473,9 +2975,11 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD mod->setLastNexusQuery(QDateTime::currentDateTimeUtc()); mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC)); mod->saveMeta(); + + if (foundUpdate) { + m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name())); + } } - if (foundUpdate && m_ModListSortProxy != nullptr) - m_ModListSortProxy->invalidate(); } void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) @@ -5582,7 +3086,6 @@ void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultDat m_OrganizerCore.settings().network().updateServers(servers); } - void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString) { if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) { @@ -5606,7 +3109,6 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in } } - BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &progress) { @@ -5657,11 +3159,10 @@ bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std: } -void MainWindow::extractBSATriggered() +void MainWindow::extractBSATriggered(QTreeWidgetItem* item) { using namespace boost::placeholders; - QTreeWidgetItem *item = m_ContextItem; QString origin; QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA")); @@ -5700,14 +3201,11 @@ void MainWindow::extractBSATriggered() } } -void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos) +void MainWindow::on_bsaList_customContextMenuRequested(const QPoint& pos) { - m_ContextItem = ui->bsaList->itemAt(pos); - -// m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos)); - QMenu menu; - menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered())); + menu.addAction(tr("Extract..."), + [=, item = ui->bsaList->itemAt(pos)]() { extractBSATriggered(item); }); menu.exec(ui->bsaList->viewport()->mapToGlobal(pos)); } @@ -5752,101 +3250,9 @@ void MainWindow::on_displayCategoriesBtn_toggled(bool checked) setCategoryListVisible(checked); } -void MainWindow::deselectFilters() -{ - m_Filters->clearSelection(); -} - -void MainWindow::refreshFilters() -{ - QItemSelection currentSelection = ui->modList->selectionModel()->selection(); - - int idxRow = ui->modList->currentIndex().row(); - QVariant currentIndexName = ui->modList->model()->index(idxRow, 0).data(); - ui->modList->setCurrentIndex(QModelIndex()); - - m_Filters->refresh(); - - ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select); - - QModelIndexList matchList; - if (currentIndexName.isValid()) { - matchList = ui->modList->model()->match( - ui->modList->model()->index(0, 0), - Qt::DisplayRole, - currentIndexName); - } - - if (matchList.size() > 0) { - ui->modList->setCurrentIndex(matchList.at(0)); - } -} - -void MainWindow::onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& criteria) -{ - m_ModListSortProxy->setCriteria(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_OrganizerCore.modDataContents().findById(c.id); - label = content ? content->name() : QString(); - } else { - label = m_CategoryFactory.getCategoryNameByID(c.id); - } - - if (label.isEmpty()) { - log::error("category {}:{} not found", c.type, c.id); - } - } else { - label = tr("<Multiple>"); - } - - ui->currentCategoryLabel->setText(label); - ui->modList->reset(); -} - -void MainWindow::onFiltersOptions( - ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep) -{ - m_ModListSortProxy->setOptions(mode, sep); -} - -void MainWindow::updateESPLock(bool locked) -{ - QItemSelection currentSelection = ui->espList->selectionModel()->selection(); - if (currentSelection.count() == 0) { - // this path is probably useless - m_OrganizerCore.pluginList()->lockESPIndex(m_ContextRow, locked); - } else { - Q_FOREACH (const QModelIndex &idx, currentSelection.indexes()) { - if (m_OrganizerCore.pluginList()->isEnabled(mapToModel(m_OrganizerCore.pluginList(), idx).row())) { - m_OrganizerCore.pluginList()->lockESPIndex(mapToModel(m_OrganizerCore.pluginList(), idx).row(), locked); - } - } - } -} - - -void MainWindow::lockESPIndex() -{ - updateESPLock(true); -} - -void MainWindow::unlockESPIndex() -{ - updateESPLock(false); -} - - -void MainWindow::removeFromToolbar() +void MainWindow::removeFromToolbar(QAction* action) { - const auto& title = m_ContextAction->text(); + const auto& title = action->text(); auto& list = *m_OrganizerCore.executablesList(); auto itor = list.find(title); @@ -5866,9 +3272,10 @@ void MainWindow::toolBar_customContextMenuRequested(const QPoint &point) if (action != nullptr) { if (action->objectName().startsWith("custom_")) { - m_ContextAction = action; QMenu menu; - menu.addAction(tr("Remove '%1' from the toolbar").arg(action->text()), this, SLOT(removeFromToolbar())); + menu.addAction( + tr("Remove '%1' from the toolbar").arg(action->text()), + [&, action]() { removeFromToolbar(action); }); menu.exec(ui->toolBar->mapToGlobal(point)); return; } @@ -5879,105 +3286,6 @@ void MainWindow::toolBar_customContextMenuRequested(const QPoint &point) m->exec(ui->toolBar->mapToGlobal(point)); } -void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) -{ - m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row(); - - QMenu menu; - menu.addAction(tr("Enable selected"), this, SLOT(enableSelectedPlugins_clicked())); - menu.addAction(tr("Disable selected"), this, SLOT(disableSelectedPlugins_clicked())); - - menu.addSeparator(); - - menu.addAction(tr("Enable all"), m_OrganizerCore.pluginList(), SLOT(enableAll())); - menu.addAction(tr("Disable all"), m_OrganizerCore.pluginList(), SLOT(disableAll())); - - menu.addSeparator(); - - addPluginSendToContextMenu(&menu); - - QItemSelection currentSelection = ui->espList->selectionModel()->selection(); - bool hasLocked = false; - bool hasUnlocked = false; - for (const QModelIndex &idx : currentSelection.indexes()) { - int row = m_PluginListSortProxy->mapToSource(idx).row(); - if (m_OrganizerCore.pluginList()->isEnabled(row)) { - if (m_OrganizerCore.pluginList()->isESPLocked(row)) { - hasLocked = true; - } else { - hasUnlocked = true; - } - } - } - - if (hasLocked) { - menu.addAction(tr("Unlock load order"), this, SLOT(unlockESPIndex())); - } - if (hasUnlocked) { - menu.addAction(tr("Lock load order"), this, SLOT(lockESPIndex())); - } - - menu.addSeparator(); - - - QModelIndex idx = ui->espList->selectionModel()->currentIndex(); - unsigned int modInfoIndex = ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(idx.data().toString())); - //this is to avoid showing the option on game files like skyrim.esm - if (modInfoIndex != UINT_MAX) { - menu.addAction(tr("Open Origin in Explorer"), this, SLOT(openPluginOriginExplorer_clicked())); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end()) { - QAction *infoAction = menu.addAction(tr("Open Origin Info..."), this, SLOT(openOriginInformation_clicked())); - menu.setDefaultAction(infoAction); - } - } - - try { - menu.exec(ui->espList->viewport()->mapToGlobal(pos)); - } catch (const std::exception &e) { - reportError(tr("Exception: ").arg(e.what())); - } catch (...) { - reportError(tr("Unknown exception")); - } -} - -void MainWindow::on_groupCombo_currentIndexChanged(int index) -{ - if (m_ModListSortProxy == nullptr) { - return; - } - QAbstractProxyModel *newModel = nullptr; - switch (index) { - case 1: { - newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_CATEGORY, Qt::UserRole, - 0, Qt::UserRole + 2); - } break; - case 2: { - newModel = new QtGroupingProxy(m_OrganizerCore.modList(), QModelIndex(), ModList::COL_MODID, Qt::DisplayRole, - QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE, - Qt::UserRole + 2); - } break; - default: { - newModel = nullptr; - } break; - } - - if (newModel != nullptr) { -#ifdef TEST_MODELS - new ModelTest(newModel, this); -#endif // TEST_MODELS - m_ModListSortProxy->setSourceModel(newModel); - connect(ui->modList, SIGNAL(expanded(QModelIndex)),newModel, SLOT(expanded(QModelIndex))); - connect(ui->modList, SIGNAL(collapsed(QModelIndex)), newModel, SLOT(collapsed(QModelIndex))); - connect(newModel, SIGNAL(expandItem(QModelIndex)), this, SLOT(expandModList(QModelIndex))); - } else { - m_ModListSortProxy->setSourceModel(m_OrganizerCore.modList()); - } - modFilterActive(m_ModListSortProxy->isFilterActive()); -} - Executable* MainWindow::getSelectedExecutable() { const QString name = ui->executablesListBox->itemText( @@ -5998,55 +3306,10 @@ void MainWindow::on_showHiddenBox_toggled(bool checked) m_OrganizerCore.downloadManager()->setShowHidden(checked); } -void MainWindow::on_bossButton_clicked() -{ - const bool offline = m_OrganizerCore.settings().network().offlineMode(); - - auto r = QMessageBox::No; - - if (offline) { - r = QMessageBox::question( - this, tr("Sorting plugins"), - tr("Are you sure you want to sort your plugins list?") + "\r\n\r\n" + - tr("Note: You are currently in offline mode and LOOT will not update the master list."), - QMessageBox::Yes | QMessageBox::No); - } else { - r = QMessageBox::question( - this, tr("Sorting plugins"), - tr("Are you sure you want to sort your plugins list?"), - QMessageBox::Yes | QMessageBox::No); - } - - if (r != QMessageBox::Yes) { - return; - } - - - m_OrganizerCore.savePluginList(); - - setEnabled(false); - ON_BLOCK_EXIT([&] () { setEnabled(true); }); - - // don't try to update the master list in offline mode - const bool didUpdateMasterList = offline ? true : m_DidUpdateMasterList; - - if (runLoot(this, m_OrganizerCore, didUpdateMasterList)) { - // don't assume the master list was updated in offline mode - if (!offline) { - m_DidUpdateMasterList = true; - } - - m_OrganizerCore.refreshESPList(false); - m_OrganizerCore.savePluginList(); - } -} - - const char *MainWindow::PATTERN_BACKUP_GLOB = ".????_??_??_??_??_??"; const char *MainWindow::PATTERN_BACKUP_REGEX = "\\.(\\d\\d\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d)"; const char *MainWindow::PATTERN_BACKUP_DATE = "yyyy_MM_dd_hh_mm_ss"; - bool MainWindow::createBackup(const QString &filePath, const QDateTime &time) { QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE); @@ -6274,96 +3537,3 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event) QMainWindow::keyReleaseEvent(event); } - -void MainWindow::on_clearFiltersButton_clicked() -{ - ui->modFilterEdit->clear(); - deselectFilters(); -} - -void MainWindow::sendSelectedModsToPriority(int newPriority) -{ - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - std::vector<int> modsToMove; - for (auto idx : selection->selectedRows(ModList::COL_PRIORITY)) { - modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt())); - } - m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority); - } else { - m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority); - } -} - -void MainWindow::sendSelectedModsToTop_clicked() -{ - sendSelectedModsToPriority(0); -} - -void MainWindow::sendSelectedModsToBottom_clicked() -{ - sendSelectedModsToPriority(INT_MAX); -} - -void MainWindow::sendSelectedModsToPriority_clicked() -{ - bool ok; - int newPriority = QInputDialog::getInt(this, - tr("Set Priority"), tr("Set the priority of the selected mods"), - 0, 0, INT_MAX, 1, &ok); - if (!ok) return; - - sendSelectedModsToPriority(newPriority); -} - -void MainWindow::sendSelectedModsToSeparator_clicked() -{ - QStringList separators; - auto indexesByPriority = m_OrganizerCore.currentProfile()->getAllIndexesByPriority(); - for (auto iter = indexesByPriority.begin(); iter != indexesByPriority.end(); iter++) { - if ((iter->second != UINT_MAX)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second); - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) { - separators << modInfo->name().chopped(10); // Chops the "_separator" away from the name - } - } - } - - ListDialog dialog(this); - dialog.setWindowTitle("Select a separator..."); - dialog.setChoices(separators); - - if (dialog.exec() == QDialog::Accepted) { - QString result = dialog.getChoice(); - if (!result.isEmpty()) { - result += "_separator"; - - int newPriority = INT_MAX; - bool foundSection = false; - for (auto mod : m_OrganizerCore.modsSortedByProfilePriority(m_OrganizerCore.currentProfile())) { - unsigned int modIndex = ModInfo::getIndex(mod); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (!foundSection && result.compare(mod) == 0) { - foundSection = true; - } else if (foundSection && modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) { - newPriority = m_OrganizerCore.currentProfile()->getModPriority(modIndex); - break; - } - } - - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1) { - std::vector<int> modsToMove; - for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) { - modsToMove.push_back(m_OrganizerCore.currentProfile()->modIndexByPriority(idx.data().toInt())); - } - m_OrganizerCore.modList()->changeModPriority(modsToMove, newPriority); - } else { - int oldPriority = m_OrganizerCore.currentProfile()->getModPriority(m_ContextRow); - if (oldPriority < newPriority) - --newPriority; - m_OrganizerCore.modList()->changeModPriority(m_ContextRow, newPriority); - } - } - } -} diff --git a/src/mainwindow.h b/src/mainwindow.h index f8910361..6ead7a77 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -26,6 +26,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "imoinfo.h" #include "iuserinterface.h" #include "modinfo.h" +#include "modlistbypriorityproxy.h" #include "modlistsortproxy.h" #include "tutorialcontrol.h" #include "plugincontainer.h" //class PluginContainer; @@ -123,13 +124,8 @@ public: bool addProfile(); void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives); - void setModListSorting(int index); - void setESPListSorting(int index); - void saveArchiveList(); - void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info); - void installTranslator(const QString &name); void displayModInformation( @@ -143,12 +139,7 @@ public: virtual MOBase::DelayedFileWriterBase &archivesWriter() override { return m_ArchiveListWriter; } - ModInfo::Ptr nextModInList(); - ModInfo::Ptr previousModInList(); - public slots: - void modorder_changed(); - void esplist_changed(); void refresherProgress(const DirectoryRefreshProgress* p); void directory_refreshed(); @@ -156,28 +147,21 @@ public slots: signals: /** - * @brief emitted after the information dialog has been closed - */ - void modInfoDisplayed(); - - /** * @brief emitted when the selected style changes */ void styleChanged(const QString &styleFile); - void modListDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); - void checkForProblemsDone(); protected: - virtual void showEvent(QShowEvent *event); - virtual void paintEvent(QPaintEvent* event); - virtual void closeEvent(QCloseEvent *event); - virtual bool eventFilter(QObject *obj, QEvent *event); - virtual void resizeEvent(QResizeEvent *event); - virtual void dragEnterEvent(QDragEnterEvent *event); - virtual void dropEvent(QDropEvent *event); + void showEvent(QShowEvent *event) override; + void paintEvent(QPaintEvent* event) override; + void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private slots: @@ -209,29 +193,8 @@ private: bool refreshProfiles(bool selectProfile = true); void refreshExecutablesList(); - void installMod(QString fileName = ""); bool modifyExecutablesDialog(int selection); - void displayModInformation(int row, ModInfoTabIDs tab=ModInfoTabIDs::None); - - /** - * Sets category selections from menu; for multiple mods, this will only apply - * the changes made in the menu (which is the delta between the current menu selection and the reference mod) - * @param menu the menu after editing by the user - * @param modRow index of the mod to edit - * @param referenceRow row of the reference mod - */ - void addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow); - - /** - * Sets category selections from menu; for multiple mods, this will completely - * replace the current set of categories on each selected with those selected in the menu - * @param menu the menu after editing by the user - * @param modRow index of the mod to edit - */ - void replaceCategoriesFromMenu(QMenu *menu, int modRow); - - bool populateMenuCategories(QMenu *menu, int targetID); // remove invalid category-references from mods void fixCategories(); @@ -245,24 +208,16 @@ private: bool errorReported(QString &logFile); - void updateESPLock(bool locked); - static void setupNetworkProxy(bool activate); void activateProxy(bool activate); bool createBackup(const QString &filePath, const QDateTime &time); QString queryRestore(const QString &filePath); - void initModListContextMenu(QMenu *menu); - void addModSendToContextMenu(QMenu *menu); - void addPluginSendToContextMenu(QMenu *menu); - QMenu *openFolderMenu(); void dropLocalFile(const QUrl &url, const QString &outputDir, bool move); - void sendSelectedModsToPriority(int newPriority); - void toggleMO2EndorseState(); void toggleUpdateAction(); @@ -285,7 +240,6 @@ private: MOBase::TutorialControl m_Tutorial; - std::unique_ptr<FilterList> m_Filters; std::unique_ptr<DataTab> m_DataTab; std::unique_ptr<DownloadsTab> m_DownloadsTab; std::unique_ptr<SavesTab> m_SavesTab; @@ -296,15 +250,8 @@ private: QStringList m_DefaultArchives; - ModListSortProxy *m_ModListSortProxy; - - PluginListSortProxy *m_PluginListSortProxy; - int m_OldExecutableIndex; - int m_ContextRow; - QPersistentModelIndex m_ContextIdx; - QTreeWidgetItem *m_ContextItem; QAction *m_ContextAction; CategoryFactory &m_CategoryFactory; @@ -327,8 +274,6 @@ private: QByteArray m_ArchiveListHash; - bool m_DidUpdateMasterList; - MOBase::DelayedFileWriter m_ArchiveListWriter; QAction* m_LinkToolbar; @@ -357,53 +302,10 @@ private slots: void wikiTriggered(); void discordTriggered(); void tutorialTriggered(); - void extractBSATriggered(); + void extractBSATriggered(QTreeWidgetItem* item); - //modlist shortcuts - void openExplorer_activated(); void refreshProfile_activated(); - // modlist context menu - void installMod_clicked(); - void createEmptyMod_clicked(); - void createSeparator_clicked(); - void restoreBackup_clicked(); - void renameMod_clicked(); - void removeMod_clicked(); - void setColor_clicked(); - void resetColor_clicked(); - void backupMod_clicked(); - void reinstallMod_clicked(); - void endorse_clicked(); - void dontendorse_clicked(); - void unendorse_clicked(); - void track_clicked(); - void untrack_clicked(); - void ignoreMissingData_clicked(); - void markConverted_clicked(); - void restoreHiddenFiles_clicked(); - void visitOnNexus_clicked(); - void visitWebPage_clicked(); - void visitNexusOrWebPage_clicked(); - void openExplorer_clicked(); - void openPluginOriginExplorer_clicked(); - void openOriginInformation_clicked(); - void information_clicked(); - void enableSelectedMods_clicked(); - void disableSelectedMods_clicked(); - void sendSelectedModsToTop_clicked(); - void sendSelectedModsToBottom_clicked(); - void sendSelectedModsToPriority_clicked(); - void sendSelectedModsToSeparator_clicked(); - // data-tree context menu - - // pluginlist context menu - void enableSelectedPlugins_clicked(); - void disableSelectedPlugins_clicked(); - void sendSelectedPluginsToTop_clicked(); - void sendSelectedPluginsToBottom_clicked(); - void sendSelectedPluginsToPriority_clicked(); - void linkToolbar(); void linkDesktop(); void linkMenu(); @@ -414,22 +316,7 @@ private slots: BSA::EErrorCode extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &extractProgress); - void createModFromOverwrite(); - /** - * @brief sends the content of the overwrite folder to an already existing mod - */ - void moveOverwriteContentToExistingMod(); - /** - * @brief actually sends the content of the overwrite folder to specified mod - */ - void doMoveOverwriteContentToMod(const QString &modAbsolutePath); - void clearOverwrite(); - // nexus related - void checkModsForUpdates(); - - void linkClicked(const QString &url); - void updateAvailable(); void actionEndorseMO(); @@ -439,15 +326,8 @@ private slots: void originModified(int originID); - void addRemoveCategories_MenuHandler(); - void replaceCategories_MenuHandler(); - - void addPrimaryCategoryCandidates(); - void modInstalled(const QString &modName); - void modUpdateCheck(std::multimap<QString, int> IDs); - void finishUpdateInfo(); void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int); @@ -461,32 +341,12 @@ private slots: void onRequestsChanged(const APIStats& stats, const APIUserAccount& user); - void deselectFilters(); - void refreshFilters(); - void onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& filters); - void onFiltersOptions( - ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep); - - void displayModInformation(const QString &modName, ModInfoTabIDs tabID); - void visitNexusOrWebPage(const QModelIndex& idx); - void modRenamed(const QString &oldName, const QString &newName); void modRemoved(const QString &fileName); void hookUpWindowTutorials(); bool shouldStartTutorial() const; - void endorseMod(ModInfo::Ptr mod); - void unendorseMod(ModInfo::Ptr mod); - void trackMod(ModInfo::Ptr mod, bool doTrack); - void cancelModListEditor(); - - void lockESPIndex(); - void unlockESPIndex(); - - void enableVisibleMods(); - void disableVisibleMods(); - void exportModListCSV(); void openInstanceFolder(); void openLogsFolder(); void openInstallFolder(); @@ -515,43 +375,21 @@ private slots: void updateStyle(const QString &style); - void modlistChanged(const QModelIndex &index, int role); - void modlistChanged(const QModelIndexList &indicies, int role); - void fileMoved(const QString &filePath, const QString &oldOriginName, const QString &newOriginName); - - - void modFilterActive(bool active); - void espFilterChanged(const QString &filter); - - void expandModList(const QModelIndex &index); - void resizeLists(bool pluginListCustom); + void fileMoved(const QString& filePath, const QString& oldOriginName, const QString& newOriginName); + /** * @brief allow columns in mod list and plugin list to be resized */ void allowListResize(); void toolBar_customContextMenuRequested(const QPoint &point); - void removeFromToolbar(); - void overwriteClosed(int); - - void changeVersioningScheme(); - void checkModUpdates_clicked(); - void ignoreUpdate(); - void unignoreUpdate(); + void removeFromToolbar(QAction* action); void about(); - void modListSortIndicatorChanged(int column, Qt::SortOrder order); - void modListSectionResized(int logicalIndex, int oldSize, int newSize); - - void modlistSelectionsChanged(const QItemSelection ¤t); - void esplistSelectionsChanged(const QItemSelection ¤t); - void resetActionIcons(); - void updateModCount(); - void updatePluginCount(); private slots: // ui slots // actions @@ -577,23 +415,15 @@ private slots: // ui slots void on_centralWidget_customContextMenuRequested(const QPoint &pos); void on_bsaList_customContextMenuRequested(const QPoint &pos); - void on_clearFiltersButton_clicked(); void on_executablesListBox_currentIndexChanged(int index); - void on_modList_customContextMenuRequested(const QPoint &pos); - void on_modList_doubleClicked(const QModelIndex &index); - void on_listOptionsBtn_pressed(); - void on_espList_doubleClicked(const QModelIndex &index); void on_profileBox_currentIndexChanged(int index); void on_startButton_clicked(); void on_tabWidget_currentChanged(int index); - void on_espList_customContextMenuRequested(const QPoint &pos); void on_displayCategoriesBtn_toggled(bool checked); - void on_groupCombo_currentIndexChanged(int index); void on_linkButton_pressed(); void on_showHiddenBox_toggled(bool checked); void on_bsaList_itemChanged(QTreeWidgetItem *item, int column); - void on_bossButton_clicked(); void on_saveButton_clicked(); void on_restoreButton_clicked(); @@ -605,6 +435,7 @@ private slots: // ui slots void storeSettings(); void readSettings(); + void setupModList(); }; diff --git a/src/moapplication.cpp b/src/moapplication.cpp index fb6a7121..709bcbda 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -56,40 +56,53 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase;
using namespace MOShared;
+// style proxy that changes the appearance of drop indicators
+//
class ProxyStyle : public QProxyStyle {
public:
- ProxyStyle(QStyle *baseStyle = 0)
+ ProxyStyle(QStyle* baseStyle = 0)
: QProxyStyle(baseStyle)
{
}
- void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const {
- if(element == QStyle::PE_IndicatorItemViewItemDrop) {
+ void drawPrimitive(PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const {
+ if (element == QStyle::PE_IndicatorItemViewItemDrop) {
+
+ // 1. full-width drop indicator
+ QRect rect(option->rect);
+ if (auto* view = qobject_cast<const QTreeView*>(widget)) {
+ rect.setLeft(view->indentation());
+ rect.setRight(widget->width());
+ }
+
+ // 2. stylish drop indicator
painter->setRenderHint(QPainter::Antialiasing, true);
QColor col(option->palette.windowText().color());
QPen pen(col);
pen.setWidth(2);
col.setAlpha(50);
- QBrush brush(col);
painter->setPen(pen);
- painter->setBrush(brush);
- if(option->rect.height() == 0) {
+ painter->setBrush(QBrush(col));
+ if (rect.height() == 0) {
QPoint tri[3] = {
- option->rect.topLeft(),
- option->rect.topLeft() + QPoint(-5, 5),
- option->rect.topLeft() + QPoint(-5, -5)
+ rect.topLeft(),
+ rect.topLeft() + QPoint(-5, 5),
+ rect.topLeft() + QPoint(-5, -5)
};
painter->drawPolygon(tri, 3);
- painter->drawLine(QPoint(option->rect.topLeft().x(), option->rect.topLeft().y()), option->rect.topRight());
- } else {
- painter->drawRoundedRect(option->rect, 5, 5);
+ painter->drawLine(QPoint(rect.topLeft().x(), rect.topLeft().y()), rect.topRight());
}
- } else {
+ else {
+ painter->drawRoundedRect(rect, 5, 5);
+ }
+ }
+ else {
QProxyStyle::drawPrimitive(element, option, painter, widget);
}
}
+
};
@@ -525,8 +538,8 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) void MOApplication::updateStyle(const QString& fileName)
{
if (QStyleFactory::keys().contains(fileName)) {
- setStyle(QStyleFactory::create(fileName));
setStyleSheet("");
+ setStyle(new ProxyStyle(QStyleFactory::create(fileName)));
} else {
setStyle(new ProxyStyle(QStyleFactory::create(m_DefaultStyle)));
if (QFile::exists(fileName)) {
diff --git a/src/modconflicticondelegate.cpp b/src/modconflicticondelegate.cpp index 2ccf3363..73c47a03 100644 --- a/src/modconflicticondelegate.cpp +++ b/src/modconflicticondelegate.cpp @@ -1,23 +1,14 @@ #include "modconflicticondelegate.h" +#include "modlist.h" +#include "modlistview.h" #include <log.h> #include <QList> using namespace MOBase; -ModInfo::EConflictFlag ModConflictIconDelegate::m_ConflictFlags[4] = { ModInfo::FLAG_CONFLICT_MIXED - , ModInfo::FLAG_CONFLICT_OVERWRITE - , ModInfo::FLAG_CONFLICT_OVERWRITTEN - , ModInfo::FLAG_CONFLICT_REDUNDANT }; - -ModInfo::EConflictFlag ModConflictIconDelegate::m_ArchiveLooseConflictFlags[2] = { ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE - , ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN }; - -ModInfo::EConflictFlag ModConflictIconDelegate::m_ArchiveConflictFlags[3] = { ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED - , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE - , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN }; - -ModConflictIconDelegate::ModConflictIconDelegate(QObject *parent, int logicalIndex, int compactSize) - : IconDelegate(parent) +ModConflictIconDelegate::ModConflictIconDelegate(ModListView* view, int logicalIndex, int compactSize) + : IconDelegate(view) + , m_View(view) , m_LogicalIndex(logicalIndex) , m_CompactSize(compactSize) , m_Compact(false) @@ -37,13 +28,13 @@ QList<QString> ModConflictIconDelegate::getIconsForFlags( QList<QString> result; // Don't do flags for overwrite - if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE_CONFLICT) != flags.end()) + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE_CONFLICT) != flags.end()) return result; // insert conflict icons to provide nicer alignment { // insert loose file conflicts first auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ConflictFlags, m_ConflictFlags + 4); + s_ConflictFlags.begin(), s_ConflictFlags.end()); if (iter != flags.end()) { result.append(getFlagIcon(*iter)); flags.erase(iter); @@ -64,8 +55,8 @@ QList<QString> ModConflictIconDelegate::getIconsForFlags( } { // insert loose vs archive overwritten third - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2); + auto iter = std::find(flags.begin(), flags.end(), + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN); if (iter != flags.end()) { result.append(getFlagIcon(*iter)); flags.erase(iter); @@ -76,7 +67,7 @@ QList<QString> ModConflictIconDelegate::getIconsForFlags( { // insert archive conflicts last auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3); + s_ArchiveConflictFlags.begin(), s_ArchiveConflictFlags.end()); if (iter != flags.end()) { result.append(getFlagIcon(*iter)); flags.erase(iter); @@ -96,14 +87,15 @@ QList<QString> ModConflictIconDelegate::getIconsForFlags( QList<QString> ModConflictIconDelegate::getIcons(const QModelIndex &index) const { - QVariant modid = index.data(Qt::UserRole + 1); + QVariant modIndex = index.data(ModList::IndexRole); - if (modid.isValid()) { - ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt()); - return getIconsForFlags(info->getConflictFlags(), m_Compact); + if (!modIndex.isValid()) { + return {}; } - return {}; + bool compact; + auto flags = m_View->conflictFlags(index, &compact); + return getIconsForFlags(flags, compact || m_Compact); } QString ModConflictIconDelegate::getFlagIcon(ModInfo::EConflictFlag flag) @@ -127,12 +119,13 @@ QString ModConflictIconDelegate::getFlagIcon(ModInfo::EConflictFlag flag) size_t ModConflictIconDelegate::getNumIcons(const QModelIndex &index) const { - unsigned int modIdx = index.data(Qt::UserRole + 1).toInt(); + unsigned int modIdx = index.data(ModList::IndexRole).toInt(); if (modIdx < ModInfo::getNumMods()) { ModInfo::Ptr info = ModInfo::getByIndex(modIdx); std::vector<ModInfo::EConflictFlag> flags = info->getConflictFlags(); size_t count = flags.size(); - if (std::find_first_of(flags.begin(), flags.end(), m_ConflictFlags, m_ConflictFlags + 4) == flags.end()) { + if (std::find_first_of(flags.begin(), flags.end(), + s_ConflictFlags.begin(), s_ConflictFlags.end()) == flags.end()) { ++count; } return count; @@ -141,11 +134,10 @@ size_t ModConflictIconDelegate::getNumIcons(const QModelIndex &index) const } } - QSize ModConflictIconDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &modelIndex) const { size_t count = getNumIcons(modelIndex); - unsigned int index = modelIndex.data(Qt::UserRole + 1).toInt(); + unsigned int index = modelIndex.data(ModList::IndexRole).toInt(); QSize result; if (index < ModInfo::getNumMods()) { result = QSize(static_cast<int>(count) * 40, 20); @@ -157,4 +149,3 @@ QSize ModConflictIconDelegate::sizeHint(const QStyleOptionViewItem &option, cons } return result; } - diff --git a/src/modconflicticondelegate.h b/src/modconflicticondelegate.h index 8645da12..a44ce89b 100644 --- a/src/modconflicticondelegate.h +++ b/src/modconflicticondelegate.h @@ -1,33 +1,49 @@ #ifndef MODCONFLICTICONDELEGATE_H #define MODCONFLICTICONDELEGATE_H +#include <array> + #include "icondelegate.h" +class ModListView; + class ModConflictIconDelegate : public IconDelegate { Q_OBJECT; public: - explicit ModConflictIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 80); - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; - - static QList<QString> getIconsForFlags( - std::vector<ModInfo::EConflictFlag> flags, bool compact); - - static QString getFlagIcon(ModInfo::EConflictFlag flag); + explicit ModConflictIconDelegate(ModListView* parent = nullptr, int logicalIndex = -1, int compactSize = 80); + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex &index) const override; public slots: void columnResized(int logicalIndex, int oldSize, int newSize); protected: - virtual QList<QString> getIcons(const QModelIndex &index) const; - virtual size_t getNumIcons(const QModelIndex &index) const; + + static QList<QString> getIconsForFlags(std::vector<ModInfo::EConflictFlag> flags, bool compact); + static QString getFlagIcon(ModInfo::EConflictFlag flag); + + QList<QString> getIcons(const QModelIndex &index) const override; + size_t getNumIcons(const QModelIndex &index) const override; private: - static ModInfo::EConflictFlag m_ConflictFlags[4]; - static ModInfo::EConflictFlag m_ArchiveLooseConflictFlags[2]; - static ModInfo::EConflictFlag m_ArchiveConflictFlags[3]; + static constexpr std::array s_ConflictFlags{ + ModInfo::FLAG_CONFLICT_MIXED, + ModInfo::FLAG_CONFLICT_OVERWRITE, + ModInfo::FLAG_CONFLICT_OVERWRITTEN, + ModInfo::FLAG_CONFLICT_REDUNDANT + }; + static constexpr std::array s_ArchiveLooseConflictFlags{ + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN + }; + static constexpr std::array s_ArchiveConflictFlags{ + ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED, + ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE, + ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN + }; + ModListView* m_View; int m_LogicalIndex; int m_CompactSize; bool m_Compact; diff --git a/src/modelutils.cpp b/src/modelutils.cpp new file mode 100644 index 00000000..83054806 --- /dev/null +++ b/src/modelutils.cpp @@ -0,0 +1,93 @@ +#include "modelutils.h" + +#include <QAbstractProxyModel> + +namespace MOShared { + +QModelIndexList flatIndex( + const QAbstractItemModel* model, int column, const QModelIndex& parent) +{ + QModelIndexList index; + for (std::size_t i = 0; i < model->rowCount(parent); ++i) { + index.append(model->index(i, column, parent)); + index.append(flatIndex(model, column, index.back())); + } + return index; +} + +static QModelIndexList visibleIndexImpl(QTreeView* view, int column, const QModelIndex& parent) +{ + if (parent.isValid() && !view->isExpanded(parent)) { + return {}; + } + + auto* model = view->model(); + QModelIndexList index; + for (std::size_t i = 0; i < model->rowCount(parent); ++i) { + index.append(model->index(i, column, parent)); + index.append(visibleIndexImpl(view, column, index.back())); + } + return index; +} + +QModelIndexList visibleIndex(QTreeView* view, int column) +{ + return visibleIndexImpl(view, column, QModelIndex()); +} + +QModelIndex indexModelToView(const QModelIndex& index, const QAbstractItemView* view) +{ + // we need to stack the proxy + std::vector<QAbstractProxyModel*> proxies; + { + auto* currentModel = view->model(); + while (auto* proxy = qobject_cast<QAbstractProxyModel*>(currentModel)) { + proxies.push_back(proxy); + currentModel = proxy->sourceModel(); + } + } + + if (proxies.empty() || proxies.back()->sourceModel() != index.model()) { + return QModelIndex(); + } + + auto qindex = index; + for (auto rit = proxies.rbegin(); rit != proxies.rend(); ++rit) { + qindex = (*rit)->mapFromSource(qindex); + } + + return qindex; +} + +QModelIndexList indexModelToView(const QModelIndexList& index, const QAbstractItemView* view) +{ + QModelIndexList result; + for (auto& idx : index) { + result.append(indexModelToView(idx, view)); + } + return result; +} + +QModelIndex indexViewToModel(const QModelIndex& index, const QAbstractItemModel* model) +{ + if (index.model() == model) { + return index; + } + else if (auto* proxy = qobject_cast<const QAbstractProxyModel*>(index.model())) { + return indexViewToModel(proxy->mapToSource(index), model); + } + else { + return QModelIndex(); + } +} + +QModelIndexList indexViewToModel(const QModelIndexList& index, const QAbstractItemModel* model) +{ + QModelIndexList result; + for (auto& idx : index) { + result.append(indexViewToModel(idx, model)); + } + return result; +} + +} diff --git a/src/modelutils.h b/src/modelutils.h new file mode 100644 index 00000000..e6510941 --- /dev/null +++ b/src/modelutils.h @@ -0,0 +1,25 @@ +#ifndef MODELUTILS_H +#define MODELUTILS_H + +#include <QAbstractItemView> +#include <QAbstractItemModel> +#include <QColor> + +namespace MOShared { + +// retrieve all the row index under the given parent +QModelIndexList flatIndex( + const QAbstractItemModel* model, int column = 0, const QModelIndex& parent = QModelIndex()); + +// retrieve all the visible index in the given view +QModelIndexList visibleIndex(QTreeView* view, int column = 0); + +// convert back-and-forth through model proxies +QModelIndex indexModelToView(const QModelIndex& index, const QAbstractItemView* view); +QModelIndexList indexModelToView(const QModelIndexList& index, const QAbstractItemView* view); +QModelIndex indexViewToModel(const QModelIndex& index, const QAbstractItemModel* model); +QModelIndexList indexViewToModel(const QModelIndexList& index, const QAbstractItemModel* model); + +} + +#endif diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp index bec3c97d..3ac1085e 100644 --- a/src/modflagicondelegate.cpp +++ b/src/modflagicondelegate.cpp @@ -1,4 +1,5 @@ #include "modflagicondelegate.h"
+#include "modlist.h"
#include <log.h>
#include <QList>
@@ -39,7 +40,7 @@ QList<QString> ModFlagIconDelegate::getIconsForFlags( QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const
{
- QVariant modid = index.data(Qt::UserRole + 1);
+ QVariant modid = index.data(ModList::IndexRole);
if (modid.isValid()) {
ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt());
@@ -71,7 +72,7 @@ QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) size_t ModFlagIconDelegate::getNumIcons(const QModelIndex &index) const
{
- unsigned int modIdx = index.data(Qt::UserRole + 1).toInt();
+ unsigned int modIdx = index.data(ModList::IndexRole).toInt();
if (modIdx < ModInfo::getNumMods()) {
ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
std::vector<ModInfo::EFlag> flags = info->getFlags();
@@ -85,7 +86,7 @@ size_t ModFlagIconDelegate::getNumIcons(const QModelIndex &index) const QSize ModFlagIconDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &modelIndex) const
{
size_t count = getNumIcons(modelIndex);
- unsigned int index = modelIndex.data(Qt::UserRole + 1).toInt();
+ unsigned int index = modelIndex.data(ModList::IndexRole).toInt();
QSize result;
if (index < ModInfo::getNumMods()) {
result = QSize(static_cast<int>(count) * 40, 20);
diff --git a/src/modflagicondelegate.h b/src/modflagicondelegate.h index ecab7e95..c6c7c3e8 100644 --- a/src/modflagicondelegate.h +++ b/src/modflagicondelegate.h @@ -9,7 +9,7 @@ class ModFlagIconDelegate : public IconDelegate public:
explicit ModFlagIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 120);
- virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
static QList<QString> getIconsForFlags(
std::vector<ModInfo::EFlag> flags, bool compact);
@@ -20,8 +20,8 @@ public slots: void columnResized(int logicalIndex, int oldSize, int newSize);
protected:
- virtual QList<QString> getIcons(const QModelIndex &index) const;
- virtual size_t getNumIcons(const QModelIndex &index) const;
+ QList<QString> getIcons(const QModelIndex &index) const override;
+ size_t getNumIcons(const QModelIndex &index) const override;
private:
int m_LogicalIndex;
diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 16fcf93a..c76cb4ad 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -27,6 +27,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "categories.h" #include "modinfodialog.h" +#include "organizercore.h" +#include "modlist.h" #include "overwriteinfodialog.h" #include "versioninfo.h" #include "thread_utils.h" @@ -37,6 +39,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <scriptextender.h> #include <unmanagedmods.h> #include <log.h> +#include <report.h> #include <QApplication> #include <QDirIterator> @@ -47,6 +50,7 @@ using namespace MOShared; std::vector<ModInfo::Ptr> ModInfo::s_Collection; +ModInfo::Ptr ModInfo::s_Overwrite; std::map<QString, unsigned int> ModInfo::s_ModsByName; std::map<std::pair<QString, int>, std::vector<unsigned int>> ModInfo::s_ModsByModID; int ModInfo::s_NextID; @@ -78,18 +82,19 @@ bool ModInfo::isRegularName(const QString& name) } -ModInfo::Ptr ModInfo::createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, DirectoryEntry **directoryStructure) +ModInfo::Ptr ModInfo::createFrom(const QDir &dir, OrganizerCore& core) { QMutexLocker locker(&s_Mutex); ModInfo::Ptr result; if (isBackupName(dir.dirName())) { - result = ModInfo::Ptr(new ModInfoBackup(pluginContainer, game, dir, directoryStructure)); + result = ModInfo::Ptr(new ModInfoBackup(dir, core)); } else if (isSeparatorName(dir.dirName())) { - result = Ptr(new ModInfoSeparator(pluginContainer, game, dir, directoryStructure)); + result = Ptr(new ModInfoSeparator(dir, core)); } else { - result = ModInfo::Ptr(new ModInfoRegular(pluginContainer, game, dir, directoryStructure)); + result = ModInfo::Ptr(new ModInfoRegular(dir, core)); } + result->m_Index = s_Collection.size(); s_Collection.push_back(result); return result; } @@ -98,23 +103,21 @@ ModInfo::Ptr ModInfo::createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, - const MOBase::IPluginGame* game, - DirectoryEntry **directoryStructure, - PluginContainer *pluginContainer) { + OrganizerCore& core) { QMutexLocker locker(&s_Mutex); - ModInfo::Ptr result = ModInfo::Ptr( - new ModInfoForeign(modName, espName, bsaNames, modType, game, directoryStructure, pluginContainer)); + ModInfo::Ptr result = ModInfo::Ptr(new ModInfoForeign(modName, espName, bsaNames, modType, core)); + result->m_Index = s_Collection.size(); s_Collection.push_back(result); return result; } -void ModInfo::createFromOverwrite(PluginContainer *pluginContainer, - const MOBase::IPluginGame* game, - MOShared::DirectoryEntry **directoryStructure) +ModInfo::Ptr ModInfo::createFromOverwrite(OrganizerCore& core) { QMutexLocker locker(&s_Mutex); - - s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite(pluginContainer, game, directoryStructure))); + ModInfo::Ptr overwrite = ModInfo::Ptr(new ModInfoOverwrite(core)); + overwrite->m_Index = s_Collection.size(); + s_Collection.push_back(overwrite); + return overwrite; } unsigned int ModInfo::getNumMods() @@ -174,10 +177,20 @@ bool ModInfo::removeMod(unsigned int index) QMutexLocker locker(&s_Mutex); if (index >= s_Collection.size()) { - throw MyException(tr("remove: invalid mod index %1").arg(index)); + throw Exception(tr("remove: invalid mod index %1").arg(index)); } - // update the indices first + ModInfo::Ptr modInfo = s_Collection[index]; + + // remove the actual mod (this is the most likely to fail so we do this first) + if (modInfo->isRegular()) { + if (!shellDelete(QStringList(modInfo->absolutePath()), true)) { + reportError(tr("remove: failed to delete mod '%1' directory").arg(modInfo->name())); + return false; + } + } + + // update the indices s_ModsByName.erase(s_ModsByName.find(modInfo->name())); auto iter = s_ModsByModID.find(std::pair<QString, int>(modInfo->gameName(), modInfo->nexusId())); @@ -187,12 +200,6 @@ bool ModInfo::removeMod(unsigned int index) s_ModsByModID[std::pair<QString, int>(modInfo->gameName(), modInfo->nexusId())] = indices; } - // physically remove the mod directory - //TODO the return value is ignored because the indices were already removed here, so stopping - // would cause data inconsistencies. Instead we go through with the removal but the mod will show up - // again if the user refreshes - modInfo->remove(); - // finally, remove the mod from the collection s_Collection.erase(s_Collection.begin() + index); @@ -225,28 +232,27 @@ unsigned int ModInfo::findMod(const boost::function<bool (ModInfo::Ptr)> &filter } -void ModInfo::updateFromDisc(const QString &modDirectory, - DirectoryEntry **directoryStructure, - PluginContainer *pluginContainer, - bool displayForeign, - std::size_t refreshThreadCount, - MOBase::IPluginGame const *game) +void ModInfo::updateFromDisc( + const QString& modsDirectory, OrganizerCore& core, + bool displayForeign, std::size_t refreshThreadCount) { TimeThis tt("ModInfo::updateFromDisc()"); QMutexLocker lock(&s_Mutex); s_Collection.clear(); s_NextID = 0; + s_Overwrite = nullptr; { // list all directories in the mod directory and make a mod out of each - QDir mods(QDir::fromNativeSeparators(modDirectory)); + QDir mods(QDir::fromNativeSeparators(modsDirectory)); mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QDirIterator modIter(mods); while (modIter.hasNext()) { - createFrom(pluginContainer, game, QDir(modIter.next()), directoryStructure); + createFrom(QDir(modIter.next()), core); } } + auto* game = core.managedGame(); UnmanagedMods *unmanaged = game->feature<UnmanagedMods>(); if (unmanaged != nullptr) { for (const QString &modName : unmanaged->mods(!displayForeign)) { @@ -256,14 +262,11 @@ void ModInfo::updateFromDisc(const QString &modDirectory, createFromPlugin(unmanaged->displayName(modName), unmanaged->referenceFile(modName).absoluteFilePath(), unmanaged->secondaryFiles(modName), - modType, - game, - directoryStructure, - pluginContainer); + modType, core); } } - createFromOverwrite(pluginContainer, game, directoryStructure); + s_Overwrite = createFromOverwrite(core); std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); @@ -283,14 +286,15 @@ void ModInfo::updateIndices() QString modName = s_Collection[i]->internalName(); QString game = s_Collection[i]->gameName(); int modID = s_Collection[i]->nexusId(); + s_Collection[i]->m_Index = i; s_ModsByName[modName] = i; s_ModsByModID[std::pair<QString, int>(game, modID)].push_back(i); } } -ModInfo::ModInfo(PluginContainer *pluginContainer) - : m_PrimaryCategory(-1) +ModInfo::ModInfo(OrganizerCore& core) + : m_PrimaryCategory(-1), m_Core(core) { } diff --git a/src/modinfo.h b/src/modinfo.h index 42abe51e..08ed94f8 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -23,6 +23,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "imodinterface.h" #include "versioninfo.h" +class OrganizerCore; class PluginContainer; class QDir; class QDateTime; @@ -107,12 +108,9 @@ public: // Static functions: /** * @brief Read the mod directory and Mod ModInfo objects for all subdirectories. */ - static void updateFromDisc(const QString &modDirectory, - MOShared::DirectoryEntry **directoryStructure, - PluginContainer *pluginContainer, - bool displayForeign, - std::size_t refreshThreadCount, - MOBase::IPluginGame const *game); + static void updateFromDisc( + const QString &modDirectory, OrganizerCore& core, + bool displayForeign, std::size_t refreshThreadCount); static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } @@ -178,6 +176,11 @@ public: // Static functions: static unsigned int getIndex(const QString &name); /** + * @brief Retrieve the overwrite mod. + */ + static ModInfo::Ptr getOverwrite() { return s_Overwrite; } + + /** * @brief Find the first mod that fulfills the filter function (after no particular order). * * @param filter A function to filter mods by. Should return true for a match. @@ -206,30 +209,6 @@ public: // Static functions: QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false); /** - * @brief Create a new mod from the specified directory and add it to the collection. - * - * @param dir Directory to create from. - * - * @return pointer to the info-structure of the newly created/added mod. - */ - static ModInfo::Ptr createFrom( - PluginContainer *pluginContainer, const MOBase::IPluginGame *game, - const QDir &dir, MOShared::DirectoryEntry **directoryStructure); - - /** - * @brief Create a new "foreign-managed" mod from a tuple of plugin and archives. - * - * @param espName Name of the plugin. - * @param bsaNames Names of archives. - * - * @return a new mod. - */ - static ModInfo::Ptr createFromPlugin( - const QString &modName, const QString &espName, const QStringList &bsaNames, - ModInfo::EModType modType, const MOBase::IPluginGame* game, - MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer); - - /** * @brief Check wheter a name corresponds to a separator or not, * * @return whether the given name is used for separators. @@ -379,6 +358,11 @@ public: // IModInterface implementations / Re-declaration virtual std::shared_ptr<const MOBase::IFileTree> fileTree() const = 0; /** + * @return true if this object represents a regular mod. + */ + virtual bool isRegular() const { return false; } + + /** * @return true if this object represents the overwrite mod. */ virtual bool isOverwrite() const { return false; } @@ -481,22 +465,9 @@ public: // Mutable operations: */ virtual bool setName(const QString& name) = 0; - /** - * @brief Deletes the mod from the disc. This does not update the global ModInfo structure or - * indices. - * - * @return true on success, false otherwise. - */ - virtual bool remove() = 0; - public: // Methods after this do not come from IModInterface: /** - * @return true if this mod is a regular mod, false otherwise. - */ - virtual bool isRegular() const { return false; } - - /** * @return true if this mod is empty, false otherwise. */ virtual bool isEmpty() const { return false; } @@ -964,7 +935,7 @@ protected: /** * */ - ModInfo(PluginContainer *pluginContainer); + ModInfo(OrganizerCore& core); /** * @brief Prefetch content for this mod. @@ -974,32 +945,59 @@ protected: * using multiple threads for all the mods. */ virtual void prefetch() = 0; - - static void updateIndices(); static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); protected: - static std::vector<ModInfo::Ptr> s_Collection; - static std::map<QString, unsigned int> s_ModsByName; + // the mod list + OrganizerCore& m_Core; + + // the index of the mod in s_Collection, only valid after updateIndices() + int m_Index; int m_PrimaryCategory; std::set<int> m_Categories; - MOBase::VersionInfo m_Version; - bool m_PluginSelected = false; -private: +protected: + + friend class OrganizerCore; + + /** + * @brief Create a new mod from the specified directory and add it to the collection. + * + * @param dir Directory to create from. + * + * @return pointer to the info-structure of the newly created/added mod. + */ + static ModInfo::Ptr createFrom(const QDir& dir, OrganizerCore& core); + + /** + * @brief Create a new "foreign-managed" mod from a tuple of plugin and archives. + * + * @param espName Name of the plugin. + * @param bsaNames Names of archives. + * + * @return a new mod. + */ + static ModInfo::Ptr createFromPlugin( + const QString& modName, const QString& espName, const QStringList& bsaNames, + ModInfo::EModType modType, OrganizerCore& core); + + static ModInfo::Ptr createFromOverwrite(OrganizerCore& core); - static void createFromOverwrite(PluginContainer* pluginContainer, - const MOBase::IPluginGame* game, - MOShared::DirectoryEntry** directoryStructure); + // update the m_Index attribute of all mods and the various mapping + // + static void updateIndices(); -private: +protected: static QMutex s_Mutex; - static std::map<std::pair<QString, int>, std::vector<unsigned int> > s_ModsByModID; + static std::vector<ModInfo::Ptr> s_Collection; + static ModInfo::Ptr s_Overwrite; + static std::map<QString, unsigned int> s_ModsByName; + static std::map<std::pair<QString, int>, std::vector<unsigned int>> s_ModsByModID; static int s_NextID; }; diff --git a/src/modinfobackup.cpp b/src/modinfobackup.cpp index 6a34b86a..603e74f6 100644 --- a/src/modinfobackup.cpp +++ b/src/modinfobackup.cpp @@ -15,7 +15,7 @@ QString ModInfoBackup::getDescription() const } -ModInfoBackup::ModInfoBackup(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure) - : ModInfoRegular(pluginContainer, game, path, directoryStructure) +ModInfoBackup::ModInfoBackup(const QDir& path, OrganizerCore& core) + : ModInfoRegular(path, core) { } diff --git a/src/modinfobackup.h b/src/modinfobackup.h index bf5dc5e6..f25ee9cf 100644 --- a/src/modinfobackup.h +++ b/src/modinfobackup.h @@ -42,7 +42,7 @@ public: private: - ModInfoBackup(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure); + ModInfoBackup(const QDir& path, OrganizerCore& core); }; diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index cc50d446..f50d7024 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -21,7 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "ui_modinfodialog.h" #include "plugincontainer.h" #include "organizercore.h" -#include "mainwindow.h" +#include "modlistview.h" #include "modinfodialogtextfiles.h" #include "modinfodialogimages.h" #include "modinfodialogesps.h" @@ -174,19 +174,31 @@ bool ModInfoDialog::TabInfo::isVisible() const return (realPos != -1); } - ModInfoDialog::ModInfoDialog( - MainWindow* mw, OrganizerCore* core, PluginContainer* plugin, - ModInfo::Ptr mod) : - TutorableDialog("ModInfoDialog", mw), - ui(new Ui::ModInfoDialog), m_mainWindow(mw), - m_core(core), m_plugin(plugin), m_initialTab(ModInfoTabIDs::None), + OrganizerCore& core, PluginContainer& plugin, + ModInfo::Ptr mod, ModListView* modListView, QWidget *parent) : + TutorableDialog("ModInfoDialog", parent), + ui(new Ui::ModInfoDialog), + m_core(core), + m_plugin(plugin), + m_modListView(modListView), + m_initialTab(ModInfoTabIDs::None), m_arrangingTabs(false) { ui->setupUi(this); - auto* sc = new QShortcut(QKeySequence::Delete, this); - connect(sc, &QShortcut::activated, [&]{ onDeleteShortcut(); }); + { + auto* sc = new QShortcut(QKeySequence::Delete, this); + connect(sc, &QShortcut::activated, [&] { onDeleteShortcut(); }); + } + { + auto* sc = new QShortcut(QKeySequence::MoveToNextPage, this); + connect(sc, &QShortcut::activated, [&] { onNextMod(); }); + } + { + auto* sc = new QShortcut(QKeySequence::MoveToPreviousPage, this); + connect(sc, &QShortcut::activated, [&] { onPreviousMod(); }); + } setMod(mod); createTabs(); @@ -204,7 +216,7 @@ template <class T> std::unique_ptr<ModInfoDialogTab> createTab(ModInfoDialog& d, ModInfoTabIDs id) { return std::make_unique<T>(ModInfoDialogTabContext( - *d.m_core, *d.m_plugin, &d, d.ui.get(), id, d.m_mod, d.getOrigin())); + d.m_core, d.m_plugin, &d, d.ui.get(), id, d.m_mod, d.getOrigin())); } void ModInfoDialog::createTabs() @@ -269,7 +281,7 @@ int ModInfoDialog::exec() update(true); if (noCustomTabRequested) { - m_core->settings().widgets().restoreIndex(ui->tabWidget); + m_core.settings().widgets().restoreIndex(ui->tabWidget); } const int r = TutorableDialog::exec(); @@ -430,7 +442,7 @@ void ModInfoDialog::reAddTabs( Q_ASSERT(visibility.size() == m_tabs.size()); // ordered tab names from settings - const auto orderedNames = m_core->settings().geometry().modInfoTabOrder(); + const auto orderedNames = m_core.settings().geometry().modInfoTabOrder(); // whether the tabs can be sorted // @@ -586,7 +598,7 @@ void ModInfoDialog::feedFiles(std::vector<TabInfo*>& interestedTabs) void ModInfoDialog::setTabsColors() { - const auto p = m_mainWindow->palette(); + const auto p = m_modListView->parentWidget()->palette(); for (const auto& tabInfo : m_tabs) { if (!tabInfo.isVisible()) { @@ -619,7 +631,7 @@ void ModInfoDialog::switchToTab(ModInfoTabIDs id) MOShared::FilesOrigin* ModInfoDialog::getOrigin() { - auto* ds = m_core->directoryStructure(); + auto* ds = m_core.directoryStructure(); if (!ds->originExists(m_mod->name().toStdWString())) { return nullptr; @@ -639,7 +651,7 @@ void ModInfoDialog::saveState() const // save state for each tab for (const auto& tabInfo : m_tabs) { - tabInfo.tab->saveState(m_core->settings()); + tabInfo.tab->saveState(m_core.settings()); } } @@ -650,7 +662,7 @@ void ModInfoDialog::restoreState() // restore state for each tab for (const auto& tabInfo : m_tabs) { - tabInfo.tab->restoreState(m_core->settings()); + tabInfo.tab->restoreState(m_core.settings()); } } @@ -678,9 +690,9 @@ void ModInfoDialog::saveTabOrder() const names += ui->tabWidget->widget(i)->objectName(); } - m_core->settings().geometry().setModInfoTabOrder(names); + m_core.settings().geometry().setModInfoTabOrder(names); // save last opened index - m_core->settings().widgets().saveIndex(ui->tabWidget); + m_core.settings().widgets().saveIndex(ui->tabWidget); } void ModInfoDialog::onOriginModified(int originID) @@ -776,22 +788,31 @@ void ModInfoDialog::onTabMoved() void ModInfoDialog::onNextMod() { - auto mod = m_mainWindow->nextModInList(); + auto index = m_modListView->nextMod(ModInfo::getIndex(m_mod->name())); + if (!index) { + return; + } + auto mod = ModInfo::getByIndex(*index); if (!mod || mod == m_mod) { return; } setMod(mod); update(); + + emit modChanged(*index); } void ModInfoDialog::onPreviousMod() { - auto mod = m_mainWindow->previousModInList(); - if (!mod || mod == m_mod) { + auto index = m_modListView->prevMod(ModInfo::getIndex(m_mod->name())); + if (!index) { return; } + auto mod = ModInfo::getByIndex(*index); setMod(mod); update(); + + emit modChanged(*index); } diff --git a/src/modinfodialog.h b/src/modinfodialog.h index 48680ca4..6d408f05 100644 --- a/src/modinfodialog.h +++ b/src/modinfodialog.h @@ -33,7 +33,7 @@ class PluginContainer; class OrganizerCore;
class Settings;
class ModInfoDialogTab;
-class MainWindow;
+class ModListView;
/**
* this is a larger dialog used to visualise information about the mod.
@@ -52,8 +52,8 @@ class ModInfoDialog : public MOBase::TutorableDialog public:
ModInfoDialog(
- MainWindow* mw, OrganizerCore* core, PluginContainer* plugin,
- ModInfo::Ptr mod);
+ OrganizerCore& core, PluginContainer& plugin,
+ ModInfo::Ptr mod, ModListView* view, QWidget* parent = nullptr);
~ModInfoDialog();
@@ -71,6 +71,10 @@ signals: //
void originModified(int originID);
+ // emitted when the mod of the dialog is changed
+ //
+ void modChanged(unsigned int modIndex);
+
protected:
// forwards to tryClose()
//
@@ -115,10 +119,10 @@ private: };
std::unique_ptr<Ui::ModInfoDialog> ui;
- MainWindow* m_mainWindow;
+ OrganizerCore& m_core;
+ PluginContainer& m_plugin;
+ ModListView* m_modListView;
ModInfo::Ptr m_mod;
- OrganizerCore* m_core;
- PluginContainer* m_plugin;
std::vector<TabInfo> m_tabs;
// initial tab requested by the main window when the dialog is opened; whether
diff --git a/src/modinfoforeign.cpp b/src/modinfoforeign.cpp index 19413891..97d3f4e1 100644 --- a/src/modinfoforeign.cpp +++ b/src/modinfoforeign.cpp @@ -42,14 +42,11 @@ QString ModInfoForeign::getDescription() const return tr("This pseudo mod represents content managed outside MO. It isn't modified by MO."); } -ModInfoForeign::ModInfoForeign(const QString &modName, - const QString &referenceFile, - const QStringList &archives, - ModInfo::EModType modType, - const MOBase::IPluginGame* gamePlugin, - DirectoryEntry **directoryStructure, - PluginContainer *pluginContainer) - : ModInfoWithConflictInfo(pluginContainer, gamePlugin, directoryStructure), +ModInfoForeign::ModInfoForeign( + const QString &modName, const QString &referenceFile, + const QStringList &archives, ModInfo::EModType modType, + OrganizerCore& core) + : ModInfoWithConflictInfo(core), m_ReferenceFile(referenceFile), m_Archives(archives), m_ModType(modType) { m_CreationTime = QFileInfo(referenceFile).birthTime(); diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 0a4ec42a..31fd13aa 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -32,7 +32,6 @@ public: virtual void setIsEndorsed(bool) override {} virtual void setNeverEndorse() override {} virtual void setIsTracked(bool) override {} - virtual bool remove() override { return false; } virtual void endorse(bool) override {} virtual void track(bool) override {} virtual bool isEmpty() const override { return false; } @@ -81,9 +80,8 @@ public: protected: ModInfoForeign(const QString &modName, const QString &referenceFile, const QStringList &archives, ModInfo::EModType modType, - const MOBase::IPluginGame *gamePlugin, - MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer); - + OrganizerCore &core); + private: QString m_Name; diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp index 8aad6209..5ee7a0d9 100644 --- a/src/modinfooverwrite.cpp +++ b/src/modinfooverwrite.cpp @@ -6,8 +6,7 @@ #include <QApplication> #include <QDirIterator> -ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, MOShared::DirectoryEntry **directoryStructure) - : ModInfoWithConflictInfo(pluginContainer, game, directoryStructure) +ModInfoOverwrite::ModInfoOverwrite(OrganizerCore& core) : ModInfoWithConflictInfo(core) { } @@ -60,7 +59,7 @@ QString ModInfoOverwrite::getDescription() const "modified (i.e. by the construction kit)"); } -QStringList ModInfoOverwrite::archives(bool checkOnDisk) +QStringList ModInfoOverwrite::archives(bool checkOnDisk) { QStringList result; QDir dir(this->absolutePath()); diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index 7236d3bf..065a3ba2 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -34,7 +34,6 @@ public: virtual void setIsEndorsed(bool) override {} virtual void setNeverEndorse() override {} virtual void setIsTracked(bool) override {} - virtual bool remove() override { return false; } virtual void endorse(bool) override {} virtual void track(bool) override {} virtual bool alwaysEnabled() const override { return true; } @@ -78,7 +77,7 @@ public: virtual std::map<QString, QVariant> clearPluginSettings(const QString& pluginName) override { return {}; } private: - ModInfoOverwrite(PluginContainer *pluginContainer, const MOBase::IPluginGame* game, MOShared::DirectoryEntry **directoryStructure); + ModInfoOverwrite(OrganizerCore& core); }; diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index c712adb1..94f2fc31 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -5,6 +5,9 @@ #include "report.h" #include "moddatacontent.h" #include "settings.h" +#include "organizercore.h" +#include "plugincontainer.h" +#include <iplugingame.h> #include <QApplication> #include <QDirIterator> @@ -24,25 +27,25 @@ namespace { } } -ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGame *game, const QDir &path, DirectoryEntry **directoryStructure) - : ModInfoWithConflictInfo(pluginContainer, game, directoryStructure) +ModInfoRegular::ModInfoRegular(const QDir &path, OrganizerCore& core) + : ModInfoWithConflictInfo(core) , m_Name(path.dirName()) , m_Path(path.absolutePath()) , m_Repository() - , m_GameName(game->gameShortName()) + , m_GameName(core.managedGame()->gameShortName()) , m_IsAlternate(false) , m_Converted(false) , m_Validated(false) , m_MetaInfoChanged(false) , m_EndorsedState(EndorsedState::ENDORSED_UNKNOWN) , m_TrackedState(TrackedState::TRACKED_UNKNOWN) - , m_NexusBridge(pluginContainer) + , m_NexusBridge(&core.pluginContainer()) { m_CreationTime = QFileInfo(path.absolutePath()).birthTime(); // read out the meta-file for information readMeta(); - if (m_GameName.compare(game->gameShortName(), Qt::CaseInsensitive) != 0) - if (!game->primarySources().contains(m_GameName, Qt::CaseInsensitive)) + if (m_GameName.compare(core.managedGame()->gameShortName(), Qt::CaseInsensitive) != 0) + if (!core.managedGame()->primarySources().contains(m_GameName, Qt::CaseInsensitive)) m_IsAlternate = true; //populate m_Archives @@ -254,7 +257,7 @@ void ModInfoRegular::saveMeta() if (m_TrackedState != TrackedState::TRACKED_UNKNOWN) { metaFile.setValue("tracked", static_cast<std::underlying_type_t<TrackedState>>(m_TrackedState)); } - + metaFile.remove("installedFiles"); metaFile.beginWriteArray("installedFiles"); int idx = 0; @@ -462,6 +465,8 @@ bool ModInfoRegular::setName(const QString &name) std::map<QString, unsigned int>::iterator nameIter = s_ModsByName.find(m_Name); if (nameIter != s_ModsByName.end()) { + QMutexLocker locker(&s_Mutex); + unsigned int index = nameIter->second; s_ModsByName.erase(nameIter); @@ -586,12 +591,6 @@ QColor ModInfoRegular::color() const return m_Color; } -bool ModInfoRegular::remove() -{ - m_MetaInfoChanged = false; - return shellDelete(QStringList(absolutePath()), true); -} - void ModInfoRegular::endorse(bool doEndorse) { if (doEndorse != (m_EndorsedState == EndorsedState::ENDORSED_TRUE)) { @@ -682,7 +681,7 @@ std::vector<ModInfo::EFlag> ModInfoRegular::getFlags() const std::set<int> ModInfoRegular::doGetContents() const { - ModDataContent* contentFeature = m_GamePlugin->feature<ModDataContent>(); + ModDataContent* contentFeature = m_Core.managedGame()->feature<ModDataContent>(); if (contentFeature) { auto result = contentFeature->getContentsFor(fileTree()); @@ -879,7 +878,7 @@ std::vector<QString> ModInfoRegular::getIniTweaks() const } -std::map<QString, QVariant> ModInfoRegular::pluginSettings(const QString& pluginName) const +std::map<QString, QVariant> ModInfoRegular::pluginSettings(const QString& pluginName) const { auto itp = m_PluginSettings.find(pluginName); if (itp == std::end(m_PluginSettings)) { diff --git a/src/modinforegular.h b/src/modinforegular.h index f2e0b82d..08660993 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -187,12 +187,6 @@ public: virtual void setIsTracked(bool tracked) override; /** - * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices - * @return true if the mod was successfully removed - **/ - bool remove() override; - - /** * @brief endorse or un-endorse the mod * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. * @note if doEndorse doesn't differ from the current value, nothing happens. @@ -424,7 +418,7 @@ protected: virtual std::set<int> doGetContents() const override; - ModInfoRegular(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure); + ModInfoRegular(const QDir& path, OrganizerCore& core); private: diff --git a/src/modinfoseparator.cpp b/src/modinfoseparator.cpp index 58c8310a..02a67ecd 100644 --- a/src/modinfoseparator.cpp +++ b/src/modinfoseparator.cpp @@ -30,7 +30,7 @@ QString ModInfoSeparator::name() const } -ModInfoSeparator::ModInfoSeparator(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure) - : ModInfoRegular(pluginContainer, game, path, directoryStructure) +ModInfoSeparator::ModInfoSeparator(const QDir& path, OrganizerCore& core) + : ModInfoRegular(path, core) { } diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index 88262f37..eaff7c42 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -54,10 +54,7 @@ protected: private: - ModInfoSeparator( - PluginContainer* pluginContainer, - const MOBase::IPluginGame* game, const QDir& path, - MOShared::DirectoryEntry** directoryStructure); + ModInfoSeparator(const QDir& path, OrganizerCore& core); }; #endif diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index ad7eda3f..7a51a727 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -5,6 +5,7 @@ #include "shared/fileentry.h" #include <filesystem> +#include "organizercore.h" #include "iplugingame.h" #include "moddatachecker.h" #include "qdirfiletree.h" @@ -13,13 +14,12 @@ using namespace MOBase; using namespace MOShared; namespace fs = std::filesystem; -ModInfoWithConflictInfo::ModInfoWithConflictInfo( - PluginContainer *pluginContainer, const MOBase::IPluginGame* gamePlugin, DirectoryEntry **directoryStructure) - : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), +ModInfoWithConflictInfo::ModInfoWithConflictInfo(OrganizerCore& core) : + ModInfo(core), m_FileTree([this]() { return QDirFileTree::makeTree(absolutePath()); }), m_Valid([this]() { return doIsValid(); }), m_Contents([this]() { return doGetContents(); }), - m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {} + m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {} void ModInfoWithConflictInfo::clearCaches() { @@ -95,8 +95,8 @@ void ModInfoWithConflictInfo::doConflictCheck() const bool hasHiddenFiles = false; int dataID = 0; - if ((*m_DirectoryStructure)->originExists(L"data")) { - dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); + if (m_Core.directoryStructure()->originExists(L"data")) { + dataID = m_Core.directoryStructure()->getOriginByName(L"data").getID(); } std::wstring name = ToWString(this->name()); @@ -106,8 +106,8 @@ void ModInfoWithConflictInfo::doConflictCheck() const m_ArchiveConflictState = CONFLICT_NONE; m_ArchiveConflictLooseState = CONFLICT_NONE; - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + if (m_Core.directoryStructure()->originExists(name)) { + FilesOrigin &origin = m_Core.directoryStructure()->getOriginByName(name); std::vector<FileEntryPtr> files = origin.getFiles(); std::set<const DirectoryEntry*> checkedDirs; @@ -164,7 +164,7 @@ void ModInfoWithConflictInfo::doConflictCheck() const // If this is not the origin then determine the correct overwrite if (file->getOrigin() != origin.getID()) { - FilesOrigin &altOrigin = (*m_DirectoryStructure)->getOriginByID(file->getOrigin()); + FilesOrigin &altOrigin = m_Core.directoryStructure()->getOriginByID(file->getOrigin()); unsigned int altIndex = ModInfo::getIndex(ToQString(altOrigin.getName())); if (!file->isFromArchive()) { if (!archiveData.isValid()) @@ -182,7 +182,7 @@ void ModInfoWithConflictInfo::doConflictCheck() const // Sort out the alternatives for (const auto& altInfo : alternatives) { if ((altInfo.originID() != dataID) && (altInfo.originID() != origin.getID())) { - FilesOrigin &altOrigin = (*m_DirectoryStructure)->getOriginByID(altInfo.originID()); + FilesOrigin &altOrigin = m_Core.directoryStructure()->getOriginByID(altInfo.originID()); QString altOriginName = ToQString(altOrigin.getName()); unsigned int altIndex = ModInfo::getIndex(altOriginName); if (!altInfo.isFromArchive()) { @@ -276,8 +276,8 @@ ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isLooseArchiveCo bool ModInfoWithConflictInfo::isRedundant() const { std::wstring name = ToWString(this->name()); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + if (m_Core.directoryStructure()->originExists(name)) { + FilesOrigin &origin = m_Core.directoryStructure()->getOriginByName(name); std::vector<FileEntryPtr> files = origin.getFiles(); bool ignore = false; for (auto iter = files.begin(); iter != files.end(); ++iter) { @@ -315,7 +315,7 @@ void ModInfoWithConflictInfo::prefetch() { } bool ModInfoWithConflictInfo::doIsValid() const { - auto mdc = m_GamePlugin->feature<ModDataChecker>(); + auto mdc = m_Core.managedGame()->feature<ModDataChecker>(); if (mdc) { auto qdirfiletree = fileTree(); diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 0d11fcb1..889f1246 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -91,10 +91,7 @@ protected: **/ virtual std::set<int> doGetContents() const { return {}; } - ModInfoWithConflictInfo( - PluginContainer* pluginContainer, - const MOBase::IPluginGame* gamePlugin, - MOShared::DirectoryEntry** directoryStructure); + ModInfoWithConflictInfo(OrganizerCore& core); private: @@ -142,17 +139,12 @@ protected: */ virtual void prefetch() override; - // Current game plugin running in MO2: - MOBase::IPluginGame const * const m_GamePlugin; - private: MOBase::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_FileTree; MOBase::MemoizedLocked<bool> m_Valid; MOBase::MemoizedLocked<std::set<int>> m_Contents; - MOShared::DirectoryEntry **m_DirectoryStructure; - mutable EConflictType m_CurrentConflictState; mutable EConflictType m_ArchiveConflictState; mutable EConflictType m_ArchiveConflictLooseState; diff --git a/src/modlist.cpp b/src/modlist.cpp index c8056140..ea1d2754 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -28,6 +28,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "settings.h" #include "organizercore.h" #include "modinforegular.h" +#include "modlistdropinfo.h" #include "shared/directoryentry.h" #include "shared/fileentry.h" #include "shared/filesorigin.h" @@ -60,7 +61,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase; - ModList::ModList(PluginContainer *pluginContainer, OrganizerCore *organizer) : QAbstractItemModel(organizer) , m_Organizer(organizer) @@ -69,7 +69,6 @@ ModList::ModList(PluginContainer *pluginContainer, OrganizerCore *organizer) , m_Modified(false) , m_InNotifyChange(false) , m_FontMetrics(QFont()) - , m_DropOnItems(false) , m_PluginContainer(pluginContainer) { m_LastCheck.start(); @@ -112,6 +111,22 @@ int ModList::columnCount(const QModelIndex &) const return COL_LASTCOLUMN + 1; } +QString ModList::getDisplayName(ModInfo::Ptr info) const +{ + QString name = info->name(); + if (info->isSeparator()) { + name = name.replace("_separator", ""); + } + return name; +} + +QString ModList::makeInternalName(ModInfo::Ptr info, QString name) const +{ + if (info->isSeparator()) { + name += "_separator"; + } + return name; +} QVariant ModList::getOverwriteData(int column, int role) const { @@ -131,7 +146,7 @@ QVariant ModList::getOverwriteData(int column, int role) const } } break; case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - case Qt::UserRole: return -1; + case GroupingRole: return -1; case Qt::ForegroundRole: return QBrush(Qt::red); case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); default: return QVariant(); @@ -217,15 +232,11 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const || (column == COL_CONTENT) || (column == COL_CONFLICTFLAGS)) { return QVariant(); - } else if (column == COL_NAME) { - auto flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - return modInfo->name().replace("_separator", ""); - } - else - return modInfo->name(); - } else if (column == COL_VERSION) { + } + else if (column == COL_NAME) { + return getDisplayName(modInfo); + } + else if (column == COL_VERSION) { VersionInfo verInfo = modInfo->version(); QString version = verInfo.displayString(); if (role != Qt::EditRole) { @@ -234,14 +245,17 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } } return version; - } else if (column == COL_PRIORITY) { + } + else if (column == COL_PRIORITY) { int priority = modInfo->getFixedPriority(); if (priority != INT_MIN) { return QVariant(); // hide priority for mods where it's fixed - } else { + } + else { return m_Profile->getModPriority(modIndex); } - } else if (column == COL_MODID) { + } + else if (column == COL_MODID) { int modID = modInfo->nexusId(); if (modID > 0) { return modID; @@ -249,7 +263,8 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const else { return QVariant(); } - } else if (column == COL_GAME) { + } + else if (column == COL_GAME) { if (m_PluginContainer != nullptr) { for (auto game : m_PluginContainer->plugins<IPluginGame>()) { if (game->gameShortName().compare(modInfo->gameName(), Qt::CaseInsensitive) == 0) @@ -257,10 +272,12 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } } return modInfo->gameName(); - } else if (column == COL_CATEGORY) { + } + else if (column == COL_CATEGORY) { if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { return tr("Non-MO"); - } else { + } + else { int category = modInfo->primaryCategory(); if (category != -1) { CategoryFactory &categoryFactory = CategoryFactory::instance(); @@ -268,53 +285,67 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const try { int categoryIdx = categoryFactory.getCategoryIndex(category); return categoryFactory.getCategoryName(categoryIdx); - } catch (const std::exception &e) { + } + catch (const std::exception &e) { log::error("failed to retrieve category name: {}", e.what()); return QString(); } - } else { + } + else { log::warn("category {} doesn't exist (may have been removed)", category); modInfo->setCategory(category, false); return QString(); } - } else { + } + else { return QVariant(); } } - } else if (column == COL_INSTALLTIME) { + } + else if (column == COL_INSTALLTIME) { // display installation time for mods that can be updated if (modInfo->creationTime().isValid()) { return modInfo->creationTime(); - } else { + } + else { return QVariant(); } - } else if (column == COL_NOTES) { + } + else if (column == COL_NOTES) { return modInfo->comments(); - } else { + } + else { return tr("invalid"); } - } else if ((role == Qt::CheckStateRole) && (column == 0)) { + } + else if ((role == Qt::CheckStateRole) && (column == 0)) { if (modInfo->canBeEnabled()) { return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; - } else { + } + else { return QVariant(); } - } else if (role == Qt::TextAlignmentRole) { - auto flags = modInfo->getFlags(); + } + else if (role == Qt::TextAlignmentRole) { if (column == COL_NAME) { if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } else { + } + else { return QVariant(Qt::AlignLeft | Qt::AlignVCenter); } - } else if (column == COL_VERSION) { + } + else if (column == COL_VERSION) { return QVariant(Qt::AlignRight | Qt::AlignVCenter); - } else if (column == COL_NOTES) { + } + else if (column == COL_NOTES) { return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } else { + } + else { return QVariant(Qt::AlignCenter | Qt::AlignVCenter); } - } else if (role == Qt::UserRole) { + } + else if (role == GroupingRole) { if (column == COL_CATEGORY) { QVariantList categoryNames; std::set<int> categories = modInfo->getCategories(); @@ -324,22 +355,28 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } if (categoryNames.count() != 0) { return categoryNames; - } else { + } + else { return QVariant(); } - } else if (column == COL_PRIORITY) { + } + else if (column == COL_PRIORITY) { int priority = modInfo->getFixedPriority(); if (priority != INT_MIN) { return priority; - } else { + } + else { return m_Profile->getModPriority(modIndex); } - } else { + } + else { return modInfo->nexusId(); } - } else if (role == Qt::UserRole + 1) { + } + else if (role == IndexRole) { return modIndex; - } else if (role == Qt::UserRole + 2) { + } + else if (role == AggrRole) { switch (column) { case COL_MODID: return QtGroupingProxy::AGGR_FIRST; case COL_VERSION: return QtGroupingProxy::AGGR_MAX; @@ -347,26 +384,37 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN; default: return QtGroupingProxy::AGGR_NONE; } - } else if (role == Qt::UserRole + 3) { + } + else if (role == ContentsRole) { return contentsToIcons(modInfo->getContents()); - } else if (role == Qt::UserRole + 4) { + } + else if (role == GameNameRole) { return modInfo->gameName(); - } else if (role == Qt::FontRole) { + } + else if (role == PriorityRole) { + int priority = modInfo->getFixedPriority(); + if (priority != std::numeric_limits<int>::min()) { + return priority; + } + else { + return m_Profile->getModPriority(modIndex); + } + } + else if (role == Qt::FontRole) { QFont result; - auto flags = modInfo->getFlags(); if (column == COL_NAME) { - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - //result.setCapitalization(QFont::AllUppercase); + if (modInfo->isSeparator()) { result.setItalic(true); - //result.setUnderline(true); result.setBold(true); - } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { + } + else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { result.setItalic(true); } - } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) { + } + else if (column == COL_CATEGORY && modInfo->isForeign()) { result.setItalic(true); - } else if (column == COL_VERSION) { + } + else if (column == COL_VERSION) { if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { result.setWeight(QFont::Bold); } @@ -375,65 +423,57 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } } return result; - } else if (role == Qt::DecorationRole) { + } + else if (role == Qt::DecorationRole) { if (column == COL_VERSION) { if (modInfo->updateAvailable()) { return QIcon(":/MO/gui/update_available"); - } else if (modInfo->downgradeAvailable()) { + } + else if (modInfo->downgradeAvailable()) { return QIcon(":/MO/gui/warning"); - } else if (modInfo->version().scheme() == VersionInfo::SCHEME_DATE) { + } + else if (modInfo->version().scheme() == VersionInfo::SCHEME_DATE) { return QIcon(":/MO/gui/version_date"); } } return QVariant(); - } else if (role == Qt::ForegroundRole) { - if ((modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) || (column == COL_NOTES)) && modInfo->color().isValid()) { - return ColorSettings::idealTextColor(modInfo->color()); - } else if (column == COL_NAME) { + } + else if (role == Qt::ForegroundRole) { + if (column == COL_NAME) { int highlight = modInfo->getHighlight(); - if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) + if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) { return QBrush(Qt::darkRed); - else if (highlight & ModInfo::HIGHLIGHT_INVALID) + } + else if (highlight & ModInfo::HIGHLIGHT_INVALID) { return QBrush(Qt::darkGray); - } else if (column == COL_VERSION) { + } + } + else if (column == COL_VERSION) { if (!modInfo->newestVersion().isValid()) { return QVariant(); - } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { + } + else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { return QBrush(Qt::red); - } else { + } + else { return QBrush(Qt::darkGreen); } } return QVariant(); - } else if ((role == Qt::BackgroundRole) - || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { - bool overwrite = m_Overwrite.find(modIndex) != m_Overwrite.end(); - bool archiveOverwrite = m_ArchiveOverwrite.find(modIndex) != m_ArchiveOverwrite.end(); - bool archiveLooseOverwrite = m_ArchiveLooseOverwrite.find(modIndex) != m_ArchiveLooseOverwrite.end(); - bool overwritten = m_Overwritten.find(modIndex) != m_Overwritten.end(); - bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end(); - bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end(); + } + else if (role == Qt::BackgroundRole || role == ScrollMarkRole) { if (column == COL_NOTES && modInfo->color().isValid()) { return modInfo->color(); - } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) { - 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(); - } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) - && modInfo->color().isValid() - && ((role != ViewMarkingScrollBar::DEFAULT_ROLE) - || Settings::instance().colors().colorSeparatorScrollbar())) { + } + else if (modInfo->isSeparator() && modInfo->color().isValid() + && (role != ScrollMarkRole || Settings::instance().colors().colorSeparatorScrollbar())) { return modInfo->color(); - } else { + } + else { return QVariant(); } - } else if (role == Qt::ToolTipRole) { + } + else if (role == Qt::ToolTipRole) { if (column == COL_FLAGS) { QString result; @@ -443,7 +483,8 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } return result; - } else if (column == COL_CONFLICTFLAGS) { + } + else if (column == COL_CONFLICTFLAGS) { QString result; for (ModInfo::EConflictFlag flag : modInfo->getConflictFlags()) { @@ -452,16 +493,20 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } return result; - } else if (column == COL_CONTENT) { + } + else if (column == COL_CONTENT) { return contentsToToolTip(modInfo->getContents()); - } else if (column == COL_NAME) { + } + else if (column == COL_NAME) { try { return modInfo->getDescription(); - } catch (const std::exception &e) { + } + catch (const std::exception &e) { log::error("invalid mod description: {}", e.what()); return QString(); } - } else if (column == COL_VERSION) { + } + else if (column == COL_VERSION) { QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->version().displayString(3)).arg(modInfo->newestVersion().displayString(3)); if (modInfo->downgradeAvailable()) { text += "<br>" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn " @@ -470,7 +515,8 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } if (modInfo->getNexusFileStatus() == 4) { text += "<br>" + tr("This file has been marked as \"Old\". There is most likely an updated version of this file available."); - } else if (modInfo->getNexusFileStatus() == 6) { + } + else if (modInfo->getNexusFileStatus() == 6) { text += "<br>" + tr("This file has been marked as \"Deleted\"! You may want to check for an update or remove the nexus ID from this mod!"); } if (modInfo->nexusId() > 0) { @@ -484,7 +530,8 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } } return text; - } else if (column == COL_CATEGORY) { + } + else if (column == COL_CATEGORY) { const std::set<int> &categories = modInfo->getCategories(); std::wostringstream categoryString; categoryString << ToWString(tr("Categories: <br>")); @@ -496,19 +543,23 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } try { categoryString << "<span style=\"white-space: nowrap;\"><i>" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << "</font></span>"; - } catch (const std::exception &e) { + } + catch (const std::exception &e) { log::error("failed to generate tooltip: {}", e.what()); return QString(); } } return ToQString(categoryString.str()); - } else if (column == COL_NOTES) { + } + else if (column == COL_NOTES) { return getFlagText(ModInfo::FLAG_NOTES, modInfo); - } else { + } + else { return QVariant(); } - } else { + } + else { return QVariant(); } } @@ -569,21 +620,16 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) m_Profile->setModEnabled(modID, enabled); m_Modified = true; m_LastCheck.restart(); - emit modlistChanged(index, role); + emit modStatesChanged({ index }); emit tutorialModlistUpdate(); } result = true; emit dataChanged(index, index); - } else if (role == Qt::EditRole) { + } + else if (role == Qt::EditRole) { switch (index.column()) { case COL_NAME: { - auto flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - result = renameMod(modID, value.toString() + "_separator"); - } - else - result = renameMod(modID, value.toString()); + result = renameMod(modID, makeInternalName(info, value.toString())); } break; case COL_PRIORITY: { bool ok = false; @@ -593,8 +639,7 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) } if (ok) { m_Profile->setModPriority(modID, newPriority); - - emit modorder_changed(); + emit modPrioritiesChanged({ index }); result = true; } else { result = false; @@ -605,7 +650,6 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) int newID = value.toInt(&ok); if (ok) { info->setNexusID(newID); - emit modlistChanged(index, role); emit tutorialModlistUpdate(); result = true; } else { @@ -665,7 +709,6 @@ QVariant ModList::headerData(int section, Qt::Orientation orientation, return QAbstractItemModel::headerData(section, orientation, role); } - Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const { Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); @@ -674,7 +717,6 @@ Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const } if (modelIndex.isValid()) { ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); if (modInfo->getFixedPriority() == INT_MIN) { result |= Qt::ItemIsDragEnabled; result |= Qt::ItemIsUserCheckable; @@ -683,19 +725,19 @@ Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const (modelIndex.column() == COL_MODID)) { result |= Qt::ItemIsEditable; } - if (((modelIndex.column() == COL_NAME) || - (modelIndex.column() == COL_NOTES)) - && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) { + if ((modelIndex.column() == COL_NAME || modelIndex.column() == COL_NOTES) + && !modInfo->isForeign()) { result |= Qt::ItemIsEditable; } } - if (m_DropOnItems - && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) { + if (modInfo->isSeparator() || m_DropOnMod) { result |= Qt::ItemIsDropEnabled; } - } else { - if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled; } + else if (!m_DropOnMod) { + result |= Qt::ItemIsDropEnabled; + } + return result; } @@ -710,7 +752,7 @@ QStringList ModList::mimeTypes() const QMimeData *ModList::mimeData(const QModelIndexList &indexes) const { QMimeData *result = QAbstractItemModel::mimeData(indexes); - result->setData("text/plain", "mod"); + result->setData("text/plain", ModListDropInfo::MOD_TEXT); return result; } @@ -719,35 +761,34 @@ void ModList::changeModPriority(std::vector<int> sourceIndices, int newPriority) if (m_Profile == nullptr) return; emit layoutAboutToBeChanged(); - Profile *profile = m_Profile; // sort the moving mods by ascending priorities std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) > profile->getModPriority(RHS); + [=](const int &LHS, const int &RHS) { + return m_Profile->getModPriority(LHS) > m_Profile->getModPriority(RHS); }); // move mods that are decreasing in priority for (std::vector<int>::const_iterator iter = sourceIndices.begin(); iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); + int oldPriority = m_Profile->getModPriority(*iter); if (oldPriority > newPriority) { - profile->setModPriority(*iter, newPriority); + m_Profile->setModPriority(*iter, newPriority); m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); } } // sort the moving mods by descending priorities std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) < profile->getModPriority(RHS); + [=](const int &LHS, const int &RHS) { + return m_Profile->getModPriority(LHS) < m_Profile->getModPriority(RHS); }); // if at least one mod is increasing in priority, the target index is // that of the row BELOW the dropped location, otherwise it's the one above for (std::vector<int>::const_iterator iter = sourceIndices.begin(); iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); + int oldPriority = m_Profile->getModPriority(*iter); if (oldPriority < newPriority) { --newPriority; break; @@ -757,16 +798,21 @@ void ModList::changeModPriority(std::vector<int> sourceIndices, int newPriority) // move mods that are increasing in priority for (std::vector<int>::const_iterator iter = sourceIndices.begin(); iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); + int oldPriority = m_Profile->getModPriority(*iter); if (oldPriority < newPriority) { - profile->setModPriority(*iter, newPriority); + m_Profile->setModPriority(*iter, newPriority); m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); } } emit layoutChanged(); - emit modorder_changed(); + QModelIndexList indices; + for (auto& idx : sourceIndices) { + indices.append(index(idx, 0, QModelIndex())); + } + + emit modPrioritiesChanged(indices); } @@ -778,29 +824,7 @@ void ModList::changeModPriority(int sourceIndex, int newPriority) m_Profile->setModPriority(sourceIndex, newPriority); emit layoutChanged(); - - emit modorder_changed(); -} - -void ModList::setOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten) -{ - m_Overwrite = overwrite; - m_Overwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setArchiveOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten) -{ - m_ArchiveOverwrite = overwrite; - m_ArchiveOverwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setArchiveLooseOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten) -{ - m_ArchiveLooseOverwrite = overwrite; - m_ArchiveLooseOverwritten = overwritten; - notifyChange(0, rowCount() - 1); + emit modPrioritiesChanged({ index(sourceIndex, 0) }); } void ModList::setPluginContainer(PluginContainer *pluginContianer) @@ -848,28 +872,6 @@ int ModList::timeElapsedSinceLastChecked() const return m_LastCheck.elapsed(); } -void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry) -{ - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - ModInfo::getByIndex(i)->setPluginSelected(false); - } - for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) { - QString pluginName = idx.data().toString(); - - 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) { - auto modInfo = ModInfo::getByIndex(index); - modInfo->setPluginSelected(true); - } - } - } - notifyChange(0, rowCount() - 1); -} - IModList::ModStates ModList::state(unsigned int modIndex) const { IModList::ModStates result; @@ -938,19 +940,11 @@ MOBase::IModInterface* ModList::getMod(const QString& name) const bool ModList::removeMod(MOBase::IModInterface* mod) { - unsigned int index = ModInfo::getIndex(mod->name()); - if (index == UINT_MAX) { - if (auto* p = dynamic_cast<ModInfo*>(mod)) { - return p->remove(); - } - else { - return false; - } + bool result = ModInfo::removeMod(ModInfo::getIndex(mod->name())); + if (result) { + notifyModRemoved(mod->name()); } - else { - return ModInfo::removeMod(index); - } - notifyModRemoved(mod->name()); + return result; } MOBase::IModInterface* ModList::renameMod(MOBase::IModInterface* mod, const QString& name) @@ -1087,7 +1081,33 @@ boost::signals2::connection ModList::onModMoved(const std::function<void (const return m_ModMoved.connect(func); } -bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent) +int ModList::dropPriority(int row, const QModelIndex& parent) const +{ + if (row == -1) { + row = parent.row(); + } + + if ((row < 0) || (static_cast<unsigned int>(row) >= ModInfo::getNumMods())) { + return -1; + } + + int newPriority = 0; + { + if ((row < 0) || (row > static_cast<int>(m_Profile->numRegularMods()))) { + newPriority = m_Profile->numRegularMods() + 1; + } + else { + newPriority = m_Profile->getModPriority(row); + } + if (newPriority == -1) { + newPriority = m_Profile->numRegularMods() + 1; + } + } + + return newPriority; +} + +bool ModList::dropLocalFiles(const ModListDropInfo& dropInfo, int row, const QModelIndex &parent) { if (row == -1) { row = parent.row(); @@ -1095,49 +1115,19 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa ModInfo::Ptr modInfo = ModInfo::getByIndex(row); QDir modDir = QDir(modInfo->absolutePath()); - QDir allModsDir(Settings::instance().paths().mods()); - QDir overwriteDir(Settings::instance().paths().overwrite()); - QStringList sourceList; QStringList targetList; QList<QPair<QString,QString>> relativePathList; - unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool { - std::vector<ModInfo::EFlag> flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); - - QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name(); - - for (auto url : mimeData->urls()) { - if (!url.isLocalFile()) { - log::debug("URL drop ignored: \"{}\" is not a local file", url.url()); - continue; - } + for (auto localUrl : dropInfo.localUrls()) { - QFileInfo sourceInfo(url.toLocalFile()); + QFileInfo sourceInfo(localUrl.url.toLocalFile()); QString sourceFile = sourceInfo.canonicalFilePath(); - QString relativePath; - QString originName; - - if (sourceFile.startsWith(allModsDir.canonicalPath())) { - QDir relativeDir(allModsDir.relativeFilePath(sourceFile)); - QStringList splitPath = relativeDir.path().split("/"); - originName = splitPath[0]; - splitPath.pop_front(); - relativePath = splitPath.join("/"); - } else if (sourceFile.startsWith(overwriteDir.canonicalPath())) { - originName = overwriteName; - relativePath = overwriteDir.relativeFilePath(sourceFile); - } else { - log::debug("URL drop ignored: \"{}\" is not a known file to MO", sourceFile); - continue; - } - - QFileInfo targetInfo(modDir.absoluteFilePath(relativePath)); + QFileInfo targetInfo(modDir.absoluteFilePath(localUrl.relativePath)); sourceList << sourceFile; targetList << targetInfo.absoluteFilePath(); - relativePathList << QPair<QString,QString>(relativePath, originName); + relativePathList << QPair<QString,QString>(localUrl.relativePath, localUrl.originName); } if (sourceList.count()) { @@ -1158,66 +1148,80 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa return true; } -bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent) +void ModList::onDragEnter(const QMimeData* mimeData) +{ + m_DropOnMod = ModListDropInfo(mimeData, *m_Organizer).isLocalFileDrop(); +} + +bool ModList::canDropMimeData(const QMimeData* mimeData, Qt::DropAction action, int row, int column, const QModelIndex& parent) const { + if (action == Qt::IgnoreAction) { + return false; + } - try { - QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); - QDataStream stream(&encoded, QIODevice::ReadOnly); - std::vector<int> sourceRows; + ModListDropInfo dropInfo(mimeData, *m_Organizer); - while (!stream.atEnd()) { - int sourceRow, col; - QMap<int, QVariant> roleDataMap; - stream >> sourceRow >> col >> roleDataMap; - if (col == 0) { - sourceRows.push_back(sourceRow); - } + if (dropInfo.isLocalFileDrop()) { + if (row == -1 && parent.isValid()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(parent.row()); + return modInfo->isRegular() && !modInfo->isSeparator(); } - - if (row == -1) { - row = parent.row(); + } + else if (dropInfo.isValid()) { + // drop on item + if (row == -1 && parent.isValid()) { + return true; } - - if ((row < 0) || (static_cast<unsigned int>(row) >= ModInfo::getNumMods())) { - return false; + else if (hasIndex(row, column, parent)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + return !modInfo->isBackup() && (modInfo->isSeparator() || !parent.isValid()); } - - int newPriority = 0; - { - if ((row < 0) || (row > static_cast<int>(m_Profile->numRegularMods()))) { - newPriority = m_Profile->numRegularMods() + 1; - } else { - newPriority = m_Profile->getModPriority(row); - } - if (newPriority == -1) { - newPriority = m_Profile->numRegularMods() + 1; - } + else { + return true; } - changeModPriority(sourceRows, newPriority); - } catch (const std::exception &e) { - reportError(tr("drag&drop failed: %1").arg(e.what())); } return false; } - bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) { if (action == Qt::IgnoreAction) { return true; } - if (m_Profile == nullptr) return false; + ModListDropInfo dropInfo(mimeData, *m_Organizer); - if (mimeData->hasUrls()) { - return dropURLs(mimeData, row, parent); - } else if (mimeData->hasText()) { - return dropMod(mimeData, row, parent); - } else { + if (!m_Profile || !dropInfo.isValid()) { + return false; + } + + int dropPriority = this->dropPriority(row, parent); + if (dropPriority == -1) { return false; } + + if (dropInfo.isLocalFileDrop()) { + return dropLocalFiles(dropInfo, row, parent); + } + else { + if (dropInfo.isModDrop()) { + changeModPriority(dropInfo.rows(), dropPriority); + } + else if (dropInfo.isDownloadDrop()) { + emit downloadArchiveDropped(dropInfo.download(), dropPriority); + } + else if (dropInfo.isExternalArchiveDrop()) { + emit externalArchiveDropped(dropInfo.externalUrl(), dropPriority); + } + else if (dropInfo.isExternalFolderDrop()) { + emit externalFolderDropped(dropInfo.externalUrl(), dropPriority); + } + else { + return false; + } + } + return false; } void ModList::removeRowForce(int row, const QModelIndex &parent) @@ -1236,8 +1240,8 @@ void ModList::removeRowForce(int row, const QModelIndex &parent) m_Profile->cancelModlistWrite(); beginRemoveRows(parent, row, row); ModInfo::removeMod(row); - endRemoveRows(); m_Profile->refreshModStatus(); // removes the mod from the status list + endRemoveRows(); m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed notifyModRemoved(modInfo->name()); @@ -1245,8 +1249,7 @@ void ModList::removeRowForce(int row, const QModelIndex &parent) if (wasEnabled) { emit removeOrigin(modInfo->name()); } - auto flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end()) { + if (!modInfo->isBackup()) { emit modUninstalled(modInfo->installationFile()); } } @@ -1264,8 +1267,7 @@ bool ModList::removeRows(int row, int count, const QModelIndex &parent) if (count == 1) { ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) { + if (modInfo->isOverwrite() && QDir(modInfo->absolutePath()).count() > 2) { emit clearOverwrite(); success = true; } @@ -1280,7 +1282,7 @@ bool ModList::removeRows(int row, int count, const QModelIndex &parent) success = true; QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), - tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), + tr("Are you sure you want to remove \"%1\"?").arg(getDisplayName(modInfo)), QMessageBox::Yes | QMessageBox::No); if (confirmBox.exec() == QMessageBox::Yes) { @@ -1322,12 +1324,6 @@ void ModList::notifyChange(int rowStart, int rowEnd) Guard g([&]{ m_InNotifyChange = false; }); if (rowStart < 0) { - m_Overwrite.clear(); - m_Overwritten.clear(); - m_ArchiveOverwrite.clear(); - m_ArchiveOverwritten.clear(); - m_ArchiveLooseOverwrite.clear(); - m_ArchiveLooseOverwritten.clear(); beginResetModel(); endResetModel(); } else { @@ -1357,19 +1353,12 @@ QModelIndex ModList::parent(const QModelIndex&) const QMap<int, QVariant> ModList::itemData(const QModelIndex &index) const { QMap<int, QVariant> result = QAbstractItemModel::itemData(index); - result[Qt::UserRole] = data(index, Qt::UserRole); - return result; -} - - -void ModList::dropModeUpdate(bool dropOnItems) -{ - if (m_DropOnItems != dropOnItems) { - m_DropOnItems = dropOnItems; + for (int role = Qt::UserRole; role < ModUserRole; ++role) { + result[role] = data(index, role); } + return result; } - QString ModList::getColumnName(int column) { switch (column) { @@ -1419,86 +1408,78 @@ QString ModList::getColumnToolTip(int column) const } } - -bool ModList::moveSelection(QAbstractItemView *itemView, int direction) +void ModList::shiftModsPriority(const QModelIndexList& indices, int offset) { - QItemSelectionModel *selectionModel = itemView->selectionModel(); + // retrieve the mod index and sort them by priority to avoid issue + // when moving them + std::vector<int> allIndex; + for (auto& idx : indices) { + auto index = idx.data(IndexRole).toInt(); + allIndex.push_back(index); + } + std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) { + bool cmp = m_Profile->getModPriority(lhs) < m_Profile->getModPriority(rhs); + return offset > 0 ? !cmp : cmp; + }); - const QAbstractProxyModel *proxyModel = qobject_cast<const QAbstractProxyModel*>(selectionModel->model()); - const QSortFilterProxyModel *filterModel = nullptr; + emit layoutAboutToBeChanged(); - while ((filterModel == nullptr) && (proxyModel != nullptr)) { - filterModel = qobject_cast<const QSortFilterProxyModel*>(proxyModel); - if (filterModel == nullptr) { - proxyModel = qobject_cast<const QAbstractProxyModel*>(proxyModel->sourceModel()); + std::vector<int> notify; + for (auto index : allIndex) { + int newPriority = m_Profile->getModPriority(index) + offset; + if ((newPriority >= 0) && (newPriority < static_cast<int>(m_Profile->numRegularMods()))) { + m_Profile->setModPriority(index, newPriority); + notify.push_back(index); } } - if (filterModel == nullptr) { - return true; - } - int offset = -1; - if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) || - ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) { - offset = 1; - } + emit layoutChanged(); - QModelIndexList rows = selectionModel->selectedRows(); - if (direction > 0) { - for (int i = 0; i < rows.size() / 2; ++i) { - rows.swapItemsAt(i, rows.size() - i - 1); - } + for (auto index : notify) { + notifyChange(index); } - for (QModelIndex idx : rows) { - if (filterModel != nullptr) { - idx = filterModel->mapToSource(idx); - } - int newPriority = m_Profile->getModPriority(idx.row()) + offset; - if ((newPriority >= 0) && (newPriority < static_cast<int>(m_Profile->numRegularMods()))) { - m_Profile->setModPriority(idx.row(), newPriority); - notifyChange(idx.row()); - } - } - emit modorder_changed(); - return true; + + emit modPrioritiesChanged(indices); } -bool ModList::deleteSelection(QAbstractItemView *itemView) +void ModList::changeModsPriority(const QModelIndexList& indices, int priority) { - QItemSelectionModel *selectionModel = itemView->selectionModel(); + if (indices.isEmpty()) { + return; + } - QModelIndexList rows = selectionModel->selectedRows(); - if (rows.count() > 1) { - emit removeSelectedMods(); - } else if (rows.count() == 1) { - removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex()); + std::vector<int> allIndex; + for (auto& idx : indices) { + auto index = idx.data(IndexRole).toInt(); + allIndex.push_back(index); + } + + if (allIndex.size() == 1) { + changeModPriority(allIndex[0], priority); + } + else { + changeModPriority(allIndex, priority); } - return true; } -bool ModList::toggleSelection(QAbstractItemView *itemView) +bool ModList::toggleState(const QModelIndexList& indices) { emit aboutToChangeData(); - QItemSelectionModel *selectionModel = itemView->selectionModel(); - QList<unsigned int> modsToEnable; QList<unsigned int> modsToDisable; - QModelIndexList dirtyMods; - for (QModelIndex idx : selectionModel->selectedRows()) { - int modId = idx.data(Qt::UserRole + 1).toInt(); - if (m_Profile->modEnabled(modId)) { - modsToDisable.append(modId); - dirtyMods.append(idx); + for (auto index : indices) { + auto idx = index.data(IndexRole).toInt(); + if (m_Profile->modEnabled(idx)) { + modsToDisable.append(idx); } else { - modsToEnable.append(modId); - dirtyMods.append(idx); + modsToEnable.append(idx); } } m_Profile->setModsEnabled(modsToEnable, modsToDisable); - emit modlistChanged(dirtyMods, 0); + emit modStatesChanged(indices); emit tutorialModlistUpdate(); m_Modified = true; @@ -1511,48 +1492,17 @@ bool ModList::toggleSelection(QAbstractItemView *itemView) return true; } -bool ModList::eventFilter(QObject *obj, QEvent *event) +void ModList::setActive(const QModelIndexList& indices, bool active) { - if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) { - QAbstractItemView *itemView = qobject_cast<QAbstractItemView*>(obj); - QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event); - - if ((itemView != nullptr) - && (keyEvent->modifiers() == Qt::ControlModifier) - && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { - return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1); - } else if (keyEvent->key() == Qt::Key_Delete) { - return deleteSelection(itemView); - } else if (keyEvent->key() == Qt::Key_Space) { - return toggleSelection(itemView); - } - return QAbstractItemModel::eventFilter(obj, event); + QList<unsigned int> mods; + for (auto& index : indices) { + mods.append(index.data(IndexRole).toInt()); } - return QAbstractItemModel::eventFilter(obj, event); -} -//note: caller needs to make sure sort proxy is updated -void ModList::enableSelected(const QItemSelectionModel *selectionModel) -{ - if (selectionModel->hasSelection()) { - QList<unsigned int> modsToEnable; - for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { - int modID = m_Profile->modIndexByPriority(row.data().toInt()); - modsToEnable.append(modID); - } - m_Profile->setModsEnabled(modsToEnable, QList<unsigned int>()); + if (active) { + m_Profile->setModsEnabled(mods, {}); } -} - -//note: caller needs to make sure sort proxy is updated -void ModList::disableSelected(const QItemSelectionModel *selectionModel) -{ - if (selectionModel->hasSelection()) { - QList<unsigned int> modsToDisable; - for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { - int modID = m_Profile->modIndexByPriority(row.data().toInt()); - modsToDisable.append(modID); - } - m_Profile->setModsEnabled(QList<unsigned int>(), modsToDisable); + else { + m_Profile->setModsEnabled({}, mods); } } diff --git a/src/modlist.h b/src/modlist.h index 21a19fab..1d94749c 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -31,6 +31,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QFile>
#include <QListWidget>
#include <QNetworkReply>
+#include <QMetaEnum>
#include <QNetworkAccessManager>
#ifndef Q_MOC_RUN
#include <boost/signals2.hpp>
@@ -43,6 +44,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. class QSortFilterProxyModel;
class PluginContainer;
class OrganizerCore;
+class ModListDropInfo;
/**
* Model presenting an overview of the installed mod
@@ -55,6 +57,33 @@ class ModList : public QAbstractItemModel 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,
+
+ // data(ContentsRole) contains mod data contents as a QVariantList
+ // containing icon paths
+ ContentsRole = Qt::UserRole + 3,
+
+ GameNameRole = Qt::UserRole + 4,
+
+ PriorityRole = Qt::UserRole + 5,
+
+ // marking role for the scrollbar
+ ScrollMarkRole = Qt::UserRole + 6,
+
+ // this is the first available role
+ ModUserRole = Qt::UserRole + 7
+ };
+
enum EColumn {
COL_NAME,
COL_CONFLICTFLAGS,
@@ -70,8 +99,6 @@ public: COL_LASTCOLUMN = COL_NOTES,
};
- friend class ModListProxy;
-
using SignalModInstalled = boost::signals2::signal<void(MOBase::IModInterface*)>;
using SignalModRemoved = boost::signals2::signal<void(QString const&)>;
using SignalModStateChanged = boost::signals2::signal<void (const std::map<QString, MOBase::IModList::ModStates>&)>;
@@ -113,12 +140,8 @@ public: void changeModPriority(int sourceIndex, int newPriority);
void changeModPriority(std::vector<int> sourceIndices, int newPriority);
- void setOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten);
void setPluginContainer(PluginContainer *pluginContainer);
- void setArchiveOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten);
- void setArchiveLooseOverwriteMarkers(const std::set<unsigned int> &overwrite, const std::set<unsigned int> &overwritten);
-
bool modInfoAboutToChange(ModInfo::Ptr info);
void modInfoChanged(ModInfo::Ptr info);
@@ -126,7 +149,7 @@ public: int timeElapsedSinceLastChecked() const;
- void highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry);
+public:
/**
* @brief Notify the mod list that the given mod has been installed. This is used
@@ -199,11 +222,13 @@ public: // implementation of virtual functions of QAbstractItemModel 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 Qt::DropActions supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction; }
- virtual QStringList mimeTypes() const;
- virtual QMimeData *mimeData(const QModelIndexList &indexes) const;
- virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
- virtual bool removeRows(int row, int count, const QModelIndex &parent);
+ 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;
@@ -213,21 +238,36 @@ public: // implementation of virtual functions of QAbstractItemModel public slots:
- void dropModeUpdate(bool dropOnItems);
+ void onDragEnter(const QMimeData* data);
- void enableSelected(const QItemSelectionModel *selectionModel);
+ // enable/disable mods at the given indices.
+ //
+ void setActive(const QModelIndexList& indices, bool active);
- void disableSelected(const QItemSelectionModel *selectionModel);
+ // 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:
- /**
- * @brief emitted whenever the sorting in the list was changed by the user
- *
- * 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 modorder_changed();
+ // 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);
+
+ // emitted when the state (active/inactive) of one or multiple mods have changed
+ //
+ void modStatesChanged(const QModelIndexList& indices);
/**
* @brief emitted when the model wants a text to be displayed by the UI
@@ -264,36 +304,11 @@ signals: void modUninstalled(const QString &fileName);
/**
- * @brief emitted whenever a row in the list has changed
- *
- * @param index the index of the changed field
- * @param role role of the field that changed
- * @note this signal must only be emitted if the row really did change.
- * Slots handling this signal therefore do not have to verify that a change has happened
- **/
- void modlistChanged(const QModelIndex &index, int role);
-
- /**
- * @brief emitted whenever multiple row sin the list has changed
- *
- * @param indicies the list of indicies of the changed field
- * @param role role of the field that changed
- * @note this signal must only be emitted if the row really did change.
- * Slots handling this signal therefore do not have to verify that a change has happened
- **/
- void modlistChanged(const QModelIndexList &indicies, int role);
-
- /**
* @brief QML seems to handle overloaded signals poorly - create unique signal for tutorials
*/
void tutorialModlistUpdate();
/**
- * @brief emitted to have all selected mods deleted
- */
- void removeSelectedMods();
-
- /**
* @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
@@ -310,13 +325,27 @@ signals: void postDataChanged();
-protected:
+ // 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);
- // event filter, handles event from the header and the tree view itself
- bool eventFilter(QObject *obj, QEvent *event);
+ // 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;
+
QVariant getOverwriteData(int column, int role) const;
QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const;
@@ -336,17 +365,15 @@ private: bool renameMod(int index, const QString &newName);
- bool dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent);
-
- bool dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent);
-
MOBase::IModList::ModStates state(unsigned int modIndex) const;
- bool moveSelection(QAbstractItemView *itemView, int direction);
-
- bool deleteSelection(QAbstractItemView *itemView);
+ // handle dropping of local URLs files
+ //
+ bool dropLocalFiles(const ModListDropInfo& dropInfo, int row, const QModelIndex& parent);
- bool toggleSelection(QAbstractItemView *itemView);
+ // return the priority of the mod for a drop event
+ //
+ int dropPriority(int row, const QModelIndex& parent) const;
private slots:
@@ -377,18 +404,10 @@ private: mutable bool m_Modified;
bool m_InNotifyChange;
+ bool m_DropOnMod = false;
QFontMetrics m_FontMetrics;
- bool m_DropOnItems;
-
- std::set<unsigned int> m_Overwrite;
- std::set<unsigned int> m_Overwritten;
- std::set<unsigned int> m_ArchiveOverwrite;
- std::set<unsigned int> m_ArchiveOverwritten;
- std::set<unsigned int> m_ArchiveLooseOverwrite;
- std::set<unsigned int> m_ArchiveLooseOverwritten;
-
TModInfoChange m_ChangeInfo;
SignalModInstalled m_ModInstalled;
diff --git a/src/modlistbypriorityproxy.cpp b/src/modlistbypriorityproxy.cpp new file mode 100644 index 00000000..6f549d91 --- /dev/null +++ b/src/modlistbypriorityproxy.cpp @@ -0,0 +1,322 @@ +#include "modlistbypriorityproxy.h" + +#include "modinfo.h" +#include "profile.h" +#include "organizercore.h" +#include "modlist.h" +#include "modlistdropinfo.h" +#include "log.h" + +ModListByPriorityProxy::ModListByPriorityProxy(Profile* profile, OrganizerCore& core, QObject* parent) : + QAbstractProxyModel(parent), m_core(core), m_profile(profile) +{ +} + +ModListByPriorityProxy::~ModListByPriorityProxy() +{ +} + +void ModListByPriorityProxy::setSourceModel(QAbstractItemModel* model) +{ + if (sourceModel()) { + disconnect(sourceModel(), nullptr, this, nullptr); + } + + QAbstractProxyModel::setSourceModel(model); + + if (sourceModel()) { + connect(sourceModel(), &QAbstractItemModel::layoutChanged, this, &ModListByPriorityProxy::buildTree, Qt::UniqueConnection); + connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &ModListByPriorityProxy::buildTree, Qt::UniqueConnection); + connect(sourceModel(), &QAbstractItemModel::modelReset, this, &ModListByPriorityProxy::buildTree, Qt::UniqueConnection); + connect(sourceModel(), &QAbstractItemModel::dataChanged, this, &ModListByPriorityProxy::modelDataChanged, Qt::UniqueConnection); + refresh(); + } +} + +void ModListByPriorityProxy::refresh() +{ + buildTree(); +} + +void ModListByPriorityProxy::setProfile(Profile* profile) +{ + m_profile = profile; +} + +void ModListByPriorityProxy::buildTree() +{ + if (!sourceModel()) return; + + beginResetModel(); + + // reset the root + m_Root = { }; + m_IndexToItem.clear(); + + TreeItem* root = &m_Root; + std::unique_ptr<TreeItem> overwrite; + std::vector<std::unique_ptr<TreeItem>> backups; + for (auto& [priority, index] : m_profile->getAllIndexesByPriority()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + + TreeItem* item; + + if (modInfo->isSeparator()) { + m_Root.children.push_back(std::make_unique<TreeItem>(modInfo, index, &m_Root)); + item = m_Root.children.back().get(); + root = item; + } + else if (modInfo->isOverwrite()) { + // do not push here, because the overwrite is usually not at the right position + overwrite = std::make_unique<TreeItem>(modInfo, index, &m_Root); + item = overwrite.get(); + } + else if (modInfo->isBackup()) { + backups.push_back(std::make_unique<TreeItem>(modInfo, index, &m_Root)); + item = backups.back().get(); + } + else { + root->children.push_back(std::make_unique<TreeItem>(modInfo, index, root)); + item = root->children.back().get(); + } + + m_IndexToItem[index] = item; + } + + m_Root.children.insert(m_Root.children.begin(), + std::make_move_iterator(backups.begin()), std::make_move_iterator(backups.end())); + m_Root.children.push_back(std::move(overwrite)); + + endResetModel(); +} + +void ModListByPriorityProxy::modelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles) +{ + QModelIndex proxyTopLeft = mapFromSource(topLeft); + if (!proxyTopLeft.isValid()) { + return; + } + + if (topLeft == bottomRight) { + emit dataChanged(proxyTopLeft, proxyTopLeft); + } + else { + QModelIndex proxyBottomRight = mapFromSource(bottomRight); + emit dataChanged(proxyTopLeft, proxyBottomRight); + } +} + +QModelIndex ModListByPriorityProxy::mapFromSource(const QModelIndex& sourceIndex) const +{ + if (!sourceIndex.isValid()) { + return QModelIndex(); + } + + auto* item = m_IndexToItem.at(sourceIndex.row()); + return createIndex(item->parent->childIndex(item), sourceIndex.column(), item); +} + +QModelIndex ModListByPriorityProxy::mapToSource(const QModelIndex& proxyIndex) const +{ + if (!proxyIndex.isValid()) { + return QModelIndex(); + } + auto* item = static_cast<TreeItem*>(proxyIndex.internalPointer()); + + return sourceModel()->index(item->index, proxyIndex.column()); +} + +int ModListByPriorityProxy::rowCount(const QModelIndex& parent) const +{ + if (!parent.isValid()) { + return m_Root.children.size(); + } + + auto* item = static_cast<TreeItem*>(parent.internalPointer()); + + if (item->mod->isSeparator()) { + return item->children.size(); + } + + return 0; +} + +int ModListByPriorityProxy::columnCount(const QModelIndex& index) const +{ + return sourceModel()->columnCount(mapToSource(index)); +} + +QModelIndex ModListByPriorityProxy::parent(const QModelIndex& child) const +{ + if (!child.isValid()) { + return QModelIndex(); + } + + auto* item = static_cast<TreeItem*>(child.internalPointer()); + + if (!item->parent || item->parent == &m_Root) { + return QModelIndex(); + } + + return createIndex(item->parent->parent->childIndex(item->parent), child.column(), item->parent); +} + +bool ModListByPriorityProxy::hasChildren(const QModelIndex& parent) const +{ + if (!parent.isValid()) { + return m_Root.children.size() > 0; + } + auto* item = static_cast<TreeItem*>(parent.internalPointer()); + return item->children.size() > 0; +} + +bool ModListByPriorityProxy::setData(const QModelIndex& index, const QVariant& value, int role) +{ + // only care about the "name" column + if (index.column() == 0 && role == Qt::EditRole) { + QString oldValue = data(index, role).toString(); + if (m_CollapsedItems.contains(oldValue)) { + m_CollapsedItems.erase(oldValue); + m_CollapsedItems.insert(value.toString()); + } + } + return QAbstractProxyModel::setData(index, value, role); +} + +bool ModListByPriorityProxy::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const +{ + ModListDropInfo dropInfo(data, m_core); + + if (!dropInfo.isValid() || dropInfo.isLocalFileDrop()) { + return QAbstractProxyModel::canDropMimeData(data, action, row, column, parent); + } + + if (dropInfo.isModDrop()) { + + bool hasSeparator = false; + unsigned int firstRowIndex = -1; + + int firstRowPriority = INT_MAX; + for (auto sourceRow : dropInfo.rows()) { + hasSeparator = hasSeparator || ModInfo::getByIndex(sourceRow)->isSeparator(); + if (m_profile->getModPriority(sourceRow) < firstRowPriority) { + firstRowIndex = sourceRow; + firstRowPriority = m_profile->getModPriority(sourceRow); + } + } + + bool firstRowSeparator = firstRowIndex != -1 && ModInfo::getByIndex(firstRowIndex)->isSeparator(); + + // row = -1 and invalid parent means we're dropping onto an item, we don't want to drop + // separators onto items or items into their own separator + if (row == -1 && parent.isValid()) { + auto* parentItem = static_cast<TreeItem*>(parent.internalPointer()); + if (hasSeparator) { + return !parentItem->mod->isSeparator(); + } + + for (auto row : dropInfo.rows()) { + auto it = m_IndexToItem.find(row); + if (it != m_IndexToItem.end() && it->second->parent == parentItem) { + return false; + } + } + } + + // first row is a separator, we can drop it anywhere + if (firstRowSeparator) { + return QAbstractProxyModel::canDropMimeData(data, action, row, column, parent); + } + } + + // top-level drop is disabled unless it's before the first separator + if (!parent.isValid() && row >= 0) { + + // the row may be outside of the children list if we insert at the end + if (row >= m_Root.children.size()) { + return false; + } + + // if the previous row is a collapsed separator, disable dropping + if (row > 0 && m_Root.children[row - 1]->mod->isSeparator()) { + // we cannot use the name of the mod directly because it does not exactly + // match the display value (e.g. for separators) + QString display = sourceModel()->index(m_Root.children[row - 1]->index, ModList::COL_NAME).data(Qt::DisplayRole).toString(); + if (m_CollapsedItems.contains(display) + && (m_Root.children[row]->mod->isSeparator() || m_Root.children[row]->mod->isOverwrite())) { + return false; + } + } + } + + return QAbstractProxyModel::canDropMimeData(data, action, row, column, parent); +} + +bool ModListByPriorityProxy::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +{ + // we need to fix the source model row + ModListDropInfo dropInfo(data, m_core); + int sourceRow = -1; + + if (dropInfo.isLocalFileDrop()) { + if (parent.isValid()) { + sourceRow = static_cast<TreeItem*>(parent.internalPointer())->index; + } + } + else { + if (row >= 0) { + if (!parent.isValid()) { + if (row < m_Root.children.size()) { + sourceRow = m_Root.children[row]->index; + if (row > 0 + && m_Root.children[row - 1]->mod->isSeparator() + && !m_Root.children[row - 1]->children.empty() + && m_dropPosition == ModListView::DropPosition::BelowItem) { + sourceRow = m_Root.children[row - 1]->children[0]->index; + } + } + else { + sourceRow = ModInfo::getNumMods(); + } + } + else { + auto* item = static_cast<TreeItem*>(parent.internalPointer()); + + if (row < item->children.size()) { + sourceRow = item->children[row]->index; + } + else if (parent.row() + 1 < m_Root.children.size()) { + sourceRow = m_Root.children[parent.row() + 1]->index; + } + } + } + else if (parent.isValid()) { + // this is a drop in a separator + sourceRow = m_Root.children[parent.row() + 1]->index; + } + } + + return sourceModel()->dropMimeData(data, action, sourceRow, column, QModelIndex()); +} + +QModelIndex ModListByPriorityProxy::index(int row, int column, const QModelIndex& parent) const +{ + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + const TreeItem* parentItem; + if (!parent.isValid()) { + parentItem = &m_Root; + } + else { + parentItem = static_cast<TreeItem*>(parent.internalPointer()); + } + + return createIndex(row, column, parentItem->children[row].get()); +} + +void ModListByPriorityProxy::onDropEnter(const QMimeData*, ModListView::DropPosition dropPosition) +{ + m_dropPosition = dropPosition; +} diff --git a/src/modlistbypriorityproxy.h b/src/modlistbypriorityproxy.h new file mode 100644 index 00000000..6b48678a --- /dev/null +++ b/src/modlistbypriorityproxy.h @@ -0,0 +1,89 @@ +#ifndef MODLISBYPRIORITYPROXY_H +#define MODLISBYPRIORITYPROXY_H + +#include <optional> +#include <set> +#include <vector> + +#include <QAbstractProxyModel> +#include <QModelIndex> +#include <QMultiHash> +#include <QStringList> +#include <QIcon> +#include <QSet> + +#include "modinfo.h" +#include "modlistview.h" + +class ModList; +class Profile; + +class ModListByPriorityProxy : public QAbstractProxyModel +{ + Q_OBJECT + +public: + explicit ModListByPriorityProxy(Profile* profile, OrganizerCore& core, QObject* parent = nullptr); + ~ModListByPriorityProxy(); + + void setProfile(Profile* profile); + void refresh(); + + void setSourceModel(QAbstractItemModel* sourceModel) override; + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& index) const override; + bool hasChildren(const QModelIndex& parent) const override; + + bool setData(const QModelIndex& index, const QVariant& value, int role) 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; + + QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; + +public slots: + + void onDropEnter(const QMimeData* data, ModListView::DropPosition dropPosition); + +protected slots: + + void modelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles = QVector<int>()); + +private: + + void buildTree(); + + struct TreeItem { + ModInfo::Ptr mod; + unsigned int index; + std::vector<std::unique_ptr<TreeItem>> children; + TreeItem* parent; + + std::size_t childIndex(TreeItem* child) const { + for (std::size_t i = 0; i < children.size(); ++i) { + if (children[i].get() == child) { + return i; + } + } + return -1; + } + + TreeItem() : TreeItem(nullptr, -1) { } + TreeItem(ModInfo::Ptr mod, unsigned int index, TreeItem* parent = nullptr) : + mod(mod), index(index), parent(parent) { } + }; + + TreeItem m_Root; + std::map<unsigned int, TreeItem*> m_IndexToItem; + std::set<QString> m_CollapsedItems; + +private: + OrganizerCore& m_core; + Profile* m_profile; + ModListView::DropPosition m_dropPosition = ModListView::DropPosition::OnItem; +}; + +#endif //GROUPINGPROXY_H diff --git a/src/modlistcontextmenu.cpp b/src/modlistcontextmenu.cpp new file mode 100644 index 00000000..f5307dc7 --- /dev/null +++ b/src/modlistcontextmenu.cpp @@ -0,0 +1,443 @@ +#include "modlistcontextmenu.h" + +#include <report.h> + +#include "modlist.h" +#include "modlistview.h" +#include "modlistviewactions.h" +#include "organizercore.h" + +using namespace MOBase; + +ModListGlobalContextMenu::ModListGlobalContextMenu(OrganizerCore& core, ModListView* view, QWidget* parent) + : ModListGlobalContextMenu(core, view, QModelIndex(), parent) +{ +} + +ModListGlobalContextMenu::ModListGlobalContextMenu(OrganizerCore& core, ModListView* view, const QModelIndex& index, QWidget* parent) + : QMenu(parent) +{ + addAction(tr("Install Mod..."), [=]() { view->actions().installMod(); }); + + auto modIndex = index.data(ModList::IndexRole); + if (modIndex.isValid()) { + auto info = ModInfo::getByIndex(modIndex.toInt()); + if (!info->isBackup()) { + addAction(tr("Create empty mod"), [=]() { view->actions().createEmptyMod(index); }); + addAction(tr("Create Separator"), [=]() { view->actions().createSeparator(index); }); + } + } + + if (view->hasCollapsibleSeparators()) { + addSeparator(); + addAction(tr("Collapse all"), view, &QTreeView::collapseAll); + addAction(tr("Expand all"), view, &QTreeView::expandAll); + } + + addSeparator(); + + addAction(tr("Enable all visible"), [=]() { + if (QMessageBox::question(view, tr("Confirm"), tr("Really enable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + view->enableAllVisible(); + } + }); + addAction(tr("Disable all visible"), [=]() { + if (QMessageBox::question(parent, tr("Confirm"), tr("Really disable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + view->disableAllVisible(); + } + }); + addAction(tr("Check for updates"), [=]() { view->actions().checkModsForUpdates(); }); + addAction(tr("Refresh"), &core, &OrganizerCore::profileRefresh); + addAction(tr("Export to csv..."), [=]() { view->actions().exportModListCSV(); }); +} + + +ModListChangeCategoryMenu::ModListChangeCategoryMenu(CategoryFactory& categories, ModInfo::Ptr mod, QMenu* parent) + : QMenu(tr("Change Categories"), parent) +{ + populate(this, categories, mod); +} + +std::vector<std::pair<int, bool>> ModListChangeCategoryMenu::categories() const +{ + return categories(this); +} + +std::vector<std::pair<int, bool>> ModListChangeCategoryMenu::categories(const QMenu* menu) const +{ + std::vector<std::pair<int, bool>> cats; + for (QAction* action : menu->actions()) { + if (action->menu() != nullptr) { + auto pcats = categories(action->menu()); + cats.insert(cats.end(), pcats.begin(), pcats.end()); + } + else { + QWidgetAction* widgetAction = qobject_cast<QWidgetAction*>(action); + if (widgetAction != nullptr) { + QCheckBox* checkbox = qobject_cast<QCheckBox*>(widgetAction->defaultWidget()); + cats.emplace_back(widgetAction->data().toInt(), checkbox->isChecked()); + } + } + } + return cats; +} + +bool ModListChangeCategoryMenu::populate(QMenu* menu, CategoryFactory& factory, ModInfo::Ptr mod, int targetId) +{ + const std::set<int>& categories = mod->getCategories(); + + bool childEnabled = false; + + for (unsigned int i = 1; i < factory.numCategories(); ++i) { + if (factory.getParentID(i) == targetId) { + QMenu* targetMenu = menu; + if (factory.hasChildren(i)) { + targetMenu = menu->addMenu(factory.getCategoryName(i).replace('&', "&&")); + } + + int id = factory.getCategoryID(i); + QScopedPointer<QCheckBox> checkBox(new QCheckBox(targetMenu)); + bool enabled = categories.find(id) != categories.end(); + checkBox->setText(factory.getCategoryName(i).replace('&', "&&")); + if (enabled) { + childEnabled = true; + } + checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); + + QScopedPointer<QWidgetAction> checkableAction(new QWidgetAction(targetMenu)); + checkableAction->setDefaultWidget(checkBox.take()); + checkableAction->setData(id); + targetMenu->addAction(checkableAction.take()); + + if (factory.hasChildren(i)) { + if (populate(targetMenu, factory, mod, factory.getCategoryID(i)) || enabled) { + targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); + } + } + } + } + return childEnabled; +} + +ModListPrimaryCategoryMenu::ModListPrimaryCategoryMenu(CategoryFactory& categories, ModInfo::Ptr mod, QMenu* parent) + : QMenu(tr("Primary Category"), parent) +{ + connect(this, &QMenu::aboutToShow, [=]() { populate(categories, mod); }); +} + +void ModListPrimaryCategoryMenu::populate(const CategoryFactory& factory, ModInfo::Ptr mod) +{ + clear(); + const std::set<int>& categories = mod->getCategories(); + for (int categoryID : categories) { + int catIdx = factory.getCategoryIndex(categoryID); + QWidgetAction* action = new QWidgetAction(this); + try { + QRadioButton* categoryBox = new QRadioButton( + factory.getCategoryName(catIdx).replace('&', "&&"), + this); + categoryBox->setChecked(categoryID == mod->primaryCategory()); + action->setDefaultWidget(categoryBox); + action->setData(categoryID); + } + catch (const std::exception& e) { + log::error("failed to create category checkbox: {}", e.what()); + } + + action->setData(categoryID); + addAction(action); + } +} + +int ModListPrimaryCategoryMenu::primaryCategory() const +{ + for (QAction* action : actions()) { + QWidgetAction* widgetAction = qobject_cast<QWidgetAction*>(action); + if (widgetAction) { + QRadioButton* button = qobject_cast<QRadioButton*>(widgetAction->defaultWidget()); + if (button && button->isChecked()) { + return widgetAction->data().toInt(); + } + } + } + return -1; +} + +ModListContextMenu::ModListContextMenu( + const QModelIndex& index, OrganizerCore& core, CategoryFactory& categories, ModListView* view) : + QMenu(view) + , m_core(core) + , m_categories(categories) + , m_index(index.model() == view->model() ? view->indexViewToModel(index) : index) + , m_view(view) + , m_actions(view->actions()) +{ + if (view->selectionModel()->hasSelection()) { + m_selected = view->indexViewToModel(view->selectionModel()->selectedRows()); + } + else { + m_selected = { index }; + } + + ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt()); + + QMenu* allMods = new ModListGlobalContextMenu(core, view, m_index, view->topLevelWidget()); + allMods->setTitle(tr("All Mods")); + addMenu(allMods); + + auto viewIndex = view->indexModelToView(m_index); + if (view->model()->hasChildren(viewIndex)) { + bool expanded = view->isExpanded(viewIndex); + addSeparator(); + addAction(tr("Collapse all"), view, &QTreeView::collapseAll); + addAction(tr("Collapse others"), [=]() { + m_view->collapseAll(); + m_view->setExpanded(viewIndex, expanded); + }); + addAction(tr("Expand all"), view, &QTreeView::expandAll); + } + + addSeparator(); + + // Add type-specific items + if (info->isOverwrite()) { + addOverwriteActions(info); + } + else if (info->isBackup()) { + addBackupActions(info); + } + else if (info->isSeparator()) { + addSeparatorActions(info); + } + else if (info->isForeign()) { + addForeignActions(info); + } + else { + addRegularActions(info); + } + + // add information for all except foreign + if (!info->isForeign()) { + QAction* infoAction = addAction(tr("Information..."), [=]() { view->actions().displayModInformation(m_index.data(ModList::IndexRole).toInt()); }); + setDefaultAction(infoAction); + } +} + +void ModListContextMenu::addMenuAsPushButton(QMenu* menu) +{ + QPushButton* pushBtn = new QPushButton(menu->title()); + pushBtn->setMenu(menu); + QWidgetAction* action = new QWidgetAction(this); + action->setDefaultWidget(pushBtn); + addAction(action); +} + +void ModListContextMenu::addSendToContextMenu() +{ + QMenu* menu = new QMenu(m_view); + menu->setTitle(tr("Send to... ")); + menu->addAction(tr("Top"), [=]() { m_actions.sendModsToTop(m_selected); }); + menu->addAction(tr("Bottom"), [=]() { m_actions.sendModsToBottom(m_selected); }); + menu->addAction(tr("Priority..."), [=]() { m_actions.sendModsToPriority(m_selected); }); + menu->addAction(tr("Separator..."), [=]() { m_actions.sendModsToSeparator(m_selected); }); + addMenu(menu); +} + +void ModListContextMenu::addCategoryContextMenus(ModInfo::Ptr mod) +{ + ModListChangeCategoryMenu* categoriesMenu = new ModListChangeCategoryMenu(m_categories, mod, this); + connect(categoriesMenu, &QMenu::aboutToHide, [=]() { + m_actions.setCategories(m_selected, m_index, categoriesMenu->categories()); + }); + addMenuAsPushButton(categoriesMenu); + + ModListPrimaryCategoryMenu* primaryCategoryMenu = new ModListPrimaryCategoryMenu(m_categories, mod, this); + connect(primaryCategoryMenu, &QMenu::aboutToHide, [=]() { + int category = primaryCategoryMenu->primaryCategory(); + if (category != -1) { + m_actions.setPrimaryCategory(m_selected, category); + } + }); + addMenuAsPushButton(primaryCategoryMenu); +} + +void ModListContextMenu::addOverwriteActions(ModInfo::Ptr mod) +{ + if (QDir(mod->absolutePath()).count() > 2) { + addAction(tr("Sync to Mods..."), [=]() { m_core.syncOverwrite(); }); + addAction(tr("Create Mod..."), [=]() { m_actions.createModFromOverwrite(); }); + addAction(tr("Move content to Mod..."), [=]() { m_actions.moveOverwriteContentToExistingMod(); }); + addAction(tr("Clear Overwrite..."), [=]() { m_actions.clearOverwrite(); }); + } + addAction(tr("Open in Explorer"), [=]() { m_actions.openExplorer(m_selected); }); +} + +void ModListContextMenu::addSeparatorActions(ModInfo::Ptr mod) +{ + addCategoryContextMenus(mod); + addSeparator(); + + + addAction(tr("Rename Separator..."), [=]() { m_actions.renameMod(m_index); }); + addAction(tr("Remove Separator..."), [=]() { m_actions.removeMods(m_selected); }); + addSeparator(); + + if (m_view->sortColumn() == ModList::COL_PRIORITY) { + addSendToContextMenu(); + addSeparator(); + } + addAction(tr("Select Color..."), [=]() { m_actions.setColor(m_selected, m_index); }); + + if (mod->color().isValid()) { + addAction(tr("Reset Color"), [=]() { m_actions.resetColor(m_selected); }); + } + + addSeparator(); +} + +void ModListContextMenu::addForeignActions(ModInfo::Ptr mod) +{ + if (m_view->sortColumn() == ModList::COL_PRIORITY) { + addSendToContextMenu(); + } +} + +void ModListContextMenu::addBackupActions(ModInfo::Ptr mod) +{ + auto flags = mod->getFlags(); + addAction(tr("Restore Backup"), [=]() { m_actions.restoreBackup(m_index); }); + addAction(tr("Remove Backup..."), [=]() { m_actions.removeMods(m_selected); }); + addSeparator(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { + addAction(tr("Ignore missing data"), [=]() { m_actions.ignoreMissingData(m_selected); }); + } + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) { + addAction(tr("Mark as converted/working"), [=]() { m_actions.markConverted(m_selected); }); + } + addSeparator(); + if (mod->nexusId() > 0) { + addAction(tr("Visit on Nexus"), [=]() { m_actions.visitOnNexus(m_selected); }); + } + + const auto url = mod->parseCustomURL(); + if (url.isValid()) { + addAction(tr("Visit on %1").arg(url.host()), [=]() { m_actions.visitWebPage(m_selected); }); + } + + addAction(tr("Open in Explorer"), [=]() { m_actions.openExplorer(m_selected); }); +} + +void ModListContextMenu::addRegularActions(ModInfo::Ptr mod) +{ + auto flags = mod->getFlags(); + + addCategoryContextMenus(mod); + addSeparator(); + + if (mod->downgradeAvailable()) { + addAction(tr("Change versioning scheme"), [=]() { m_actions.changeVersioningScheme(m_index); }); + } + + if (mod->nexusId() > 0) + addAction(tr("Force-check updates"), [=]() { m_actions.checkModsForUpdates(m_selected); }); + if (mod->updateIgnored()) { + addAction(tr("Un-ignore update"), [=]() { m_actions.setIgnoreUpdate(m_selected, false); }); + } + else { + if (mod->updateAvailable() || mod->downgradeAvailable()) { + addAction(tr("Ignore update"), [=]() { m_actions.setIgnoreUpdate(m_selected, true); }); + } + } + addSeparator(); + + addAction(tr("Enable selected"), [=]() { m_core.modList()->setActive(m_selected, true); }); + addAction(tr("Disable selected"), [=]() { m_core.modList()->setActive(m_selected, false); }); + + addSeparator(); + + + if (m_view->sortColumn() == ModList::COL_PRIORITY) { + addSendToContextMenu(); + addSeparator(); + } + + addAction(tr("Rename Mod..."), [=]() { m_actions.renameMod(m_index); }); + addAction(tr("Reinstall Mod"), [=]() { m_actions.reinstallMod(m_index); }); + addAction(tr("Remove Mod..."), [=]() { m_actions.removeMods(m_selected); }); + addAction(tr("Create Backup"), [=]() { m_actions.createBackup(m_index); }); + + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) != flags.end()) { + addAction(tr("Restore hidden files"), [=]() { m_actions.restoreHiddenFiles(m_selected); }); + } + + addSeparator(); + + if (m_index.column() == ModList::COL_NOTES) { + addAction(tr("Select Color..."), [=]() { m_actions.setColor(m_selected, m_index); }); + if (mod->color().isValid()) { + addAction(tr("Reset Color"), [=]() { m_actions.resetColor(m_selected); }); + } + addSeparator(); + } + + if (mod->nexusId() > 0 && Settings::instance().nexus().endorsementIntegration()) { + switch (mod->endorsedState()) { + case EndorsedState::ENDORSED_TRUE: { + addAction(tr("Un-Endorse"), [=]() { m_actions.setEndorsed(m_selected, false); }); + } break; + case EndorsedState::ENDORSED_FALSE: { + addAction(tr("Endorse"), [=]() { m_actions.setEndorsed(m_selected, true); }); + addAction(tr("Won't endorse"), [=]() { m_actions.willNotEndorsed(m_selected); }); + } break; + case EndorsedState::ENDORSED_NEVER: { + addAction(tr("Endorse"), [=]() { m_actions.setEndorsed(m_selected, true); }); + } break; + default: { + QAction* action = new QAction(tr("Endorsement state unknown"), this); + action->setEnabled(false); + addAction(action); + } break; + } + } + + if (mod->nexusId() > 0 && Settings::instance().nexus().trackedIntegration()) { + switch (mod->trackedState()) { + case TrackedState::TRACKED_FALSE: { + addAction(tr("Start tracking"), [=]() { m_actions.setTracked(m_selected, true); }); + } break; + case TrackedState::TRACKED_TRUE: { + addAction(tr("Stop tracking"), [=]() { m_actions.setTracked(m_selected, false); }); + } break; + default: { + QAction* action = new QAction(tr("Tracked state unknown"), this); + action->setEnabled(false); + addAction(action); + } break; + } + } + + addSeparator(); + + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { + addAction(tr("Ignore missing data"), [=]() { m_actions.ignoreMissingData(m_selected); }); + } + + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_ALTERNATE_GAME) != flags.end()) { + addAction(tr("Mark as converted/working"), [=]() { m_actions.markConverted(m_selected); }); + } + + addSeparator(); + + if (mod->nexusId() > 0) { + addAction(tr("Visit on Nexus"), [=]() { m_actions.visitOnNexus(m_selected); }); + } + + const auto url = mod->parseCustomURL(); + if (url.isValid()) { + addAction(tr("Visit on %1").arg(url.host()), [=]() { m_actions.visitWebPage(m_selected); }); + } + + addAction(tr("Open in Explorer"), [=]() { m_actions.openExplorer(m_selected); }); +} diff --git a/src/modlistcontextmenu.h b/src/modlistcontextmenu.h new file mode 100644 index 00000000..2b3f9dcd --- /dev/null +++ b/src/modlistcontextmenu.h @@ -0,0 +1,119 @@ +#ifndef MODLISTCONTEXTMENU_H +#define MODLISTCONTEXTMENU_H + +#include <vector> + +#include <QMenu> +#include <QModelIndex> +#include <QTreeView> + +#include "modinfo.h" + +class CategoryFactory; +class ModListView; +class ModListViewActions; +class OrganizerCore; + +class ModListGlobalContextMenu : public QMenu +{ + Q_OBJECT +public: + + ModListGlobalContextMenu(OrganizerCore& core, ModListView* view, QWidget* parent = nullptr); + +protected: + + friend class ModListContextMenu; + + // creates a "All mods" context menu for the given index (can be invalid). + ModListGlobalContextMenu(OrganizerCore& core, ModListView* view, const QModelIndex& index, QWidget* parent = nullptr); + +}; + +class ModListChangeCategoryMenu : public QMenu +{ + Q_OBJECT +public: + + ModListChangeCategoryMenu( + CategoryFactory& categories, ModInfo::Ptr mod, QMenu* parent = nullptr); + + // return a list of pair <category id, enabled> from the menu + // + std::vector<std::pair<int, bool>> categories() const; + +private: + + // populate the tree with the category, using the enabled/disabled state from the + // given mod + // + bool populate(QMenu* menu, CategoryFactory& categories, ModInfo::Ptr mod, int targetId = 0); + + // internal implementation of categories() for recursion + // + std::vector<std::pair<int, bool>> categories(const QMenu* menu) const; +}; + +class ModListPrimaryCategoryMenu : public QMenu +{ + Q_OBJECT +public: + + ModListPrimaryCategoryMenu(CategoryFactory& categories, ModInfo::Ptr mod, QMenu* parent = nullptr); + + // return the selected primary category + // + int primaryCategory() const; + +private: + + // populate the categories + // + void populate(const CategoryFactory& categories, ModInfo::Ptr mod); + +}; + +class ModListContextMenu : public QMenu +{ + Q_OBJECT + +public: + + // creates a new context menu, the given index is the one for the click and should be valid + // + ModListContextMenu( + const QModelIndex& index, OrganizerCore& core, CategoryFactory& categories, ModListView* modListView); + +private: + + // adds the "Send to... " context menu + // + void addSendToContextMenu(); + + // adds the categories menu (change/primary) + // + void addCategoryContextMenus(ModInfo::Ptr mod); + + // special menu for categories + // + void addMenuAsPushButton(QMenu* menu); + + + // add actions/menus to this menu for each type of mod + // + void addOverwriteActions(ModInfo::Ptr mod); + void addSeparatorActions(ModInfo::Ptr mod); + void addForeignActions(ModInfo::Ptr mod); + void addBackupActions(ModInfo::Ptr mod); + void addRegularActions(ModInfo::Ptr mod); + + OrganizerCore& m_core; + CategoryFactory& m_categories; + QModelIndex m_index; + QModelIndexList m_selected; + ModListView* m_view; + ModListViewActions& m_actions; // shortcut for m_view->actions() + +}; + +#endif diff --git a/src/modlistdropinfo.cpp b/src/modlistdropinfo.cpp new file mode 100644 index 00000000..62a103a4 --- /dev/null +++ b/src/modlistdropinfo.cpp @@ -0,0 +1,124 @@ +#include "modlistdropinfo.h" + +#include "organizercore.h" + +ModListDropInfo::ModListDropInfo(const QMimeData* mimeData, OrganizerCore& core) : + m_rows{}, m_download{ -1 }, m_localUrls{}, m_url{} +{ + // this only check if the drop is valid, not if the content of the drop + // matches the target, a drop is valid if either + // 1. it contains items from another model (drag&drop in modlist or from download list) + // 2. it contains URLs from MO2 (overwrite or from another mod) + // 3. it contains a single URL to an external folder + // 4. it contains a single URL to a valid archive for MO2 + try { + if (mimeData->hasUrls()) { + for (auto& url : mimeData->urls()) { + auto p = relativeUrl(url); + if (p) { + m_localUrls.push_back(*p); + } + } + + // external drop + if (m_localUrls.empty() && mimeData->urls().size() == 1) { + auto url = mimeData->urls()[0]; + if (url.isLocalFile() && !relativeUrl(url)) { + QFileInfo info(url.toLocalFile()); + if (info.isDir()) { + m_url = url; + } + else if (core.installationManager()->getSupportedExtensions().contains(info.suffix(), Qt::CaseInsensitive)) { + m_url = url; + } + } + } + + } + else if (mimeData->hasText()) { + QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); + QDataStream stream(&encoded, QIODevice::ReadOnly); + + while (!stream.atEnd()) { + int sourceRow, col; + QMap<int, QVariant> roleDataMap; + stream >> sourceRow >> col >> roleDataMap; + if (col == 0) { + m_rows.push_back(sourceRow); + } + } + + if (mimeData->text() != ModListDropInfo::MOD_TEXT) { + if (mimeData->text() == ModListDropInfo::DOWNLOAD_TEXT && m_rows.size() == 1) { + m_download = m_rows[0]; + } + m_rows = {}; + } + } + } + catch (std::exception const&) { + m_rows = {}; + m_download = -1; + m_localUrls.clear(); + m_url = {}; + } +} + +std::optional<ModListDropInfo::RelativeUrl> ModListDropInfo::relativeUrl(const QUrl& url) const +{ + if (!url.isLocalFile()) { + return {}; + } + + QDir allModsDir(Settings::instance().paths().mods()); + QDir overwriteDir(Settings::instance().paths().overwrite()); + + QFileInfo sourceInfo(url.toLocalFile()); + QString sourceFile = sourceInfo.canonicalFilePath(); + + QString relativePath; + QString originName; + + if (sourceFile.startsWith(allModsDir.canonicalPath())) { + QDir relativeDir(allModsDir.relativeFilePath(sourceFile)); + QStringList splitPath = relativeDir.path().split("/"); + originName = splitPath[0]; + splitPath.pop_front(); + return { { url, splitPath.join("/"), originName } }; + } + else if (sourceFile.startsWith(overwriteDir.canonicalPath())) { + return { { url, overwriteDir.relativeFilePath(sourceFile), ModInfo::getOverwrite()->name() } }; + } + + return {}; +} + +bool ModListDropInfo::isValid() const +{ + return isLocalFileDrop() || isModDrop() || isDownloadDrop() || m_url.isLocalFile(); +} + +bool ModListDropInfo::isLocalFileDrop() const +{ + return !m_localUrls.empty(); +} + +bool ModListDropInfo::isModDrop() const +{ + return !m_rows.empty(); +} + +bool ModListDropInfo::isDownloadDrop() const +{ + return m_download != -1; +} + +bool ModListDropInfo::isExternalArchiveDrop() const +{ + return m_url.isLocalFile() && QFileInfo(m_url.toLocalFile()).isFile(); +} + +bool ModListDropInfo::isExternalFolderDrop() const +{ + return m_url.isLocalFile() && QFileInfo(m_url.toLocalFile()).isDir(); +} diff --git a/src/modlistdropinfo.h b/src/modlistdropinfo.h new file mode 100644 index 00000000..14a21691 --- /dev/null +++ b/src/modlistdropinfo.h @@ -0,0 +1,92 @@ +#ifndef MODLISTDROPINFO_H +#define MODLISTDROPINFO_H + +#include <optional> +#include <vector> + +#include <QMimeData> +#include <QString> +#include <QUrl> + +class OrganizerCore; + +// small class that extract information from mimeData +// +class ModListDropInfo { +public: + + // text value for the mime-data for the various possible + // origin (not for external drops) + // + static constexpr const char* MOD_TEXT = "mod"; + static constexpr const char* DOWNLOAD_TEXT = "download"; + +public: + + struct RelativeUrl { + const QUrl url; + const QString relativePath; + const QString originName; + }; + +public: + + ModListDropInfo(const QMimeData* mimeData, OrganizerCore& core); + + // returns true if this drop is valid + // + bool isValid() const; + + // returns true if these data corresponds to drag&drop + // of local files (e.g. from overwrite) + // + bool isLocalFileDrop() const; + + // returns true if these data corresponds to drag&drop + // of mod in the list + // + bool isModDrop() const; + + // returns true if these data corresponds to drag&drop + // from the download list + // + bool isDownloadDrop() const; + + // returns true if these data corresponds to dropping + // an archive for installation + // + bool isExternalArchiveDrop() const; + + // returns true if these data corresponds to dropping + // a folder for copy + // + bool isExternalFolderDrop() const; + + const auto& rows() const { return m_rows; } + const auto& download() const { return m_download; } + const auto& localUrls() const { return m_localUrls; } + const auto& externalUrl() const { return m_url; } + +private: + + friend class ModList; + + // retrieve the relative path of file and its origin given a URL from Mime data + // returns an empty optional if the URL is not a valid file for dropping + // + std::optional<RelativeUrl> relativeUrl(const QUrl&) const; + +private: + + // rows for drag&drop between views + std::vector<int> m_rows; + int m_download; // -1 if invalid + + // local URLs from the data (relative path + origin name) + std::vector<RelativeUrl> m_localUrls; + + // external URL + QUrl m_url; +}; + +#endif diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp index ca0f3bb1..9ae37a43 100644 --- a/src/modlistsortproxy.cpp +++ b/src/modlistsortproxy.cpp @@ -18,6 +18,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */
#include "modlistsortproxy.h"
+#include "modlistdropinfo.h"
#include "modinfo.h"
#include "profile.h"
#include "messagedialog.h"
@@ -69,43 +70,11 @@ void ModListSortProxy::setCriteria(const std::vector<Criteria>& criteria) if (changed || isForUpdates) {
m_Criteria = criteria;
updateFilterActive();
- invalidate();
+ invalidateFilter();
+ emit filterInvalidated();
}
}
-Qt::ItemFlags ModListSortProxy::flags(const QModelIndex &modelIndex) const
-{
- Qt::ItemFlags flags = sourceModel()->flags(mapToSource(modelIndex));
-
- return flags;
-}
-
-void ModListSortProxy::enableAllVisible()
-{
- if (m_Profile == nullptr) return;
-
- QList<unsigned int> modsToEnable;
- for (int i = 0; i < this->rowCount(); ++i) {
- int modID = mapToSource(index(i, 0)).data(Qt::UserRole + 1).toInt();
- modsToEnable.append(modID);
- }
- m_Profile->setModsEnabled(modsToEnable, QList<unsigned int>());
- invalidate();
-}
-
-void ModListSortProxy::disableAllVisible()
-{
- if (m_Profile == nullptr) return;
-
- QList<unsigned int> modsToDisable;
- for (int i = 0; i < this->rowCount(); ++i) {
- int modID = mapToSource(index(i, 0)).data(Qt::UserRole + 1).toInt();
- modsToDisable.append(modID);
- }
- m_Profile->setModsEnabled(QList<unsigned int>(), modsToDisable);
- invalidate();
-}
-
unsigned long ModListSortProxy::flagsId(const std::vector<ModInfo::EFlag> &flags) const
{
unsigned long result = 0;
@@ -133,12 +102,17 @@ bool ModListSortProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
if (sourceModel()->hasChildren(left) || sourceModel()->hasChildren(right)) {
- return QSortFilterProxyModel::lessThan(left, 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);
+ }
}
bool lOk, rOk;
- int leftIndex = left.data(Qt::UserRole + 1).toInt(&lOk);
- int rightIndex = right.data(Qt::UserRole + 1).toInt(&rOk);
+ int leftIndex = left.data(ModList::IndexRole).toInt(&lOk);
+ int rightIndex = right.data(ModList::IndexRole).toInt(&rOk);
+
if (!lOk || !rOk) {
return false;
}
@@ -146,18 +120,7 @@ bool ModListSortProxy::lessThan(const QModelIndex &left, ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex);
ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex);
- bool lt = false;
-
- {
- QModelIndex leftPrioIdx = left.sibling(left.row(), ModList::COL_PRIORITY);
- QVariant leftPrio = leftPrioIdx.data();
- if (!leftPrio.isValid()) leftPrio = left.data(Qt::UserRole);
- QModelIndex rightPrioIdx = right.sibling(right.row(), ModList::COL_PRIORITY);
- QVariant rightPrio = rightPrioIdx.data();
- if (!rightPrio.isValid()) rightPrio = right.data(Qt::UserRole);
-
- lt = leftPrio.toInt() < rightPrio.toInt();
- }
+ bool lt = left.data(ModList::PriorityRole).toInt() < right.data(ModList::PriorityRole).toInt();
switch (left.column()) {
case ModList::COL_FLAGS: {
@@ -264,7 +227,8 @@ void ModListSortProxy::updateFilter(const QString& filter) {
m_Filter = filter;
updateFilterActive();
- invalidate();
+ invalidateFilter();
+ emit filterInvalidated();
}
bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const
@@ -383,7 +347,7 @@ bool ModListSortProxy::categoryMatchesMod( b = (hasConflictFlag(info->getConflictFlags()));
break;
}
-
+
case CategoryFactory::HasHiddenFiles:
{
b = (info->hasFlag(ModInfo::FLAG_HIDDEN_FILES));
@@ -583,27 +547,44 @@ void ModListSortProxy::setOptions( if (m_FilterMode != mode || separators != m_FilterSeparators) {
m_FilterMode = mode;
m_FilterSeparators = separators;
- this->invalidate();
+ invalidateFilter();
+ emit filterInvalidated();
}
}
-bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex &parent) const
+bool ModListSortProxy::filterAcceptsRow(int source_row, const QModelIndex &parent) const
{
if (m_Profile == nullptr) {
return false;
}
- if (row >= static_cast<int>(m_Profile->numMods())) {
- log::warn("invalid row index: {}", row);
+ if (source_row >= static_cast<int>(m_Profile->numMods())) {
+ log::warn("invalid row index: {}", source_row);
return false;
}
- QModelIndex idx = sourceModel()->index(row, 0, parent);
+ 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;
@@ -612,23 +593,48 @@ bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex &parent) cons return false;
} else {
- bool modEnabled = idx.sibling(row, 0).data(Qt::CheckStateRole).toInt() == Qt::Checked;
- unsigned int index = idx.data(Qt::UserRole + 1).toInt();
+ bool modEnabled = idx.sibling(source_row, 0).data(Qt::CheckStateRole).toInt() == Qt::Checked;
return filterMatchesMod(ModInfo::getByIndex(index), modEnabled);
}
}
+bool ModListSortProxy::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const
+{
+ ModListDropInfo dropInfo(data, *m_Organizer);
+
+ if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
+ return false;
+ }
+
+ // disable drop install with group proxy, except the one for collapsible separator
+ // - it would be nice to be able to "install to category" or something like that but
+ // it's a bit more complicated since the drop position is based on the category, so
+ // just disabling for now
+ if (dropInfo.isDownloadDrop()) {
+ // maybe there is a cleaner way?
+ if (qobject_cast<QtGroupingProxy*>(sourceModel())) {
+ return false;
+ }
+ }
+
+ return QSortFilterProxyModel::canDropMimeData(data, action, row, column, parent);
+}
+
bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent)
{
- if (!data->hasUrls() && (sortColumn() != ModList::COL_PRIORITY)) {
+ ModListDropInfo dropInfo(data, *m_Organizer);
+
+ if (!dropInfo.isLocalFileDrop() && sortColumn() != ModList::COL_PRIORITY) {
QWidget *wid = qApp->activeWindow()->findChild<QTreeView*>("modList");
MessageDialog::showMessage(tr("Drag&Drop is only supported when sorting by priority"), wid);
return false;
}
- if ((row == -1) && (column == -1)) {
+
+ 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.
// This should fix that
@@ -636,21 +642,20 @@ bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action --row;
}
- QModelIndex proxyIndex = index(row, column, parent);
- QModelIndex sourceIndex = mapToSource(proxyIndex);
- return this->sourceModel()->dropMimeData(data, action, sourceIndex.row(), sourceIndex.column(),
- sourceIndex.parent());
+ return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent);
}
void ModListSortProxy::setSourceModel(QAbstractItemModel *sourceModel)
{
QSortFilterProxyModel::setSourceModel(sourceModel);
- QtGroupingProxy *proxy = qobject_cast<QtGroupingProxy*>(sourceModel);
+ QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(sourceModel);
if (proxy != nullptr) {
sourceModel = proxy->sourceModel();
}
- connect(sourceModel, SIGNAL(aboutToChangeData()), this, SLOT(aboutToChangeData()), Qt::UniqueConnection);
- connect(sourceModel, SIGNAL(postDataChanged()), this, SLOT(postDataChanged()), Qt::UniqueConnection);
+ if (sourceModel) {
+ connect(sourceModel, SIGNAL(aboutToChangeData()), this, SLOT(aboutToChangeData()), Qt::UniqueConnection);
+ connect(sourceModel, SIGNAL(postDataChanged()), this, SLOT(postDataChanged()), Qt::UniqueConnection);
+ }
}
void ModListSortProxy::aboutToChangeData()
diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h index 90b6251e..421c82cc 100644 --- a/src/modlistsortproxy.h +++ b/src/modlistsortproxy.h @@ -77,24 +77,11 @@ public: void setProfile(Profile *profile);
-
- virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const;
- virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action,
- int row, int column, const QModelIndex &parent);
+ 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 enable all mods visible under the current filter
- **/
- void enableAllVisible();
-
- /**
- * @brief disable all mods visible under the current filter
- **/
- void disableAllVisible();
-
/**
* @brief tests if a filtere matches for a mod
* @param info mod information
@@ -111,6 +98,9 @@ public: void setCriteria(const std::vector<Criteria>& criteria);
void setOptions(FilterMode mode, SeparatorsMode separators);
+ auto filterMode() const { return m_FilterMode; }
+ auto separatorsMode() const { return m_FilterSeparators; }
+
/**
* @brief tests if the specified index has child nodes
* @param parent the node to test
@@ -134,6 +124,7 @@ public slots: signals:
void filterActive(bool active);
+ void filterInvalidated();
protected:
diff --git a/src/modlistview.cpp b/src/modlistview.cpp index c6dbc6ea..4b285733 100644 --- a/src/modlistview.cpp +++ b/src/modlistview.cpp @@ -1,58 +1,1284 @@ #include "modlistview.h"
-#include <widgetutility.h>
#include <QUrl>
#include <QMimeData>
#include <QProxyStyle>
+#include <widgetutility.h>
+
+#include <utility.h>
+#include <report.h>
+
+#include "ui_mainwindow.h"
+
+#include "filterlist.h"
+#include "organizercore.h"
+#include "modlist.h"
+#include "modlistsortproxy.h"
+#include "modlistbypriorityproxy.h"
+#include "log.h"
+#include "modflagicondelegate.h"
+#include "modconflicticondelegate.h"
+#include "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 ModListStyledItemDelegated : public QStyledItemDelegate
+{
+ ModListView* m_view;
+
+public:
+
+ ModListStyledItemDelegated(ModListView* view) :
+ QStyledItemDelegate(view), m_view(view) { }
+
+ void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
+ {
+ // the parent version always overwrite the background brush, so
+ // we need to save it and restore it
+ auto backgroundColor = option->backgroundBrush.color();
+ QStyledItemDelegate::initStyleOption(option, index);
+
+ if (backgroundColor.isValid()) {
+ option->backgroundBrush = backgroundColor;
+ }
+ }
+
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
+ {
+ QStyleOptionViewItem opt(option);
+
+ // remove items indentaiton when using collapsible separators
+ if (index.column() == 0 && m_view->hasCollapsibleSeparators()) {
+ if (!index.model()->hasChildren(index) && index.parent().isValid()) {
+ auto parentIndex = index.parent().data(ModList::IndexRole).toInt();
+ if (ModInfo::getByIndex(parentIndex)->isSeparator()) {
+ opt.rect.adjust(-m_view->indentation(), 0, 0, 0);
+ }
+ }
+ }
+
+ // compute required color from children, otherwise fallback to the
+ // color from the model, and draw the background here
+ auto color = m_view->markerColor(index);
+ if (!color.isValid()) {
+ color = index.data(Qt::BackgroundRole).value<QColor>();
+ }
+ else {
+ // disable alternating row if the color is from the children
+ opt.features &= ~QStyleOptionViewItem::Alternate;
+ }
+ opt.backgroundBrush = color;
+
+ // compute ideal foreground color for some rows
+ if (color.isValid()) {
+ if ((index.column() == ModList::COL_NAME
+ && ModInfo::getByIndex(index.data(ModList::IndexRole).toInt())->isSeparator())
+ || index.column() == ModList::COL_NOTES) {
+ opt.palette.setBrush(QPalette::Text, ColorSettings::idealTextColor(color));
+ }
+ }
+
+ QStyledItemDelegate::paint(painter, opt, index);
+ }
+};
-class ModListViewStyle: public QProxyStyle {
+class ModListViewMarkingScrollBar : public ViewMarkingScrollBar {
+ ModListView* m_view;
public:
- ModListViewStyle(QStyle *style, int indentation);
+ 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;
+ }
- void drawPrimitive (PrimitiveElement element, const QStyleOption *option,
- QPainter *painter, const QWidget *widget = 0) const;
-private:
- int m_Indentation;
};
-ModListViewStyle::ModListViewStyle(QStyle *style, int indentation)
- : QProxyStyle(style), m_Indentation(indentation)
+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 ModListStyledItemDelegated(this));
+
+ connect(this, &ModListView::doubleClicked, this, &ModListView::onDoubleClicked);
+ connect(this, &ModListView::customContextMenuRequested, this, &ModListView::onCustomContextMenuRequested);
+
+ 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::setProfile(Profile* profile)
+{
+ m_sortProxy->setProfile(profile);
+ m_byPriorityProxy->setProfile(profile);
+}
+
+bool ModListView::hasCollapsibleSeparators() const
+{
+ return groupByMode() == GroupByMode::SEPARATOR;
+}
+
+int ModListView::sortColumn() const
+{
+ return m_sortProxy ? m_sortProxy->sortColumn() : -1;
+}
+
+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;
+ }
}
-void ModListViewStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
- QPainter *painter, const QWidget *widget) const
+ModListViewActions& ModListView::actions() const
{
- if (element == QStyle::PE_IndicatorItemViewItemDrop && !option->rect.isNull()) {
- QStyleOption opt(*option);
- opt.rect.setLeft(m_Indentation);
- if (widget) {
- opt.rect.setRight(widget->width() - 5); // 5 is an arbitrary value that seems to work ok
+ return *m_actions;
+}
+
+std::optional<unsigned int> ModListView::nextMod(unsigned int modIndex) const
+{
+ const QModelIndex start = indexModelToView(m_core->modList()->index(modIndex, 0));
+
+ auto index = start;
+
+ for (;;) {
+ index = nextIndex(index);
+
+ if (index == start || !index.isValid()) {
+ // wrapped around, give up
+ break;
+ }
+
+ modIndex = index.data(ModList::IndexRole).toInt();
+
+ ModInfo::Ptr mod = ModInfo::getByIndex(modIndex);
+
+ // skip overwrite and backups and separators
+ if (mod->hasFlag(ModInfo::FLAG_OVERWRITE) ||
+ mod->hasFlag(ModInfo::FLAG_BACKUP) ||
+ mod->hasFlag(ModInfo::FLAG_SEPARATOR)) {
+ continue;
}
- QProxyStyle::drawPrimitive(element, &opt, painter, widget);
- } else {
- QProxyStyle::drawPrimitive(element, option, painter, widget);
+
+ return modIndex;
}
+
+ return {};
}
-ModListView::ModListView(QWidget *parent)
- : QTreeView(parent)
- , m_Scrollbar(new ViewMarkingScrollBar(this->model(), this))
+std::optional<unsigned int> ModListView::prevMod(unsigned int modIndex) const
{
- setVerticalScrollBar(m_Scrollbar);
- MOBase::setCustomizableColumns(this);
+ 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 and backups and separators
+ ModInfo::Ptr mod = ModInfo::getByIndex(modIndex);
+
+ if (mod->hasFlag(ModInfo::FLAG_OVERWRITE) ||
+ mod->hasFlag(ModInfo::FLAG_BACKUP) ||
+ mod->hasFlag(ModInfo::FLAG_SEPARATOR)) {
+ continue;
+ }
+
+ return modIndex;
+ }
+
+ return {};
+}
+
+void ModListView::enableAllVisible()
+{
+ m_core->modList()->setActive(indexViewToModel(flatIndex(model())), true);
+}
+
+void ModListView::disableAllVisible()
+{
+ m_core->modList()->setActive(indexViewToModel(flatIndex(model())), false);
+}
+
+void ModListView::setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
+{
+ m_sortProxy->setCriteria(criteria);
+}
+
+void ModListView::setFilterOptions(ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep)
+{
+ m_sortProxy->setOptions(mode, sep);
+}
+
+bool ModListView::isModVisible(unsigned int index) const
+{
+ return m_sortProxy->filterMatchesMod(ModInfo::getByIndex(index), m_core->currentProfile()->modEnabled(index));
+}
+
+bool ModListView::isModVisible(ModInfo::Ptr mod) const
+{
+ return m_sortProxy->filterMatchesMod(mod, m_core->currentProfile()->modEnabled(ModInfo::getIndex(mod->name())));
+}
+
+QModelIndex ModListView::indexModelToView(const QModelIndex& index) const
+{
+ return ::indexModelToView(index, this);
+}
+
+QModelIndexList ModListView::indexModelToView(const QModelIndexList& index) const
+{
+ return ::indexModelToView(index, this);
+}
+
+QModelIndex ModListView::indexViewToModel(const QModelIndex& index) const
+{
+ return ::indexViewToModel(index, m_core->modList());
+}
+
+QModelIndexList ModListView::indexViewToModel(const QModelIndexList& index) const
+{
+ return ::indexViewToModel(index, m_core->modList());
+}
+
+QModelIndex ModListView::nextIndex(const QModelIndex& index) const
+{
+ auto* model = index.model();
+
+ if (model->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();
+ auto prev = model->index((index.row() - 1) % model->rowCount(index.parent()), index.column(), index.parent());
+
+ if (model->rowCount(prev) > 0) {
+ return model->index(model->rowCount(prev) - 1, index.column(), prev);
+ }
+
+ return prev;
+}
+
+std::pair<QModelIndex, QModelIndexList> ModListView::selected() const
+{
+ return { indexViewToModel(currentIndex()), indexViewToModel(selectionModel()->selectedRows()) };
+}
+
+void ModListView::setSelected(const QModelIndex& current, const QModelIndexList& selected)
+{
+ setCurrentIndex(indexModelToView(current));
+ for (auto idx : selected) {
+ selectionModel()->select(indexModelToView(idx), QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ }
+}
+
+void ModListView::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);
+ }
+ }
+
+ for (unsigned int i = 0; i < m_core->currentProfile()->numMods(); ++i) {
+ int priority = m_core->currentProfile()->getModPriority(i);
+ if (m_core->currentProfile()->modEnabled(i)) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(i);
+ // priorities in the directory structure are one higher because data is 0
+ m_core->directoryStructure()->getOriginByName(MOBase::ToWString(modInfo->internalName())).setPriority(priority + 1);
+ }
+ }
+ m_core->refreshBSAList();
+ m_core->currentProfile()->writeModlist();
+ m_core->directoryStructure()->getFileRegister()->sortOrigins();
+
+ { // refresh selection
+ QModelIndex current = currentIndex();
+ if (current.isValid()) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(current.data(ModList::IndexRole).toInt());
+ // clear caches on all mods conflicting with the moved mod
+ for (int i : modInfo->getModOverwrite()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModOverwritten()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveOverwrite()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveOverwritten()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveLooseOverwrite()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ for (int i : modInfo->getModArchiveLooseOverwritten()) {
+ ModInfo::getByIndex(i)->clearCaches();
+ }
+ // update conflict check on the moved mod
+ modInfo->doConflictCheck();
+ setOverwriteMarkers(modInfo);
+ }
+ }
+}
+
+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()) {
+ setExpanded(qIndex, true);
+ }
+
+ // focus, scroll to and select
+ setFocus(Qt::OtherFocusReason);
+ scrollTo(qIndex);
+ setCurrentIndex(qIndex);
+ selectionModel()->select(qIndex, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+}
+
+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("");
+ }
+}
+
+void ModListView::updateModCount()
+{
+ TimeThis tt("updateModCount");
+
+ int activeCount = 0;
+ int visActiveCount = 0;
+ int backupCount = 0;
+ int visBackupCount = 0;
+ int foreignCount = 0;
+ int visForeignCount = 0;
+ int separatorCount = 0;
+ int visSeparatorCount = 0;
+ int regularCount = 0;
+ int visRegularCount = 0;
+
+ QStringList allMods = m_core->modList()->allMods();
+
+ auto hasFlag = [](std::vector<ModInfo::EFlag> flags, ModInfo::EFlag filter) {
+ return std::find(flags.begin(), flags.end(), filter) != flags.end();
+ };
+
+ bool isEnabled;
+ bool isVisible;
+ for (QString mod : allMods) {
+ int modIndex = ModInfo::getIndex(mod);
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ std::vector<ModInfo::EFlag> modFlags = modInfo->getFlags();
+ isEnabled = m_core->currentProfile()->modEnabled(modIndex);
+ isVisible = m_sortProxy->filterMatchesMod(modInfo, isEnabled);
+
+ for (auto flag : modFlags) {
+ switch (flag) {
+ case ModInfo::FLAG_BACKUP: backupCount++;
+ if (isVisible)
+ visBackupCount++;
+ break;
+ case ModInfo::FLAG_FOREIGN: foreignCount++;
+ if (isVisible)
+ visForeignCount++;
+ break;
+ case ModInfo::FLAG_SEPARATOR: separatorCount++;
+ if (isVisible)
+ visSeparatorCount++;
+ break;
+ }
+ }
+
+ if (!hasFlag(modFlags, ModInfo::FLAG_BACKUP) &&
+ !hasFlag(modFlags, ModInfo::FLAG_FOREIGN) &&
+ !hasFlag(modFlags, ModInfo::FLAG_SEPARATOR) &&
+ !hasFlag(modFlags, ModInfo::FLAG_OVERWRITE)) {
+ if (isEnabled) {
+ activeCount++;
+ if (isVisible)
+ visActiveCount++;
+ }
+ if (isVisible)
+ visRegularCount++;
+ regularCount++;
+ }
+ }
+
+ ui.counter->display(visActiveCount);
+ ui.counter->setToolTip(tr("<table cellspacing=\"5\">"
+ "<tr><th>Type</th><th>All</th><th>Visible</th>"
+ "<tr><td>Enabled mods: </td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr>"
+ "<tr><td>Unmanaged/DLCs: </td><td align=right>%5</td><td align=right>%6</td></tr>"
+ "<tr><td>Mod backups: </td><td align=right>%7</td><td align=right>%8</td></tr>"
+ "<tr><td>Separators: </td><td align=right>%9</td><td align=right>%10</td></tr>"
+ "</table>")
+ .arg(activeCount)
+ .arg(regularCount)
+ .arg(visActiveCount)
+ .arg(visRegularCount)
+ .arg(foreignCount)
+ .arg(visForeignCount)
+ .arg(backupCount)
+ .arg(visBackupCount)
+ .arg(separatorCount)
+ .arg(visSeparatorCount)
+ );
+}
+
+void ModListView::refreshFilters()
+{
+ auto [current, sourceRows] = selected();
+
+ setCurrentIndex(QModelIndex());
+ m_filters->refresh();
+
+ setSelected(current, sourceRows);
+}
+
+void ModListView::onExternalFolderDropped(const QUrl& url, int priority)
+{
+ QFileInfo fileInfo(url.toLocalFile());
+
+ GuessedValue<QString> name;
+ name.setFilter(&fixDirectoryName);
+ name.update(fileInfo.fileName(), GUESS_PRESET);
+
+ do {
+ bool ok;
+ name.update(QInputDialog::getText(this, tr("Copy Folder..."),
+ tr("This will copy the content of %1 to a new mod.\n"
+ "Please enter the name:").arg(fileInfo.fileName()), QLineEdit::Normal, name, &ok),
+ GUESS_USER);
+ if (!ok) {
+ return;
+ }
+ } while (name->isEmpty());
+
+ if (m_core->modList()->getMod(name) != nullptr) {
+ reportError(tr("A mod with this name already exists."));
+ return;
+ }
+
+ IModInterface* newMod = m_core->createMod(name);
+ if (!newMod) {
+ return;
+ }
+
+ // TODO: this is currently a silent copy, which can take some time, but there is
+ // no clean method to do this in uibase
+ if (!copyDir(fileInfo.absoluteFilePath(), newMod->absolutePath(), true)) {
+ return;
+ }
+
+ m_core->refresh();
+
+ if (priority != -1) {
+ m_core->modList()->changeModPriority(ModInfo::getIndex(name), priority);
+ }
+}
+
+bool ModListView::moveSelection(int key)
+{
+ auto [cindex, sourceRows] = selected();
+
+ int offset = key == Qt::Key_Up ? -1 : 1;
+ if (m_sortProxy->sortOrder() == Qt::DescendingOrder) {
+ offset = -offset;
+ }
+
+ m_core->modList()->shiftModsPriority(sourceRows, offset);
+
+ // reset the selection and the index
+ setSelected(cindex, sourceRows);
+
+ 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();
+ QAbstractProxyModel* nextProxy = nullptr;
+
+ if (groupIndex == GroupBy::CATEGORY) {
+ nextProxy = m_byCategoryProxy;
+ }
+ else if (groupIndex == GroupBy::NEXUS_ID) {
+ nextProxy = m_byNexusIdProxy;
+ }
+ else if (m_core->settings().interface().collapsibleSeparators()
+ && m_sortProxy->sortColumn() == ModList::COL_PRIORITY
+ && m_sortProxy->sortOrder() == Qt::AscendingOrder) {
+ nextProxy = m_byPriorityProxy;
+ }
+
+ QAbstractItemModel* nextModel = m_core->modList();
+ if (nextProxy) {
+ nextProxy->setSourceModel(m_core->modList());
+ nextModel = nextProxy;
+ }
+
+ if (nextModel != previousModel) {
+ m_sortProxy->setSourceModel(nextModel);
+
+ // reset the source model of the old proxy because we do not want to
+ // react to signals
+ //
+ if (auto* proxy = qobject_cast<QAbstractProxyModel*>(previousModel)) {
+ proxy->setSourceModel(nullptr);
+ }
+ }
+
+ // expand items previously expanded
+ refreshExpandedItems();
+
+ if (hasCollapsibleSeparators()) {
+ ui.filterSeparators->setCurrentIndex(ModListSortProxy::SeparatorFilter);
+ ui.filterSeparators->setEnabled(false);
+ }
+ else {
+ ui.filterSeparators->setEnabled(true);
+ }
+}
+
+void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw, Ui::MainWindow* mwui)
+{
+ // attributes
+ m_core = &core;
+ m_filters.reset(new FilterList(mwui, core, factory));
+ m_categories = &factory;
+ m_actions = new ModListViewActions(core, *m_filters, factory, this, mwui->espList, mw);
+ ui = {
+ mwui->groupCombo, mwui->activeModsCounter, mwui->modFilterEdit,
+ mwui->currentCategoryLabel, mwui->clearFiltersButton, mwui->filtersSeparators
+ };
+
+ connect(m_core, &OrganizerCore::modInstalled, [=](auto&& name) { onModInstalled(name); });
+ connect(core.modList(), &ModList::modPrioritiesChanged, [=](auto&& indices) { onModPrioritiesChanged(indices); });
+ connect(core.modList(), &ModList::clearOverwrite, [=] { m_actions->clearOverwrite(); });
+ connect(core.modList(), &ModList::modStatesChanged, [=] { updateModCount(); });
+ connect(core.modList(), &ModList::modelReset, [=] { clearOverwriteMarkers(); });
+
+ // proxy for various group by
+ m_byPriorityProxy = new ModListByPriorityProxy(core.currentProfile(), core, this);
+ m_byCategoryProxy = new QtGroupingProxy(QModelIndex(), ModList::COL_CATEGORY, ModList::GroupingRole, 0, ModList::AggrRole);
+ m_byNexusIdProxy = new QtGroupingProxy(QModelIndex(), ModList::COL_MODID, ModList::GroupingRole,
+ QtGroupingProxy::FLAG_NOGROUPNAME | QtGroupingProxy::FLAG_NOSINGLE, ModList::AggrRole);
+
+ // we need to store the expanded/collapsed state of all items and restore them 1) when
+ // switching proxies, 2) when filtering and 3) when reseting the mod list.
+ connect(this, &QTreeView::expanded, [=](const QModelIndex& index) {
+ auto it = m_collapsed[m_sortProxy->sourceModel()].find(index.data(Qt::DisplayRole).toString());
+ if (it != m_collapsed[m_sortProxy->sourceModel()].end()) {
+ m_collapsed[m_sortProxy->sourceModel()].erase(it);
+ }
+ });
+ connect(this, &QTreeView::collapsed, [=](const QModelIndex& index) {
+ m_collapsed[m_sortProxy->sourceModel()].insert(index.data(Qt::DisplayRole).toString());
+ });
+
+ // the top-level proxy
+ m_sortProxy = new ModListSortProxy(core.currentProfile(), &core);
+ setModel(m_sortProxy);
+ connect(m_sortProxy, &ModList::modelReset, [=] { refreshExpandedItems(); });
+
+ // update the proxy when changing the sort column/direction and the group
+ connect(m_sortProxy, &QAbstractItemModel::layoutAboutToBeChanged, [this](auto&& parents, auto&& hint) {
+ if (hint == QAbstractItemModel::VerticalSortHint) {
+ updateGroupByProxy();
+ }
+ });
+ connect(ui.groupBy, QOverload<int>::of(&QComboBox::currentIndexChanged), [=](int index) {
+ updateGroupByProxy();
+ onModFilterActive(m_sortProxy->isFilterActive());
+ });
+ sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
+
+ // inform the mod list about the type of item being dropped at the beginning of a drag
+ // and the position of the drop indicator at the end (only for by-priority)
+ connect(this, &ModListView::dragEntered, core.modList(), &ModList::onDragEnter);
+ connect(this, &ModListView::dropEntered, m_byPriorityProxy, &ModListByPriorityProxy::onDropEnter);
+
+ connect(model(), &QAbstractItemModel::layoutChanged, 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); });
+
+ GenericIconDelegate* contentDelegate = new GenericIconDelegate(this, ModList::ContentsRole, ModList::COL_CONTENT, 150);
+ ModFlagIconDelegate* flagDelegate = new ModFlagIconDelegate(this, ModList::COL_FLAGS, 120);
+ ModConflictIconDelegate* conflictFlagDelegate = new ModConflictIconDelegate(this, ModList::COL_CONFLICTFLAGS, 80);
+
+ connect(header(), &QHeaderView::sectionResized, contentDelegate, &GenericIconDelegate::columnResized);
+ connect(header(), &QHeaderView::sectionResized, flagDelegate, &ModFlagIconDelegate::columnResized);
+ connect(header(), &QHeaderView::sectionResized, conflictFlagDelegate, &ModConflictIconDelegate::columnResized);
+
+ setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate);
+ setItemDelegateForColumn(ModList::COL_CONFLICTFLAGS, conflictFlagDelegate);
+ setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate);
+
+ 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);
+ }
+
+ // highligth plugins
+ connect(selectionModel(), &QItemSelectionModel::selectionChanged, [=](auto&& selected) {
+ std::vector<unsigned int> modIndices;
+ for (auto& idx : selectionModel()->selectedRows()) {
+ modIndices.push_back(idx.data(ModList::IndexRole).toInt());
+ }
+ m_core->pluginList()->highlightPlugins(modIndices, *m_core->directoryStructure());
+ mwui->espList->verticalScrollBar()->repaint();
+ });
+
+ // prevent the name-column from being hidden
+ header()->setSectionHidden(ModList::COL_NAME, false);
+
+ connect(m_core->modList(), &ModList::downloadArchiveDropped, [=](int row, int priority) {
+ m_core->installDownload(row, priority);
+ });
+ connect(m_core->modList(), &ModList::externalArchiveDropped, [=](const QUrl& url, int priority) {
+ m_core->installArchive(url.toLocalFile(), priority, false, nullptr);
+ });
+ connect(m_core->modList(), &ModList::externalFolderDropped, this, &ModListView::onExternalFolderDropped);
+
+ connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &ModListView::onSelectionChanged);
+
+ // 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 ModListStyledItemDelegated)
+ 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);
+}
+
+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 std::set<unsigned int>& overwrite, const std::set<unsigned int>& overwritten)
+{
+ m_markers.overwrite = overwrite;
+ m_markers.overwritten = overwritten;
+}
+
+void ModListView::setArchiveOverwriteMarkers(const std::set<unsigned int>& overwrite, const std::set<unsigned int>& overwritten)
+{
+ m_markers.archiveOverwrite = overwrite;
+ m_markers.archiveOverwritten = overwritten;
+}
+
+void ModListView::setArchiveLooseOverwriteMarkers(const std::set<unsigned int>& overwrite, const std::set<unsigned int>& overwritten)
+{
+ m_markers.archiveLooseOverwrite = overwrite;
+ m_markers.archiveLooseOverwritten = overwritten;
+}
+
+void ModListView::setOverwriteMarkers(ModInfo::Ptr mod)
+{
+ if (mod) {
+ setOverwriteMarkers(mod->getModOverwrite(), mod->getModOverwritten());
+ setArchiveOverwriteMarkers(mod->getModArchiveOverwrite(), mod->getModArchiveOverwritten());
+ setArchiveLooseOverwriteMarkers(mod->getModArchiveLooseOverwrite(), mod->getModArchiveLooseOverwritten());
+ }
+ else {
+ setOverwriteMarkers({}, {});
+ setArchiveOverwriteMarkers({}, {});
+ setArchiveLooseOverwriteMarkers({}, {});
+ }
+ dataChanged(model()->index(0, 0), model()->index(model()->rowCount(), model()->columnCount()));
+ verticalScrollBar()->repaint();
+}
+
+void ModListView::setHighlightedMods(const std::vector<unsigned int>& pluginIndices)
+{
+ m_markers.highlight.clear();
+ auto& directoryEntry = *m_core->directoryStructure();
+ for (auto idx : pluginIndices) {
+ QString pluginName = m_core->pluginList()->getName(idx);
+
+ const MOShared::FileEntryPtr fileEntry = directoryEntry.findFile(pluginName.toStdWString());
+ if (fileEntry.get() != nullptr) {
+ QString originName = QString::fromStdWString(directoryEntry.getOriginByID(fileEntry->getOrigin()).getName());
+ const auto index = ModInfo::getIndex(originName);
+ if (index != UINT_MAX) {
+ m_markers.highlight.insert(index);
+ }
+ }
+ }
+ dataChanged(model()->index(0, 0), model()->index(model()->rowCount(), model()->columnCount()));
+ verticalScrollBar()->repaint();
+}
+
+QColor ModListView::markerColor(const QModelIndex& index) const
+{
+ unsigned int modIndex = index.data(ModList::IndexRole).toInt();
+ bool highligth = m_markers.highlight.find(modIndex) != m_markers.highlight.end();
+ bool overwrite = m_markers.overwrite.find(modIndex) != m_markers.overwrite.end();
+ bool archiveOverwrite = m_markers.archiveOverwrite.find(modIndex) != m_markers.archiveOverwrite.end();
+ bool archiveLooseOverwrite = m_markers.archiveOverwritten.find(modIndex) != m_markers.archiveOverwritten.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().collapsibleSeparatorsConflicts()
+ && model()->hasChildren(rowIndex) && !isExpanded(rowIndex)) {
+
+ std::vector<QColor> colors;
+ for (int i = 0; i < model()->rowCount(rowIndex); ++i) {
+ auto childColor = markerColor(model()->index(i, index.column(), rowIndex));
+ if (childColor.isValid()) {
+ colors.push_back(childColor);
+ }
+ }
+
+ if (colors.empty()) {
+ return QColor();
+ }
+
+ int r = 0, g = 0, b = 0, a = 0;
+ for (auto& color : colors) {
+ r += color.red();
+ g += color.green();
+ b += color.blue();
+ a += color.alpha();
+ }
+
+ return QColor(r / colors.size(), g / colors.size(), b / colors.size(), a / colors.size());
+ }
+
+ return QColor();
+}
+
+std::vector<ModInfo::EConflictFlag> ModListView::conflictFlags(const QModelIndex& index, bool* forceCompact) const
+{
+ ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+
+ auto flags = info->getConflictFlags();
+ bool compact = false;
+ if (info->isSeparator()
+ && hasCollapsibleSeparators()
+ && m_core->settings().interface().collapsibleSeparatorsConflicts()
+ && !isExpanded(index.sibling(index.row(), 0))) {
+
+ // combine the child conflicts
+ std::set<ModInfo::EConflictFlag> eFlags(flags.begin(), flags.end());
+ for (int i = 0; i < model()->rowCount(index); ++i) {
+ auto cIndex = model()->index(i, index.column(), index).data(ModList::IndexRole).toInt();
+ auto cFlags = ModInfo::getByIndex(cIndex)->getConflictFlags();
+ eFlags.insert(cFlags.begin(), cFlags.end());
+ }
+ flags = { eFlags.begin(), eFlags.end() };
+
+ // force compact because there can be a lots of flags here
+ compact = true;
+ }
+
+ if (forceCompact) {
+ *forceCompact = true;
+ }
+
+ return flags;
+}
+
+void ModListView::onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
+{
+ if (hasCollapsibleSeparators()) {
+ for (auto& idx : selected.indexes()) {
+ if (idx.parent().isValid() && !isExpanded(idx.parent())) {
+ setExpanded(idx.parent(), true);
+ }
+ }
+ }
+
+ if (selected.count()) {
+ auto index = selected.indexes().last();
+ ModInfo::Ptr selectedMod = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt());
+ setOverwriteMarkers(selectedMod);
+ }
+ else {
+ setOverwriteMarkers(nullptr);
+ }
+
}
-void ModListView::dragEnterEvent(QDragEnterEvent *event)
+void ModListView::onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
{
- emit dropModeUpdate(event->mimeData()->hasUrls());
+ setFilterCriteria(criteria);
+
+ QString label = "?";
+
+ if (criteria.empty()) {
+ label = "";
+ }
+ else if (criteria.size() == 1) {
+ const auto& c = criteria[0];
+
+ if (c.type == ModListSortProxy::TypeContent) {
+ const auto* content = m_core->modDataContents().findById(c.id);
+ label = content ? content->name() : QString();
+ }
+ else {
+ label = m_categories->getCategoryNameByID(c.id);
+ }
+
+ if (label.isEmpty()) {
+ log::error("category {}:{} not found", c.type, c.id);
+ }
+ }
+ else {
+ label = tr("<Multiple>");
+ }
+ ui.currentCategory->setText(label);
+}
+
+void ModListView::dragEnterEvent(QDragEnterEvent* event)
+{
+ // this event is used by the modlist to check if we are draggin
+ // to a mod (local files) or to a priority (mods, downloads, external
+ // files)
+ emit dragEntered(event->mimeData());
QTreeView::dragEnterEvent(event);
+
+ // there is no drop event for invalid data since canDropMimeData
+ // returns false, so we notify user on drag enter
+ ModListDropInfo dropInfo(event->mimeData(), *m_core);
+
+ if (dropInfo.isValid() && !dropInfo.isLocalFileDrop()
+ && sortColumn() != ModList::COL_PRIORITY) {
+ log::warn("Drag&Drop is only supported when sorting by priority.");
+ }
+}
+
+void ModListView::dragMoveEvent(QDragMoveEvent* event)
+{
+ // this replace the openTimer from QTreeView to prevent
+ // auto-collapse of items
+ if (autoExpandDelay() >= 0) {
+ m_openTimer.start(autoExpandDelay(), this);
+ }
+
+ // see selectedIndexes()
+ m_inDragMoveEvent = true;
+ QAbstractItemView::dragMoveEvent(event);
+ m_inDragMoveEvent = false;
+}
+
+void ModListView::dropEvent(QDropEvent* event)
+{
+ // 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(), static_cast<DropPosition>(dropIndicatorPosition()));
+
+ // see selectedIndexes()
+ m_inDragMoveEvent = true;
+ QTreeView::dropEvent(event);
+ m_inDragMoveEvent = false;
}
-void ModListView::setModel(QAbstractItemModel *model)
+void ModListView::timerEvent(QTimerEvent* event)
{
- QTreeView::setModel(model);
- setVerticalScrollBar(new ViewMarkingScrollBar(model, this));
+ // 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, true);
+ }
+ m_openTimer.stop();
+ }
+ else {
+ QTreeView::timerEvent(event);
+ }
}
+bool ModListView::event(QEvent* event)
+{
+ if (event->type() == QEvent::KeyPress
+ && m_core->currentProfile()
+ && selectionModel()->hasSelection()) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+
+ auto index = selectionModel()->currentIndex();
+
+ if (keyEvent->modifiers() == Qt::ControlModifier) {
+ // ctrl+enter open explorer
+ if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
+ if (selectionModel()->selectedRows().count() == 1) {
+ m_actions->openExplorer({ indexViewToModel(index) });
+ return true;
+ }
+ }
+ // ctrl+up/down move selection
+ else if (sortColumn() == ModList::COL_PRIORITY
+ && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) {
+ return moveSelection(keyEvent->key());
+ }
+ }
+ else if (keyEvent->modifiers() == Qt::ShiftModifier) {
+ // shift+enter expand
+ if ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
+ && selectionModel()->selectedRows().count() == 1) {
+ if (model()->hasChildren(index)) {
+ setExpanded(index, !isExpanded(index));
+ }
+ else if (index.parent().isValid()) {
+ setExpanded(index.parent(), false);
+ selectionModel()->select(index.parent(),
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ setCurrentIndex(index.parent());
+ }
+ }
+ }
+ else {
+ if (keyEvent->key() == Qt::Key_Delete) {
+ return removeSelection();
+ }
+ else if (keyEvent->key() == Qt::Key_Space) {
+ return toggleSelectionState();
+ }
+ }
+ return QTreeView::event(event);
+ }
+ return QTreeView::event(event);
+}
diff --git a/src/modlistview.h b/src/modlistview.h index 6cd114b0..2fed4969 100644 --- a/src/modlistview.h +++ b/src/modlistview.h @@ -1,24 +1,280 @@ #ifndef MODLISTVIEW_H
#define MODLISTVIEW_H
+#include <map>
+#include <set>
+#include <vector>
+
+#include <QLabel>
#include <QTreeView>
#include <QDragEnterEvent>
+#include <QLCDNumber>
+
+#include "qtgroupingproxy.h"
#include "viewmarkingscrollbar.h"
+#include "modlistsortproxy.h"
+
+namespace Ui { class MainWindow; }
+
+class CategoryFactory;
+class FilterList;
+class OrganizerCore;
+class MainWindow;
+class Profile;
+class ModListByPriorityProxy;
+class ModListViewActions;
class 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);
- virtual void dragEnterEvent(QDragEnterEvent *event);
- virtual void setModel(QAbstractItemModel *model);
+ 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;
+
+ // set the current profile
+ //
+ void setProfile(Profile* profile);
+
+ // check if collapsible separators are currently used
+ //
+ bool hasCollapsibleSeparators() const;
+
+ // the column by which the mod list is currently sorted
+ //
+ int sortColumn() const;
+
+ // the current group mode
+ //
+ GroupByMode groupByMode() const;
+
+ // retrieve the actions from the view
+ //
+ ModListViewActions& actions() const;
+
+ // retrieve the next/previous mod in the current view, the given index
+ // should be a mod index (not a model row)
+ //
+ std::optional<unsigned int> nextMod(unsigned int index) const;
+ std::optional<unsigned int> prevMod(unsigned int index) const;
+
+ // check if the given mod is visible
+ //
+ bool isModVisible(unsigned int index) const;
+ bool isModVisible(ModInfo::Ptr mod) const;
+
+ // refresh the view (to call when settings have been changed)
+ //
+ void refresh();
+
signals:
- void dropModeUpdate(bool dropOnRows);
-
+
+ void dragEntered(const QMimeData* mimeData);
+ void dropEntered(const QMimeData* mimeData, DropPosition position);
+
public slots:
+
+ // enable/disable all visible mods
+ //
+ void enableAllVisible();
+ void disableAllVisible();
+
+ // set the filter criteria/options for mods
+ //
+ void setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria);
+ void setFilterOptions(ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep);
+
+ // update the mod counter
+ //
+ void updateModCount();
+
+ // refresh the filters
+ //
+ void refreshFilters();
+
+ // set highligth markers
+ //
+ void setHighlightedMods(const std::vector<unsigned int>& pluginIndices);
+
+protected:
+
+ friend class ModListContextMenu;
+ friend class ModListViewActions;
+
+ // 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;
+ bool event(QEvent* event) override;
+
+protected slots:
+
+ void onCustomContextMenuRequested(const QPoint& pos);
+ void onDoubleClicked(const QModelIndex& index);
+ void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);
+ void onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& filters);
+
private:
- ViewMarkingScrollBar *m_Scrollbar;
+ friend class ModConflictIconDelegate;
+ friend class ModListStyledItemDelegated;
+ friend class ModListViewMarkingScrollBar;
+
+ void onModPrioritiesChanged(const QModelIndexList& indices);
+ void onModInstalled(const QString& modName);
+ void onModFilterActive(bool filterActive);
+
+ // overwrite markers
+ void clearOverwriteMarkers();
+ void setOverwriteMarkers(const std::set<unsigned int>& overwrite, const std::set<unsigned int>& overwritten);
+ void setArchiveOverwriteMarkers(const std::set<unsigned int>& overwrite, const std::set<unsigned int>& overwritten);
+ void setArchiveLooseOverwriteMarkers(const std::set<unsigned int>& overwrite, const std::set<unsigned int>& overwritten);
+
+ // set overwrite markers from the mod and repaint (if mod is nullptr, clear overwrite and repaint)
+ //
+ void setOverwriteMarkers(ModInfo::Ptr mod);
+
+ // retrieve the marker color for the given index
+ //
+ QColor markerColor(const QModelIndex& index) const;
+
+ // retrieve the conflicts flags for the given index
+ //
+ std::vector<ModInfo::EConflictFlag> conflictFlags(
+ const QModelIndex& index, bool* forceCompact = nullptr) const;
+
+ // get/set the selected items on the view, this method return/take indices
+ // from the mod list model, not the view, so it's safe to restore
+ //
+ std::pair<QModelIndex, QModelIndexList> selected() const;
+ void setSelected(const QModelIndex& current, const QModelIndexList& selected);
+
+ // refresh stored expanded items for the current intermediate proxy
+ //
+ void refreshExpandedItems();
+
+ // refresh the group-by proxy, if the index is -1 will refresh the
+ // current one (e.g. when changing the sort column)
+ //
+ void updateGroupByProxy();
+
+ // index in the groupby combo
+ //
+ enum GroupBy {
+ NONE = 0,
+ CATEGORY = 1,
+ NEXUS_ID = 2
+ };
+
+private:
+
+ struct ModListViewUi
+ {
+ // the group by combo box
+ QComboBox* groupBy;
+
+ // the mod counter
+ QLCDNumber* counter;
+
+ // filters related
+ QLineEdit* filter;
+ QLabel* currentCategory;
+ QPushButton* clearFilters;
+ QComboBox* filterSeparators;
+ };
+
+ OrganizerCore* m_core;
+ std::unique_ptr<FilterList> m_filters;
+ CategoryFactory* m_categories;
+ ModListViewUi ui;
+ ModListViewActions* m_actions;
+
+ ModListSortProxy* m_sortProxy;
+ ModListByPriorityProxy* m_byPriorityProxy;
+ QtGroupingProxy* m_byCategoryProxy;
+ QtGroupingProxy* m_byNexusIdProxy;
+
+ // maintain collapsed items for each proxy to avoid
+ // losing them on model reset
+ std::map<QAbstractItemModel*, std::set<QString>> m_collapsed;
+
+ struct MarkerInfos {
+ // conflicts
+ std::set<unsigned int> overwrite;
+ std::set<unsigned int> overwritten;
+ std::set<unsigned int> archiveOverwrite;
+ std::set<unsigned int> archiveOverwritten;
+ std::set<unsigned int> archiveLooseOverwrite;
+ std::set<unsigned int> archiveLooseOverwritten;
+
+ // selected plugins
+ std::set<unsigned int> highlight;
+ } 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/modlistviewactions.cpp b/src/modlistviewactions.cpp new file mode 100644 index 00000000..9957618a --- /dev/null +++ b/src/modlistviewactions.cpp @@ -0,0 +1,1142 @@ +#include "modlistviewactions.h" + +#include <QGridLayout> +#include <QGroupBox> +#include <QInputDialog> +#include <QLabel> +#include <QRegExp> + +#include <log.h> +#include <report.h> +#include <utility.h> + +#include "categories.h" +#include "filedialogmemory.h" +#include "filterlist.h" +#include "listdialog.h" +#include "modinfodialog.h" +#include "modlist.h" +#include "modlistview.h" +#include "messagedialog.h" +#include "nexusinterface.h" +#include "nxmaccessmanager.h" +#include "savetextasdialog.h" +#include "organizercore.h" +#include "overwriteinfodialog.h" +#include "csvbuilder.h" +#include "pluginlistview.h" +#include "shared/filesorigin.h" +#include "shared/directoryentry.h" +#include "shared/fileregister.h" +#include "directoryrefresher.h" + +using namespace MOBase; +using namespace MOShared; + + +ModListViewActions::ModListViewActions( + OrganizerCore& core, FilterList& filters, CategoryFactory& categoryFactory, + ModListView* view, PluginListView* pluginView, QObject* nxmReceiver) : + QObject(view) + , m_core(core) + , m_filters(filters) + , m_categories(categoryFactory) + , m_view(view) + , m_pluginView(pluginView) + , m_parent(view->topLevelWidget()) + , m_receiver(nxmReceiver) +{ + +} + +void ModListViewActions::installMod(const QString& archivePath) const +{ + try { + QString path = archivePath; + if (path.isEmpty()) { + QStringList extensions = m_core.installationManager()->getSupportedExtensions(); + for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) { + *iter = "*." + *iter; + } + + path = FileDialogMemory::getOpenFileName("installMod", m_parent, tr("Choose Mod"), QString(), + tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" ")))); + } + + if (path.isEmpty()) { + return; + } + else { + m_core.installMod(path, false, nullptr, QString()); + } + } + catch (const std::exception& e) { + reportError(e.what()); + } +} + +void ModListViewActions::createEmptyMod(const QModelIndex& index) const +{ + GuessedValue<QString> name; + name.setFilter(&fixDirectoryName); + + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(m_parent, tr("Create Mod..."), + tr("This will create an empty mod.\n" + "Please enter a name:"), QLineEdit::Normal, "", &ok), + GUESS_USER); + if (!ok) { + return; + } + } + + if (m_core.modList()->getMod(name) != nullptr) { + reportError(tr("A mod with this name already exists")); + return; + } + + int newPriority = -1; + if (index.isValid() && m_view->sortColumn() == ModList::COL_PRIORITY) { + newPriority = m_core.currentProfile()->getModPriority(index.data(ModList::IndexRole).toInt()); + } + + IModInterface* newMod = m_core.createMod(name); + if (newMod == nullptr) { + return; + } + + m_core.refresh(); + + if (newPriority >= 0) { + m_core.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); + } +} + +void ModListViewActions::createSeparator(const QModelIndex& index) const +{ + GuessedValue<QString> name; + name.setFilter(&fixDirectoryName); + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(m_parent, tr("Create Separator..."), + tr("This will create a new separator.\n" + "Please enter a name:"), QLineEdit::Normal, "", &ok), + GUESS_USER); + if (!ok) { return; } + } + if (m_core.modList()->getMod(name) != nullptr) { + reportError(tr("A separator with this name already exists")); + return; + } + name->append("_separator"); + if (m_core.modList()->getMod(name) != nullptr) { + return; + } + + int newPriority = -1; + if (index.isValid() && m_view->sortColumn() == ModList::COL_PRIORITY) { + newPriority = m_core.currentProfile()->getModPriority(index.data(ModList::IndexRole).toInt()); + } + + if (m_core.createMod(name) == nullptr) { return; } + m_core.refresh(); + + if (newPriority >= 0) { + m_core.modList()->changeModPriority(ModInfo::getIndex(name), newPriority); + } + + if (auto c = m_core.settings().colors().previousSeparatorColor()) { + ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(*c); + } +} + +void ModListViewActions::checkModsForUpdates() const +{ + bool checkingModsForUpdate = false; + if (NexusInterface::instance().getAccessManager()->validated()) { + checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_core.pluginContainer(), m_receiver); + NexusInterface::instance().requestEndorsementInfo(m_receiver, QVariant(), QString()); + NexusInterface::instance().requestTrackingInfo(m_receiver, QVariant(), QString()); + } else { + QString apiKey; + if (GlobalSettings::nexusApiKey(apiKey)) { + m_core.doAfterLogin([=] () { checkModsForUpdates(); }); + NexusInterface::instance().getAccessManager()->apiCheck(apiKey); + } else { + log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.")); + } + } + + bool updatesAvailable = false; + for (auto mod : m_core.modList()->allMods()) { + ModInfo::Ptr modInfo = ModInfo::getByName(mod); + if (modInfo->updateAvailable()) { + updatesAvailable = true; + break; + } + } + + if (updatesAvailable || checkingModsForUpdate) { + m_view->setFilterCriteria({{ + ModListSortProxy::TypeSpecial, + CategoryFactory::UpdateAvailable, + false} + }); + + m_filters.setSelection({{ + ModListSortProxy::TypeSpecial, + CategoryFactory::UpdateAvailable, + false + }}); + } +} + +void ModListViewActions::checkModsForUpdates(std::multimap<QString, int> const& IDs) const +{ + if (m_core.settings().network().offlineMode()) { + return; + } + + if (NexusInterface::instance().getAccessManager()->validated()) { + ModInfo::manualUpdateCheck(m_receiver, IDs); + } + else { + QString apiKey; + if (GlobalSettings::nexusApiKey(apiKey)) { + m_core.doAfterLogin([=]() { checkModsForUpdates(IDs); }); + NexusInterface::instance().getAccessManager()->apiCheck(apiKey); + } + else + log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.")); + } +} + +void ModListViewActions::checkModsForUpdates(const QModelIndexList& indices) const +{ + std::multimap<QString, int> ids; + for (auto& idx : indices) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + ids.insert(std::make_pair<QString, int>(info->gameName(), info->nexusId())); + } + checkModsForUpdates(ids); +} + +void ModListViewActions::exportModListCSV() const +{ + QDialog selection(m_parent); + QGridLayout* grid = new QGridLayout; + selection.setWindowTitle(tr("Export to csv")); + + QLabel* csvDescription = new QLabel(); + csvDescription->setText(tr("CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.\nYou can also use online editors and converters instead.")); + grid->addWidget(csvDescription); + + QGroupBox* groupBoxRows = new QGroupBox(tr("Select what mods you want export:")); + QRadioButton* all = new QRadioButton(tr("All installed mods")); + QRadioButton* active = new QRadioButton(tr("Only active (checked) mods from your current profile")); + QRadioButton* visible = new QRadioButton(tr("All currently visible mods in the mod list")); + + QVBoxLayout* vbox = new QVBoxLayout; + vbox->addWidget(all); + vbox->addWidget(active); + vbox->addWidget(visible); + vbox->addStretch(1); + groupBoxRows->setLayout(vbox); + + grid->addWidget(groupBoxRows); + + QButtonGroup* buttonGroupRows = new QButtonGroup(); + buttonGroupRows->addButton(all, 0); + buttonGroupRows->addButton(active, 1); + buttonGroupRows->addButton(visible, 2); + buttonGroupRows->button(0)->setChecked(true); + + QGroupBox* groupBoxColumns = new QGroupBox(tr("Choose what Columns to export:")); + groupBoxColumns->setFlat(true); + + QCheckBox* mod_Priority = new QCheckBox(tr("Mod_Priority")); + mod_Priority->setChecked(true); + QCheckBox* mod_Name = new QCheckBox(tr("Mod_Name")); + mod_Name->setChecked(true); + QCheckBox* mod_Note = new QCheckBox(tr("Notes_column")); + QCheckBox* mod_Status = new QCheckBox(tr("Mod_Status")); + mod_Status->setChecked(true); + QCheckBox* primary_Category = new QCheckBox(tr("Primary_Category")); + QCheckBox* nexus_ID = new QCheckBox(tr("Nexus_ID")); + QCheckBox* mod_Nexus_URL = new QCheckBox(tr("Mod_Nexus_URL")); + QCheckBox* mod_Version = new QCheckBox(tr("Mod_Version")); + QCheckBox* install_Date = new QCheckBox(tr("Install_Date")); + QCheckBox* download_File_Name = new QCheckBox(tr("Download_File_Name")); + + QVBoxLayout* vbox1 = new QVBoxLayout; + vbox1->addWidget(mod_Priority); + vbox1->addWidget(mod_Name); + vbox1->addWidget(mod_Status); + vbox1->addWidget(mod_Note); + vbox1->addWidget(primary_Category); + vbox1->addWidget(nexus_ID); + vbox1->addWidget(mod_Nexus_URL); + vbox1->addWidget(mod_Version); + vbox1->addWidget(install_Date); + vbox1->addWidget(download_File_Name); + groupBoxColumns->setLayout(vbox1); + + grid->addWidget(groupBoxColumns); + + QPushButton* ok = new QPushButton("Ok"); + QPushButton* cancel = new QPushButton("Cancel"); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + connect(buttons, SIGNAL(accepted()), &selection, SLOT(accept())); + connect(buttons, SIGNAL(rejected()), &selection, SLOT(reject())); + + grid->addWidget(buttons); + + selection.setLayout(grid); + + + if (selection.exec() == QDialog::Accepted) { + + unsigned int numMods = ModInfo::getNumMods(); + int selectedRowID = buttonGroupRows->checkedId(); + + try { + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + CSVBuilder builder(&buffer); + builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS); + std::vector<std::pair<QString, CSVBuilder::EFieldType> > fields; + if (mod_Priority->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Priority"), CSVBuilder::TYPE_STRING)); + if (mod_Status->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Status"), CSVBuilder::TYPE_STRING)); + if (mod_Name->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Name"), CSVBuilder::TYPE_STRING)); + if (mod_Note->isChecked()) + fields.push_back(std::make_pair(QString("#Note"), CSVBuilder::TYPE_STRING)); + if (primary_Category->isChecked()) + fields.push_back(std::make_pair(QString("#Primary_Category"), CSVBuilder::TYPE_STRING)); + if (nexus_ID->isChecked()) + fields.push_back(std::make_pair(QString("#Nexus_ID"), CSVBuilder::TYPE_INTEGER)); + if (mod_Nexus_URL->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Nexus_URL"), CSVBuilder::TYPE_STRING)); + if (mod_Version->isChecked()) + fields.push_back(std::make_pair(QString("#Mod_Version"), CSVBuilder::TYPE_STRING)); + if (install_Date->isChecked()) + fields.push_back(std::make_pair(QString("#Install_Date"), CSVBuilder::TYPE_STRING)); + if (download_File_Name->isChecked()) + fields.push_back(std::make_pair(QString("#Download_File_Name"), CSVBuilder::TYPE_STRING)); + + builder.setFields(fields); + + builder.writeHeader(); + + auto indexesByPriority = m_core.currentProfile()->getAllIndexesByPriority(); + for (auto& iter : indexesByPriority) { + ModInfo::Ptr info = ModInfo::getByIndex(iter.second); + bool enabled = m_core.currentProfile()->modEnabled(iter.second); + if ((selectedRowID == 1) && !enabled) { + continue; + } + else if ((selectedRowID == 2) && !m_view->isModVisible(iter.second)) { + continue; + } + std::vector<ModInfo::EFlag> flags = info->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) && + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) { + if (mod_Priority->isChecked()) + builder.setRowField("#Mod_Priority", QString("%1").arg(iter.first, 4, 10, QChar('0'))); + if (mod_Status->isChecked()) + builder.setRowField("#Mod_Status", (enabled) ? "+" : "-"); + if (mod_Name->isChecked()) + builder.setRowField("#Mod_Name", info->name()); + if (mod_Note->isChecked()) + builder.setRowField("#Note", QString("%1").arg(info->comments().remove(','))); + if (primary_Category->isChecked()) + builder.setRowField("#Primary_Category", (m_categories.categoryExists(info->primaryCategory())) ? m_categories.getCategoryNameByID(info->primaryCategory()) : ""); + if (nexus_ID->isChecked()) + builder.setRowField("#Nexus_ID", info->nexusId()); + if (mod_Nexus_URL->isChecked()) + builder.setRowField("#Mod_Nexus_URL", (info->nexusId() > 0) ? NexusInterface::instance().getModURL(info->nexusId(), info->gameName()) : ""); + if (mod_Version->isChecked()) + builder.setRowField("#Mod_Version", info->version().canonicalString()); + if (install_Date->isChecked()) + builder.setRowField("#Install_Date", info->creationTime().toString("yyyy/MM/dd HH:mm:ss")); + if (download_File_Name->isChecked()) + builder.setRowField("#Download_File_Name", info->installationFile()); + + builder.writeRow(); + } + } + + SaveTextAsDialog saveDialog(m_parent); + saveDialog.setText(buffer.data()); + saveDialog.exec(); + } + catch (const std::exception& e) { + reportError(tr("export failed: %1").arg(e.what())); + } + } +} + +void ModListViewActions::displayModInformation(const QString& modName, ModInfoTabIDs tab) const +{ + unsigned int index = ModInfo::getIndex(modName); + if (index == UINT_MAX) { + log::error("failed to resolve mod name {}", modName); + return; + } + + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + displayModInformation(modInfo, index, tab); +} + +void ModListViewActions::displayModInformation(unsigned int index, ModInfoTabIDs tab) const +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + displayModInformation(modInfo, index, tab); +} + +void ModListViewActions::displayModInformation(ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tab) const +{ + if (!m_core.modList()->modInfoAboutToChange(modInfo)) { + log::debug("a different mod information dialog is open. If this is incorrect, please restart MO"); + return; + } + std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { + QDialog* dialog = m_parent->findChild<QDialog*>("__overwriteDialog"); + try { + if (dialog == nullptr) { + dialog = new OverwriteInfoDialog(modInfo, m_parent); + dialog->setObjectName("__overwriteDialog"); + } + else { + qobject_cast<OverwriteInfoDialog*>(dialog)->setModInfo(modInfo); + } + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); + connect(dialog, &QDialog::finished, [=]() { + m_core.modList()->modInfoChanged(modInfo); + dialog->deleteLater(); + m_core.refreshDirectoryStructure(); + }); + } + catch (const std::exception& e) { + reportError(tr("Failed to display overwrite dialog: %1").arg(e.what())); + } + } + else { + modInfo->saveMeta(); + + ModInfoDialog dialog(m_core, m_core.pluginContainer(), modInfo, m_view, m_parent); + connect(&dialog, &ModInfoDialog::originModified, this, &ModListViewActions::originModified); + connect(&dialog, &ModInfoDialog::modChanged, [=](unsigned int index) { + auto idx = m_view->indexModelToView(m_core.modList()->index(index, 0)); + m_view->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + m_view->scrollTo(idx); + }); + + //Open the tab first if we want to use the standard indexes of the tabs. + if (tab != ModInfoTabIDs::None) { + dialog.selectTab(tab); + } + + dialog.exec(); + + modInfo->saveMeta(); + m_core.modList()->modInfoChanged(modInfo); + } + + if (m_core.currentProfile()->modEnabled(modIndex) && !modInfo->isForeign()) { + FilesOrigin& origin = m_core.directoryStructure()->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + + if (m_core.directoryStructure()->originExists(ToWString(modInfo->name()))) { + FilesOrigin& origin = m_core.directoryStructure()->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + + m_core.directoryRefresher()->addModToStructure(m_core.directoryStructure() + , modInfo->name() + , m_core.currentProfile()->getModPriority(modIndex) + , modInfo->absolutePath() + , modInfo->stealFiles() + , modInfo->archives()); + DirectoryRefresher::cleanStructure(m_core.directoryStructure()); + m_core.directoryStructure()->getFileRegister()->sortOrigins(); + m_core.refreshLists(); + } + } +} + +void ModListViewActions::sendModsToTop(const QModelIndexList& index) const +{ + m_core.modList()->changeModsPriority(index, 0); +} + +void ModListViewActions::sendModsToBottom(const QModelIndexList& index) const +{ + m_core.modList()->changeModsPriority(index, std::numeric_limits<int>::max()); +} + +void ModListViewActions::sendModsToPriority(const QModelIndexList& index) const +{ + bool ok; + int priority = QInputDialog::getInt(m_parent, + tr("Set Priority"), tr("Set the priority of the selected mods"), + 0, 0, std::numeric_limits<int>::max(), 1, &ok); + if (!ok) return; + + m_core.modList()->changeModsPriority(index, priority); +} + +void ModListViewActions::sendModsToSeparator(const QModelIndexList& index) const +{ + QStringList separators; + auto indexesByPriority = m_core.currentProfile()->getAllIndexesByPriority(); + for (auto iter = indexesByPriority.begin(); iter != indexesByPriority.end(); iter++) { + if ((iter->second != UINT_MAX)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(iter->second); + if (modInfo->isSeparator()) { + separators << modInfo->name().chopped(10); // Chops the "_separator" away from the name + } + } + } + + ListDialog dialog(m_parent); + dialog.setWindowTitle("Select a separator..."); + dialog.setChoices(separators); + + if (dialog.exec() == QDialog::Accepted) { + QString result = dialog.getChoice(); + if (!result.isEmpty()) { + result += "_separator"; + + int newPriority = std::numeric_limits<int>::max(); + bool foundSection = false; + for (auto mod : m_core.modsSortedByProfilePriority(m_core.currentProfile())) { + unsigned int modIndex = ModInfo::getIndex(mod); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if (!foundSection && result.compare(mod) == 0) { + foundSection = true; + } + else if (foundSection && modInfo->isSeparator()) { + newPriority = m_core.currentProfile()->getModPriority(modIndex); + break; + } + } + + if (index.size() == 1 + && m_core.currentProfile()->getModPriority(index[0].data(ModList::IndexRole).toInt()) < newPriority) { + --newPriority; + } + + m_core.modList()->changeModsPriority(index, newPriority); + } + } +} + +void ModListViewActions::renameMod(const QModelIndex& index) const +{ + try { + m_view->edit(m_view->indexModelToView(index)); + } + catch (const std::exception& e) { + reportError(tr("failed to rename mod: %1").arg(e.what())); + } +} + +void ModListViewActions::removeMods(const QModelIndexList& indices) const +{ + const int max_items = 20; + + try { + if (indices.size() > 1) { + QString mods; + QStringList modNames; + + int i = 0; + for (auto& idx : indices) { + QString name = idx.data().toString(); + if (!ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->isRegular()) { + continue; + } + + // adds an item for the mod name until `i` reaches `max_items`, which + // adds one "..." item; subsequent mods are not shown on the list but + // are still added to `modNames` below so they can be removed correctly + + if (i < max_items) { + mods += "<li>" + name + "</li>"; + } + else if (i == max_items) { + mods += "<li>...</li>"; + } + + modNames.append(ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->name()); + ++i; + } + if (QMessageBox::question(m_parent, tr("Confirm"), + tr("Remove the following mods?<br><ul>%1</ul>").arg(mods), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + // use mod names instead of indexes because those become invalid during the removal + DownloadManager::startDisableDirWatcher(); + for (QString name : modNames) { + m_core.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex()); + } + DownloadManager::endDisableDirWatcher(); + } + } + else if (!indices.isEmpty()) { + m_core.modList()->removeRow(indices[0].data(ModList::IndexRole).toInt(), QModelIndex()); + } + m_view->updateModCount(); + m_pluginView->updatePluginCount(); + } + catch (const std::exception& e) { + reportError(tr("failed to remove mod: %1").arg(e.what())); + } +} + +void ModListViewActions::ignoreMissingData(const QModelIndexList& indices) const +{ + for (auto& idx : indices) { + int row_idx = idx.data(ModList::IndexRole).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(row_idx); + info->markValidated(true); + m_core.modList()->notifyChange(row_idx); + } +} + +void ModListViewActions::setIgnoreUpdate(const QModelIndexList& indices, bool ignore) const +{ + for (auto& idx : indices) { + int modIdx = idx.data(ModList::IndexRole).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(modIdx); + info->ignoreUpdate(ignore); + m_core.modList()->notifyChange(modIdx); + } +} + +void ModListViewActions::changeVersioningScheme(const QModelIndex& index) const { + if (QMessageBox::question(m_parent, tr("Continue?"), + tr("The versioning scheme decides which version is considered newer than another.\n" + "This function will guess the versioning scheme under the assumption that the installed version is outdated."), + QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) { + + ModInfo::Ptr info = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt()); + + bool success = false; + + static VersionInfo::VersionScheme schemes[] = { VersionInfo::SCHEME_REGULAR, VersionInfo::SCHEME_DECIMALMARK, VersionInfo::SCHEME_NUMBERSANDLETTERS }; + + for (int i = 0; i < sizeof(schemes) / sizeof(VersionInfo::VersionScheme) && !success; ++i) { + VersionInfo verOld(info->version().canonicalString(), schemes[i]); + VersionInfo verNew(info->newestVersion().canonicalString(), schemes[i]); + if (verOld < verNew) { + info->setVersion(verOld); + info->setNewestVersion(verNew); + success = true; + } + } + if (!success) { + QMessageBox::information(m_parent, tr("Sorry"), + tr("I don't know a versioning scheme where %1 is newer than %2.").arg(info->newestVersion().canonicalString()).arg(info->version().canonicalString()), + QMessageBox::Ok); + } + } +} + +void ModListViewActions::markConverted(const QModelIndexList& indices) const +{ + for (auto& idx : indices) { + int modIdx = idx.data(ModList::IndexRole).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(modIdx); + info->markConverted(true); + m_core.modList()->notifyChange(modIdx); + } +} + +void ModListViewActions::visitOnNexus(const QModelIndexList& indices) const +{ + if (indices.size() > 10) { + if (QMessageBox::question(m_parent, tr("Opening Nexus Links"), + tr("You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?").arg(indices.size()), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + for (auto& idx : indices) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + int modID = info->nexusId(); + QString gameName = info->gameName(); + if (modID > 0) { + shell::Open(QUrl(NexusInterface::instance().getModURL(modID, gameName))); + } + else { + log::error("mod '{}' has no nexus id", info->name()); + } + } +} + +void ModListViewActions::visitWebPage(const QModelIndexList& indices) const +{ + if (indices.size() > 10) { + if (QMessageBox::question(m_parent, tr("Opening Web Pages"), + tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(indices.size()), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + for (auto& idx : indices) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + + const auto url = info->parseCustomURL(); + if (url.isValid()) { + shell::Open(url); + } + } +} + +void ModListViewActions::visitNexusOrWebPage(const QModelIndexList& indices) const +{ + if (indices.size() > 10) { + if (QMessageBox::question(m_parent, tr("Opening Web Pages"), + tr("You are trying to open %1 Web Pages. Are you sure you want to do this?").arg(indices.size()), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + for (auto& idx : indices) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + if (!info) { + log::error("mod {} not found", idx.data(ModList::IndexRole).toInt()); + continue; + } + + int modID = info->nexusId(); + QString gameName = info->gameName(); + const auto url = info->parseCustomURL(); + + if (modID > 0) { + shell::Open(QUrl(NexusInterface::instance().getModURL(modID, gameName))); + } + else if (url.isValid()) { + shell::Open(url); + } + else { + log::error("mod '{}' has no valid link", info->name()); + } + } +} + +void ModListViewActions::reinstallMod(const QModelIndex& index) const +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt()); + QString installationFile = modInfo->installationFile(); + if (installationFile.length() != 0) { + QString fullInstallationFile; + QFileInfo fileInfo(installationFile); + if (fileInfo.isAbsolute()) { + if (fileInfo.exists()) { + fullInstallationFile = installationFile; + } + else { + fullInstallationFile = m_core.downloadManager()->getOutputDirectory() + "/" + fileInfo.fileName(); + } + } + else { + fullInstallationFile = m_core.downloadManager()->getOutputDirectory() + "/" + installationFile; + } + if (QFile::exists(fullInstallationFile)) { + m_core.installMod(fullInstallationFile, true, modInfo, modInfo->name()); + } + else { + QMessageBox::information(m_parent, tr("Failed"), tr("Installation file no longer exists")); + } + } + else { + QMessageBox::information(m_parent, tr("Failed"), + tr("Mods installed with old versions of MO can't be reinstalled in this way.")); + } +} + +void ModListViewActions::createBackup(const QModelIndex& index) const +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt()); + QString backupDirectory = m_core.installationManager()->generateBackupName(modInfo->absolutePath()); + if (!copyDir(modInfo->absolutePath(), backupDirectory, false)) { + QMessageBox::information(m_parent, tr("Failed"), + tr("Failed to create backup.")); + } + m_core.refresh(); + m_view->updateModCount(); +} + +void ModListViewActions::restoreHiddenFiles(const QModelIndexList& indices) const +{ + const int max_items = 20; + + QFlags<FileRenamer::RenameFlags> flags = FileRenamer::UNHIDE; + flags |= FileRenamer::MULTIPLE; + + FileRenamer renamer(m_parent, flags); + + FileRenamer::RenameResults result = FileRenamer::RESULT_OK; + + // multi selection + if (indices.size() > 1) { + + QStringList modNames; + for (auto& idx : indices) { + + ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + const auto flags = modInfo->getFlags(); + + if (!modInfo->isRegular() || + std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) == flags.end()) { + continue; + } + + modNames.append(idx.data(Qt::DisplayRole).toString()); + } + + QString mods = "<li>" + modNames.mid(0, max_items).join("</li><li>") + "</li>"; + if (modNames.size() > max_items) { + mods += "<li>...</li>"; + } + + if (QMessageBox::question(m_parent, tr("Confirm"), + tr("Restore all hidden files in the following mods?<br><ul>%1</ul>").arg(mods), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + + for (auto& idx : indices) { + + ModInfo::Ptr modInfo = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + + const auto flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_HIDDEN_FILES) != flags.end()) { + const QString modDir = modInfo->absolutePath(); + + auto partialResult = restoreHiddenFilesRecursive(renamer, modDir); + + if (partialResult == FileRenamer::RESULT_CANCEL) { + result = FileRenamer::RESULT_CANCEL; + break; + } + emit originModified((m_core.directoryStructure()->getOriginByName( + ToWString(modInfo->internalName()))).getID()); + } + } + } + } + else if (!indices.isEmpty()) { + //single selection + ModInfo::Ptr modInfo = ModInfo::getByIndex(indices[0].data(ModList::IndexRole).toInt()); + const QString modDir = modInfo->absolutePath(); + + if (QMessageBox::question(m_parent, tr("Are you sure?"), + tr("About to restore all hidden files in:\n") + modInfo->name(), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + + result = restoreHiddenFilesRecursive(renamer, modDir); + + emit originModified((m_core.directoryStructure()->getOriginByName( + ToWString(modInfo->internalName()))).getID()); + } + } + + if (result == FileRenamer::RESULT_CANCEL) { + log::debug("Restoring hidden files operation cancelled"); + } + else { + log::debug("Finished restoring hidden files"); + } +} + +void ModListViewActions::setTracked(const QModelIndexList& indices, bool tracked) const +{ + m_core.loggedInAction(m_parent, [=] { + for (auto& idx : indices) { + ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->track(tracked); + } + }); +} + +void ModListViewActions::setEndorsed(const QModelIndexList& indices, bool endorsed) const +{ + m_core.loggedInAction(m_parent, [=] { + if (indices.size() > 1) { + MessageDialog::showMessage(tr("Endorsing multiple mods will take a while. Please wait..."), m_parent); + } + + for (auto& idx : indices) { + ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->endorse(endorsed); + } + }); +} + +void ModListViewActions::willNotEndorsed(const QModelIndexList& indices) const +{ + for (auto& idx : indices) { + ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt())->setNeverEndorse(); + } +} + +void ModListViewActions::setColor(const QModelIndexList& indices, const QModelIndex& refIndex) const +{ + auto& settings = m_core.settings(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(refIndex.data(ModList::IndexRole).toInt()); + + QColorDialog dialog(m_parent); + dialog.setOption(QColorDialog::ShowAlphaChannel); + + QColor currentColor = modInfo->color(); + if (currentColor.isValid()) { + dialog.setCurrentColor(currentColor); + } + else if (auto c = settings.colors().previousSeparatorColor()) { + dialog.setCurrentColor(*c); + } + + if (!dialog.exec()) + return; + + currentColor = dialog.currentColor(); + if (!currentColor.isValid()) + return; + + settings.colors().setPreviousSeparatorColor(currentColor); + + for (auto& idx : indices) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + info->setColor(currentColor); + } + +} + +void ModListViewActions::resetColor(const QModelIndexList& indices) const +{ + for (auto& idx : indices) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + info->setColor(QColor()); + } + m_core.settings().colors().removePreviousSeparatorColor(); +} + +void ModListViewActions::setCategories(ModInfo::Ptr mod, const std::vector<std::pair<int, bool>>& categories) const +{ + for (auto& [id, enabled] : categories) { + mod->setCategory(id, enabled); + } +} + +void ModListViewActions::setCategoriesIf(ModInfo::Ptr mod, ModInfo::Ptr ref, const std::vector<std::pair<int, bool>>& categories) const +{ + for (auto& [id, enabled] : categories) { + if (ref->categorySet(id) != enabled) { + mod->setCategory(id, enabled); + } + } +} + +void ModListViewActions::setCategories(const QModelIndexList& selected, const QModelIndex& ref, + const std::vector<std::pair<int, bool>>& categories) const +{ + ModInfo::Ptr refMod = ModInfo::getByIndex(ref.data(ModList::IndexRole).toInt()); + if (selected.size() > 1) { + for (auto& idx : selected) { + if (idx.row() != ref.row()) { + setCategoriesIf( + ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()), + refMod, categories); + } + } + setCategories(refMod, categories); + } + else if (!selected.isEmpty()) { + // for single mod selections, just do a replace + setCategories(refMod, categories); + } + + for (auto& idx : selected) { + m_core.modList()->notifyChange(idx.data(ModList::IndexRole).toInt()); + } + + // reset the selection manually - still needed + auto viewIndices = m_view->indexModelToView(selected); + for (auto& idx : viewIndices) { + m_view->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); + } +} + +void ModListViewActions::setPrimaryCategory(const QModelIndexList& selected, int category, bool force) +{ + for (auto& idx : selected) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + if (force || info->categorySet(category)) { + info->setCategory(category, true); + info->setPrimaryCategory(category); + } + } + + // reset the selection manually - still needed + auto viewIndices = m_view->indexModelToView(selected); + for (auto& idx : viewIndices) { + m_view->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); + } +} + +void ModListViewActions::openExplorer(const QModelIndexList& index) const +{ + for (auto& idx : index) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(ModList::IndexRole).toInt()); + if (!info->isForeign()) { + shell::Explore(info->absolutePath()); + } + } +} + +void ModListViewActions::restoreBackup(const QModelIndex& index) const +{ + QRegExp backupRegEx("(.*)_backup[0-9]*$"); + ModInfo::Ptr modInfo = ModInfo::getByIndex(index.data(ModList::IndexRole).toInt()); + if (backupRegEx.indexIn(modInfo->name()) != -1) { + QString regName = backupRegEx.cap(1); + QDir modDir(QDir::fromNativeSeparators(m_core.settings().paths().mods())); + if (!modDir.exists(regName) || + (QMessageBox::question(m_parent, tr("Overwrite?"), + tr("This will replace the existing mod \"%1\". Continue?").arg(regName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { + if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) { + reportError(tr("failed to remove mod \"%1\"").arg(regName)); + } + else { + QString destinationPath = QDir::fromNativeSeparators(m_core.settings().paths().mods()) + "/" + regName; + if (!modDir.rename(modInfo->absolutePath(), destinationPath)) { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath)); + } + m_core.refresh(); + m_view->updateModCount(); + } + } + } +} + +void ModListViewActions::moveOverwriteContentsTo(const QString& absolutePath) const +{ + ModInfo::Ptr overwriteInfo = ModInfo::getOverwrite(); + bool successful = shellMove((QDir::toNativeSeparators(overwriteInfo->absolutePath()) + "\\*"), + (QDir::toNativeSeparators(absolutePath)), false, m_parent); + + if (successful) { + MessageDialog::showMessage(tr("Move successful."), m_parent); + } + else { + const auto e = GetLastError(); + log::error("Move operation failed: {}", formatSystemMessage(e)); + } + + m_core.refresh(); +} + +void ModListViewActions::createModFromOverwrite() const +{ + GuessedValue<QString> name; + name.setFilter(&fixDirectoryName); + + while (name->isEmpty()) { + bool ok; + name.update(QInputDialog::getText(m_parent, tr("Create Mod..."), + tr("This will move all files from overwrite into a new, regular mod.\n" + "Please enter a name:"), QLineEdit::Normal, "", &ok), + GUESS_USER); + if (!ok) { + return; + } + } + + if (m_core.modList()->getMod(name) != nullptr) { + reportError(tr("A mod with this name already exists")); + return; + } + + const IModInterface* newMod = m_core.createMod(name); + if (newMod == nullptr) { + return; + } + + moveOverwriteContentsTo(newMod->absolutePath()); +} + +void ModListViewActions::moveOverwriteContentToExistingMod() const +{ + QStringList mods; + auto indexesByPriority = m_core.currentProfile()->getAllIndexesByPriority(); + for (auto& iter : indexesByPriority) { + if ((iter.second != UINT_MAX)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(iter.second); + if (!modInfo->isSeparator() && !modInfo->isForeign() && !modInfo->isOverwrite()) { + mods << modInfo->name(); + } + } + } + + ListDialog dialog(m_parent); + dialog.setWindowTitle("Select a mod..."); + dialog.setChoices(mods); + + if (dialog.exec() == QDialog::Accepted) { + QString result = dialog.getChoice(); + if (!result.isEmpty()) { + + QString modAbsolutePath; + + for (const auto& mod : m_core.modsSortedByProfilePriority(m_core.currentProfile())) { + if (result.compare(mod) == 0) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(mod)); + modAbsolutePath = modInfo->absolutePath(); + break; + } + } + + if (modAbsolutePath.isNull()) { + log::warn("Mod {} has not been found, for some reason", result); + return; + } + + moveOverwriteContentsTo(modAbsolutePath); + } + } +} + +void ModListViewActions::clearOverwrite() const +{ + ModInfo::Ptr modInfo = ModInfo::getOverwrite(); + if (modInfo) + { + QDir overwriteDir(modInfo->absolutePath()); + if (QMessageBox::question(m_parent, tr("Are you sure?"), + tr("About to recursively delete:\n") + overwriteDir.absolutePath(), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) + { + QStringList delList; + for (auto f : overwriteDir.entryList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot)) + delList.push_back(overwriteDir.absoluteFilePath(f)); + if (shellDelete(delList, true)) { + emit overwriteCleared(); + m_core.refresh(); + } + else { + const auto e = GetLastError(); + log::error("Delete operation failed: {}", formatSystemMessage(e)); + } + } + } +} diff --git a/src/modlistviewactions.h b/src/modlistviewactions.h new file mode 100644 index 00000000..f1215cec --- /dev/null +++ b/src/modlistviewactions.h @@ -0,0 +1,158 @@ +#ifndef MODLISTVIEWACTIONS_H +#define MODLISTVIEWACTIONS_H + +#include <QObject> +#include <QString> + +#include "modinfo.h" +#include "modinfodialogfwd.h" + +class CategoryFactory; +class FilterList; +class MainWindow; +class ModListView; +class PluginListView; +class OrganizerCore; + +class ModListViewActions : public QObject +{ + Q_OBJECT + +public: + + // currently passing the main window itself because a lots of stuff needs it but + // it would be nice to avoid passing it at some point + // + ModListViewActions( + OrganizerCore& core, + FilterList& filters, + CategoryFactory& categoryFactory, + ModListView* view, + PluginListView* pluginView, + QObject* nxmReceiver); + + // install the mod from the given archive + // + void installMod(const QString& archivePath = "") const; + + // create an empty mod/a separator before the given mod or at + // the end of the list if the index is invalid + // + void createEmptyMod(const QModelIndex& index = QModelIndex()) const; + void createSeparator(const QModelIndex& index = QModelIndex()) const; + + // check all mods for update + // + void checkModsForUpdates() const; + void checkModsForUpdates(const QModelIndexList& indices) const; + + // start the "Export Mod List" dialog + // + void exportModListCSV() const; + + // display mod information + // + void displayModInformation(const QString& modName, ModInfoTabIDs tabID = ModInfoTabIDs::None) const; + void displayModInformation(unsigned int index, ModInfoTabIDs tab = ModInfoTabIDs::None) const; + void displayModInformation(ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID = ModInfoTabIDs::None) const; + + // move mods to top/bottom, start the "Send to priority" and "Send to separator" dialog + // + void sendModsToTop(const QModelIndexList& index) const; + void sendModsToBottom(const QModelIndexList& index) const; + void sendModsToPriority(const QModelIndexList& index) const; + void sendModsToSeparator(const QModelIndexList& index) const; + + // actions for most regular mods + // + void renameMod(const QModelIndex& index) const; + void removeMods(const QModelIndexList& indices) const; + void ignoreMissingData(const QModelIndexList& indices) const; + void setIgnoreUpdate(const QModelIndexList& indices, bool ignore) const; + void changeVersioningScheme(const QModelIndex& indices) const; + void markConverted(const QModelIndexList& indices) const; + void visitOnNexus(const QModelIndexList& indices) const; + void visitWebPage(const QModelIndexList& indices) const; + void visitNexusOrWebPage(const QModelIndexList& indices) const; + void reinstallMod(const QModelIndex& index) const; + void createBackup(const QModelIndex& index) const; + void restoreHiddenFiles(const QModelIndexList& indices) const; + void setTracked(const QModelIndexList& indices, bool tracked) const; + void setEndorsed(const QModelIndexList& indices, bool endorsed) const; + void willNotEndorsed(const QModelIndexList& indices) const; + + // set/reset color of the given selection, using the given reference index (index + // at which the context menu was shown) + // + void setColor(const QModelIndexList& indices, const QModelIndex& refIndex) const; + void resetColor(const QModelIndexList& indices) const; + + // set the category of the mods in the given list, using the given index as reference + // - the categories are set as-is on the refernce mod + // - for the other mods, the category is only set if the current state of the category + // on the reference is different + // + void setCategories(const QModelIndexList& selected, const QModelIndex& ref, + const std::vector<std::pair<int, bool>>& categories) const; + + // set the primary category of the mods in the given list, adding the appropriate + // category to the mods unless force is false, in which case the primary category + // is set only on mods that have this category + // + void setPrimaryCategory(const QModelIndexList& selected, int category, bool force = true); + + // open the Windows explorer for the specified mods + // + void openExplorer(const QModelIndexList& index) const; + + // backup-specific actions + // + void restoreBackup(const QModelIndex& index) const; + + // overwrite-specific actions + // + void createModFromOverwrite() const; + void moveOverwriteContentToExistingMod() const; + void clearOverwrite() const; + +signals: + + // emitted when the overwrite mod has been clear + // + void overwriteCleared() const; + + // emitted when the origin of a file is modified + // + void originModified(int originId) const; + +private: + + // move the contents of the overwrite to the given path + // + void moveOverwriteContentsTo(const QString& absolutePath) const; + + // set the category of the given mod based on the given array + // + void setCategories(ModInfo::Ptr mod, const std::vector<std::pair<int, bool>>& categories) const; + + // set the category of the given mod if the category from the reference mod does not match + // the one in the array of categories + // + void setCategoriesIf(ModInfo::Ptr mod, ModInfo::Ptr ref, const std::vector<std::pair<int, bool>>& categories) const; + + // check the given mods from update, the map should map game names to nexus ID + // + void checkModsForUpdates(std::multimap<QString, int> const& IDs) const; + +private: + + OrganizerCore& m_core; + FilterList& m_filters; + CategoryFactory& m_categories; + ModListView* m_view; + PluginListView* m_pluginView; + QObject* m_receiver; + QWidget* m_parent; +}; + +#endif diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 74b6ed08..d952bc4c 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -11,11 +11,9 @@ #include "modrepositoryfileinfo.h" #include "nexusinterface.h" #include "plugincontainer.h" -#include "pluginlistsortproxy.h" #include "profile.h" #include "credentialsdialog.h" #include "filedialogmemory.h" -#include "modinfodialog.h" #include "spawn.h" #include "syncoverwritedialog.h" #include "nxmaccessmanager.h" @@ -120,6 +118,7 @@ OrganizerCore::OrganizerCore(Settings &settings) connect(&m_ModList, SIGNAL(removeOrigin(QString)), this, SLOT(removeOrigin(QString))); + connect(&m_ModList, &ModList::modStatesChanged, [=] { currentProfile()->writeModlist(); }); connect(NexusInterface::instance().getAccessManager(), SIGNAL(validateSuccessful(bool)), this, SLOT(loginSuccessful(bool))); @@ -220,9 +219,9 @@ void OrganizerCore::updateExecutablesList() void OrganizerCore::updateModInfoFromDisc() { ModInfo::updateFromDisc( - m_Settings.paths().mods(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.interface().displayForeign(), - m_Settings.refreshThreadCount(), managedGame()); + m_Settings.paths().mods(), *this, + m_Settings.interface().displayForeign(), + m_Settings.refreshThreadCount()); } void OrganizerCore::setUserInterface(IUserInterface* ui) @@ -236,35 +235,6 @@ void OrganizerCore::setUserInterface(IUserInterface* ui) w = m_UserInterface->mainWindow(); } - if (w) { - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), w, - SLOT(modlistChanged(QModelIndex, int))); - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), w, - SLOT(modlistChanged(QModelIndexList, int))); - connect(&m_ModList, SIGNAL(showMessage(QString)), w, - SLOT(showMessage(QString))); - connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), w, - SLOT(modRenamed(QString, QString))); - connect(&m_ModList, SIGNAL(modUninstalled(QString)), w, - SLOT(modRemoved(QString))); - connect(&m_InstallationManager, SIGNAL(modReplaced(QString)), w, - SLOT(modRemoved(QString))); - connect(&m_ModList, SIGNAL(removeSelectedMods()), w, - SLOT(removeMod_clicked())); - connect(&m_ModList, SIGNAL(clearOverwrite()), w, - SLOT(clearOverwrite())); - connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), w, - SLOT(fileMoved(QString, QString, QString))); - connect(&m_ModList, SIGNAL(modorder_changed()), w, - SLOT(modorder_changed())); - connect(&m_PluginList, SIGNAL(writePluginsList()), w, - SLOT(esplist_changed())); - connect(&m_PluginList, SIGNAL(esplist_changed()), w, - SLOT(esplist_changed())); - connect(&m_DownloadManager, SIGNAL(showMessage(QString)), w, - SLOT(showMessage(QString))); - } - m_InstallationManager.setParentWidget(w); m_Updater.setUserInterface(w); m_UILocker.setUserInterface(w); @@ -694,8 +664,8 @@ MOBase::IPluginGame *OrganizerCore::getGame(const QString &name) const MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &name) { - bool merge = false; - if (m_InstallationManager.testOverwrite(name, &merge) != IPluginInstaller::EInstallResult::RESULT_SUCCESS) { + auto result = m_InstallationManager.testOverwrite(name); + if (!result) { return nullptr; } @@ -708,7 +678,7 @@ MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &name) QSettings settingsFile(targetDirectory + "/meta.ini", QSettings::IniFormat); - if (!merge) { + if (!result.merged()) { settingsFile.setValue("modid", 0); settingsFile.setValue("version", ""); settingsFile.setValue("newestVersion", ""); @@ -720,8 +690,9 @@ MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &name) settingsFile.endArray(); } - return ModInfo::createFrom(m_PluginContainer, m_GamePlugin, QDir(targetDirectory), &m_DirectoryStructure) - .data(); + // shouldn't this use the existing mod in case of a merge? also, this does not refresh the indices + // in the ModInfo structure + return ModInfo::createFrom(QDir(targetDirectory), *this).data(); } void OrganizerCore::modDataChanged(MOBase::IModInterface *) @@ -760,83 +731,21 @@ QString OrganizerCore::pluginDataPath() + "/data"; } -MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName, +MOBase::IModInterface *OrganizerCore::installMod(const QString & archivePath, bool reinstallation, ModInfo::Ptr currentMod, const QString &initModName) { - if (m_CurrentProfile == nullptr) { - return nullptr; - } - - if (m_InstallationManager.isRunning()) { - QMessageBox::information( - qApp->activeWindow(), tr("Installation cancelled"), - tr("Another installation is currently in progress."), QMessageBox::Ok); - return nullptr; - } - - bool hasIniTweaks = false; - GuessedValue<QString> modName; - if (!initModName.isEmpty()) { - modName.update(initModName, GUESS_USER); - } - m_CurrentProfile->writeModlistNow(); - m_InstallationManager.setModsDirectory(m_Settings.paths().mods()); - m_InstallationManager.notifyInstallationStart(fileName, reinstallation, currentMod); - auto result = m_InstallationManager.install(fileName, modName, hasIniTweaks); - if (result == IPluginInstaller::RESULT_SUCCESS) { - MessageDialog::showMessage(tr("Installation successful"), - qApp->activeWindow()); - refresh(); - - int modIndex = ModInfo::getIndex(modName); - if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - auto dlIdx = m_DownloadManager.indexByName(QFileInfo(fileName).fileName()); - if (dlIdx != -1) { - int modId = m_DownloadManager.getModID(dlIdx); - int fileId = m_DownloadManager.getFileInfo(dlIdx)->fileID; - modInfo->addInstalledFile(modId, fileId); - } - if (hasIniTweaks && (m_UserInterface != nullptr) - && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), - tr("This mod contains ini tweaks. Do you " - "want to configure them now?"), - QMessageBox::Yes | QMessageBox::No) - == QMessageBox::Yes)) { - m_UserInterface->displayModInformation( - modInfo, modIndex, ModInfoTabIDs::IniFiles); - } - m_ModList.notifyModInstalled(modInfo.get()); - m_DownloadManager.markInstalled(fileName); - m_InstallationManager.notifyInstallationEnd(result, modInfo); - emit modInstalled(modName); - return modInfo.data(); - } else { - reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); - } - } else { - m_InstallationManager.notifyInstallationEnd(result, nullptr); - if (m_InstallationManager.wasCancelled()) { - QMessageBox::information(qApp->activeWindow(), tr("Extraction cancelled"), - tr("The installation was cancelled while extracting files. " - "If this was prior to a FOMOD setup, this warning may be ignored. " - "However, if this was during installation, the mod will likely be missing files."), - QMessageBox::Ok); - refresh(); - } - } - return nullptr; + return installArchive(archivePath, -1, reinstallation, currentMod, initModName).get(); } -void OrganizerCore::installDownload(int index) +ModInfo::Ptr OrganizerCore::installDownload(int index, int priority) { if (m_InstallationManager.isRunning()) { QMessageBox::information( qApp->activeWindow(), tr("Installation cancelled"), tr("Another installation is currently in progress."), QMessageBox::Ok); - return; + return nullptr; } try { @@ -867,16 +776,21 @@ void OrganizerCore::installDownload(int index) m_InstallationManager.setModsDirectory(m_Settings.paths().mods()); m_InstallationManager.notifyInstallationStart(fileName, false, currentMod); auto result = m_InstallationManager.install(fileName, modName, hasIniTweaks); - if (result == IPluginInstaller::RESULT_SUCCESS) { + if (result) { MessageDialog::showMessage(tr("Installation successful"), qApp->activeWindow()); refresh(); int modIndex = ModInfo::getIndex(modName); + ModInfo::Ptr modInfo = nullptr; if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + modInfo = ModInfo::getByIndex(modIndex); modInfo->addInstalledFile(modID, fileID); + if (priority != -1 && !result.mergedOrReplaced()) { + m_ModList.changeModPriority(modIndex, priority); + } + if (hasIniTweaks && m_UserInterface != nullptr && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), tr("This mod contains ini tweaks. Do you " @@ -888,12 +802,13 @@ void OrganizerCore::installDownload(int index) } m_ModList.notifyModInstalled(modInfo.get()); - m_InstallationManager.notifyInstallationEnd(IPluginInstaller::RESULT_SUCCESS, modInfo); + m_InstallationManager.notifyInstallationEnd(result, modInfo); } else { reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); } m_DownloadManager.markInstalled(index); emit modInstalled(modName); + return modInfo; } else { m_InstallationManager.notifyInstallationEnd(result, nullptr); @@ -909,6 +824,84 @@ void OrganizerCore::installDownload(int index) } catch (const std::exception &e) { reportError(e.what()); } + + return nullptr; +} + +ModInfo::Ptr OrganizerCore::installArchive( + const QString& archivePath, int priority, bool reinstallation, + ModInfo::Ptr currentMod, const QString& initModName) +{ + if (m_CurrentProfile == nullptr) { + return nullptr; + } + + if (m_InstallationManager.isRunning()) { + QMessageBox::information( + qApp->activeWindow(), tr("Installation cancelled"), + tr("Another installation is currently in progress."), QMessageBox::Ok); + return nullptr; + } + + bool hasIniTweaks = false; + GuessedValue<QString> modName; + if (!initModName.isEmpty()) { + modName.update(initModName, GUESS_USER); + } + m_CurrentProfile->writeModlistNow(); + m_InstallationManager.setModsDirectory(m_Settings.paths().mods()); + m_InstallationManager.notifyInstallationStart(archivePath, reinstallation, currentMod); + auto result = m_InstallationManager.install(archivePath, modName, hasIniTweaks); + if (result) { + MessageDialog::showMessage(tr("Installation successful"), + qApp->activeWindow()); + refresh(); + + int modIndex = ModInfo::getIndex(modName); + if (modIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + auto dlIdx = m_DownloadManager.indexByName(QFileInfo(archivePath).fileName()); + if (dlIdx != -1) { + int modId = m_DownloadManager.getModID(dlIdx); + int fileId = m_DownloadManager.getFileInfo(dlIdx)->fileID; + modInfo->addInstalledFile(modId, fileId); + } + + if (priority != -1 && !result.mergedOrReplaced()) { + m_ModList.changeModPriority(modIndex, priority); + } + + if (hasIniTweaks && (m_UserInterface != nullptr) + && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), + tr("This mod contains ini tweaks. Do you " + "want to configure them now?"), + QMessageBox::Yes | QMessageBox::No) + == QMessageBox::Yes)) { + m_UserInterface->displayModInformation( + modInfo, modIndex, ModInfoTabIDs::IniFiles); + } + m_ModList.notifyModInstalled(modInfo.get()); + m_DownloadManager.markInstalled(archivePath); + m_InstallationManager.notifyInstallationEnd(result, modInfo); + emit modInstalled(modName); + return modInfo; + } + else { + reportError(tr("mod not found: %1").arg(qUtf8Printable(modName))); + } + } + else { + m_InstallationManager.notifyInstallationEnd(result, nullptr); + if (m_InstallationManager.wasCancelled()) { + QMessageBox::information(qApp->activeWindow(), tr("Extraction cancelled"), + tr("The installation was cancelled while extracting files. " + "If this was prior to a FOMOD setup, this warning may be ignored. " + "However, if this was during installation, the mod will likely be missing files."), + QMessageBox::Ok); + refresh(); + } + } + return nullptr; } QString OrganizerCore::resolvePath(const QString &fileName) const @@ -1225,11 +1218,7 @@ void OrganizerCore::refresh(bool saveChanges) m_CurrentProfile->writeModlistNow(true); } - ModInfo::updateFromDisc( - m_Settings.paths().mods(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.interface().displayForeign(), - m_Settings.refreshThreadCount(), managedGame()); - + updateModInfoFromDisc(); m_CurrentProfile->refreshModStatus(); m_ModList.notifyChange(-1); @@ -1492,18 +1481,9 @@ void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply) } } -ModListSortProxy *OrganizerCore::createModListProxyModel() -{ - ModListSortProxy *result = new ModListSortProxy(m_CurrentProfile.get(), this); - result->setSourceModel(&m_ModList); - return result; -} - -PluginListSortProxy *OrganizerCore::createPluginListProxyModel() +PluginContainer& OrganizerCore::pluginContainer() const { - PluginListSortProxy *result = new PluginListSortProxy(this); - result->setSourceModel(&m_PluginList); - return result; + return *m_PluginContainer; } IPluginGame const *OrganizerCore::managedGame() const @@ -1767,13 +1747,7 @@ void OrganizerCore::loginFailedUpdate(const QString &message) void OrganizerCore::syncOverwrite() { - unsigned int overwriteIndex = ModInfo::findMod([](ModInfo::Ptr mod) -> bool { - std::vector<ModInfo::EFlag> flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) - != flags.end(); - }); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(overwriteIndex); + ModInfo::Ptr modInfo = ModInfo::getOverwrite(); SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure, qApp->activeWindow()); if (syncDialog.exec() == QDialog::Accepted) { diff --git a/src/organizercore.h b/src/organizercore.h index f2b904cd..24de26ee 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -227,8 +227,9 @@ public: MOBase::VersionInfo getVersion() const { return m_Updater.getVersion(); }
- ModListSortProxy *createModListProxyModel();
- PluginListSortProxy *createPluginListProxyModel();
+ // return the plugin container
+ //
+ PluginContainer& pluginContainer() const;
MOBase::IPluginGame const *managedGame() const;
@@ -376,7 +377,9 @@ public slots: void refreshLists();
- void installDownload(int downloadIndex);
+ 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 modStatusChanged(unsigned int index);
void modStatusChanged(QList<unsigned int> index);
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index 504b17c5..918668ed 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -135,19 +135,19 @@ QString PluginList::getColumnToolTip(int column) }
}
-void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry, const Profile &profile)
+void PluginList::highlightPlugins(
+ const std::vector<unsigned int>& modIndices,
+ const MOShared::DirectoryEntry &directoryEntry)
{
+ auto* profile = m_Organizer.currentProfile();
+
for (auto &esp : m_ESPs) {
esp.modSelected = false;
}
- for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) {
- int modIndex = idx.data(Qt::UserRole + 1).toInt();
- if (modIndex == UINT_MAX)
- continue;
-
+ for (auto& modIndex : modIndices) {
ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex);
- if (!selectedMod.isNull() && profile.modEnabled(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());
@@ -338,94 +338,89 @@ int PluginList::findPluginByPriority(int priority) return -1;
}
-void PluginList::enableSelected(const QItemSelectionModel *selectionModel)
+void PluginList::setEnabled(const QModelIndexList& indices, bool enabled)
{
- if (selectionModel->hasSelection()) {
- QStringList dirty;
- for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
- int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].enabled) {
- m_ESPs[rowIndex].enabled = true;
- dirty.append(m_ESPs[rowIndex].name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty, IPluginList::PluginState::STATE_ACTIVE);
+ 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::disableSelected(const QItemSelectionModel *selectionModel)
+void PluginList::setEnabledAll(bool enabled)
{
- if (selectionModel->hasSelection()) {
- QStringList dirty;
- for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
- int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].forceEnabled && m_ESPs[rowIndex].enabled) {
- m_ESPs[rowIndex].enabled = false;
- dirty.append(m_ESPs[rowIndex].name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty, IPluginList::PluginState::STATE_INACTIVE);
+ 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::enableAll()
+void PluginList::sendToPriority(const QModelIndexList& indices, int newPriority)
{
- if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all plugins?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- QStringList dirty;
- for (ESPInfo &info : m_ESPs) {
- if (!info.enabled) {
- info.enabled = true;
- dirty.append(info.name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty, IPluginList::PluginState::STATE_ACTIVE);
+ std::vector<int> pluginsToMove;
+ for (auto& idx : indices) {
+ if (!m_ESPs[idx.row()].forceEnabled) {
+ pluginsToMove.push_back(idx.row());
}
}
+ if (pluginsToMove.size()) {
+ changePluginPriority(pluginsToMove, newPriority);
+ }
}
-
-void PluginList::disableAll()
+void PluginList::shiftPluginsPriority(const QModelIndexList& indices, int offset)
{
- if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all plugins?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- QStringList dirty;
- for (ESPInfo &info : m_ESPs) {
- if (!info.forceEnabled && info.enabled) {
- info.enabled = false;
- dirty.append(info.name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty, IPluginList::PluginState::STATE_INACTIVE);
+ // retrieve the plugin index and sort them by priority to avoid issue
+ // when moving them
+ std::vector<int> allIndex;
+ for (auto& idx : indices) {
+ allIndex.push_back(idx.row());
+ }
+ std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) {
+ bool cmp = m_ESPs[lhs].priority < m_ESPs[rhs].priority;
+ return offset > 0 ? !cmp : cmp;
+ });
+
+ for (auto index : allIndex) {
+ int newPriority = m_ESPs[index].priority + offset;
+ if (newPriority >= 0 && newPriority < rowCount()) {
+ setPluginPriority(index, newPriority);
}
}
+
+ refreshLoadOrder();
}
-void PluginList::sendToPriority(const QItemSelectionModel *selectionModel, int newPriority)
+void PluginList::toggleState(const QModelIndexList& indices)
{
- if (selectionModel->hasSelection()) {
- std::vector<int> pluginsToMove;
- for (auto row: selectionModel->selectedRows(COL_PRIORITY)) {
- int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].forceEnabled) {
- pluginsToMove.push_back(rowIndex);
- }
+ QModelIndex minRow, maxRow;
+ for (auto& idx : indices) {
+ if (!minRow.isValid() || (idx.row() < minRow.row())) {
+ minRow = idx;
}
- if (pluginsToMove.size()) {
- changePluginPriority(pluginsToMove, newPriority);
+ 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)
@@ -541,7 +536,6 @@ void PluginList::writeLockedOrder(const QString &fileName) const file.commit();
}
-
void PluginList::saveTo(const QString &lockedOrderFileName
, const QString& deleterFileName
, bool hideUnchecked) const
@@ -898,13 +892,11 @@ boost::signals2::connection PluginList::onRefreshed(const std::function<void ()> return m_Refreshed.connect(callback);
}
-
boost::signals2::connection PluginList::onPluginMoved(const std::function<void (const QString &, int, int)> &func)
{
return m_PluginMoved.connect(func);
}
-
void PluginList::updateIndices()
{
m_ESPsByName.clear();
@@ -951,7 +943,6 @@ void PluginList::generatePluginIndexes() emit esplist_changed();
}
-
int PluginList::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
@@ -966,7 +957,6 @@ int PluginList::columnCount(const QModelIndex &) const return COL_LASTCOLUMN + 1;
}
-
void PluginList::testMasters()
{
std::set<QString> enabledMasters;
@@ -998,7 +988,7 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const return checkstateData(modelIndex);
} else if (role == Qt::ForegroundRole) {
return foregroundData(modelIndex);
- } else if (role == Qt::BackgroundRole || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
+ } else if (role == Qt::BackgroundRole) {
return backgroundData(modelIndex);
} else if (role == Qt::FontRole) {
return fontData(modelIndex);
@@ -1371,7 +1361,6 @@ bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int return result;
}
-
QVariant PluginList::headerData(int section, Qt::Orientation orientation,
int role) const
{
@@ -1385,7 +1374,6 @@ QVariant PluginList::headerData(int section, Qt::Orientation orientation, return QAbstractItemModel::headerData(section, orientation, role);
}
-
Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const
{
int index = modelIndex.row();
@@ -1406,7 +1394,6 @@ Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const return result;
}
-
void PluginList::setPluginPriority(int row, int &newPriority)
{
int newPriorityTemp = newPriority;
@@ -1464,7 +1451,6 @@ void PluginList::setPluginPriority(int row, int &newPriority) updateIndices();
}
-
void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
{
ChangeBracket<PluginList> layoutChange(this);
@@ -1562,82 +1548,6 @@ QModelIndex PluginList::parent(const QModelIndex&) const return QModelIndex();
}
-
-bool PluginList::eventFilter(QObject *obj, QEvent *event)
-{
- if (event->type() == QEvent::KeyPress) {
- QAbstractItemView *itemView = qobject_cast<QAbstractItemView*>(obj);
-
- if (itemView == nullptr) {
- return QAbstractItemModel::eventFilter(obj, event);
- }
-
- QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
- // ctrl+up and ctrl+down -> increase or decrease priority of selected plugins
- if ((keyEvent->modifiers() == Qt::ControlModifier) &&
- ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) {
- QItemSelectionModel *selectionModel = itemView->selectionModel();
- const QSortFilterProxyModel *proxyModel = qobject_cast<const QSortFilterProxyModel*>(selectionModel->model());
- if (proxyModel != nullptr) {
- int diff = -1;
- if (((keyEvent->key() == Qt::Key_Up) && (proxyModel->sortOrder() == Qt::DescendingOrder)) ||
- ((keyEvent->key() == Qt::Key_Down) && (proxyModel->sortOrder() == Qt::AscendingOrder))) {
- diff = 1;
- }
- QModelIndexList rows = selectionModel->selectedRows();
- // remove elements that aren't supposed to be movable
- QMutableListIterator<QModelIndex> iter(rows);
- while (iter.hasNext()) {
- if ((iter.next().flags() & Qt::ItemIsDragEnabled) == 0) {
- iter.remove();
- }
- }
- if (keyEvent->key() == Qt::Key_Down) {
- for (int i = 0; i < rows.size() / 2; ++i) {
- rows.swapItemsAt(i, rows.size() - i - 1);
- }
- }
- for (QModelIndex idx : rows) {
- idx = proxyModel->mapToSource(idx);
- int newPriority = m_ESPs[idx.row()].priority + diff;
- if ((newPriority >= 0) && (newPriority < rowCount())) {
- setPluginPriority(idx.row(), newPriority);
- }
- }
- refreshLoadOrder();
- }
- return true;
- } else if (keyEvent->key() == Qt::Key_Space) {
- QItemSelectionModel *selectionModel = itemView->selectionModel();
- const QSortFilterProxyModel *proxyModel = qobject_cast<const QSortFilterProxyModel*>(selectionModel->model());
- QList<QPersistentModelIndex> indices;
- for (QModelIndex idx : selectionModel->selectedRows()) {
- indices.append(idx);
- }
-
- QModelIndex minRow, maxRow;
- for (QModelIndex idx : indices) {
- if (proxyModel != nullptr) {
- idx = proxyModel->mapToSource(idx);
- }
- 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);
-
- return true;
- }
- }
- return QAbstractItemModel::eventFilter(obj, event);
-}
-
-
PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
const QString &originName, const QString &fullPath,
bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
diff --git a/src/pluginlist.h b/src/pluginlist.h index 5f0cef3d..c16bfc98 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -214,12 +214,14 @@ public: bool isESPLocked(int index) const;
void lockESPIndex(int index, bool lock);
- bool eventFilter(QObject *obj, QEvent *event);
-
static QString getColumnName(int column);
static QString getColumnToolTip(int column);
- void highlightPlugins(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry, const Profile &profile);
+ // highlight plugins contained in the mods at the given indices
+ //
+ void highlightPlugins(
+ const std::vector<unsigned int>& modIndices,
+ const MOShared::DirectoryEntry &directoryEntry);
void refreshLoadOrder();
@@ -259,30 +261,25 @@ public: // implementation of the QAbstractTableModel interface public slots:
- /**
- * @brief enables selected plugins
- **/
- void enableSelected(const QItemSelectionModel *selectionModel);
+ // enable/disable all plugins
+ //
+ void setEnabledAll(bool enabled);
- /**
- * @brief disables selected plugins
- **/
- void disableSelected(const QItemSelectionModel *selectionModel);
+ // enable/disable plugins at the given indices.
+ //
+ void setEnabled(const QModelIndexList& indices, bool enabled);
- /**
- * @brief enables ALL plugins
- **/
- void enableAll();
+ // send plugins to the given priority
+ //
+ void sendToPriority(const QModelIndexList& indices, int priority);
- /**
- * @brief disables ALL plugins
- **/
- void disableAll();
+ // shift the priority of mods at the given indices by the given offset
+ //
+ void shiftPluginsPriority(const QModelIndexList& indices, int offset);
- /**
- * @brief moves selected plugins to specified priority
- **/
- void sendToPriority(const QItemSelectionModel *selectionModel, int priority);
+ // toggle the active state of mods at the given indices
+ //
+ void toggleState(const QModelIndexList& indices);
/**
* @brief The currently managed game has changed
diff --git a/src/pluginlistcontextmenu.cpp b/src/pluginlistcontextmenu.cpp new file mode 100644 index 00000000..e6afd996 --- /dev/null +++ b/src/pluginlistcontextmenu.cpp @@ -0,0 +1,144 @@ +#include "pluginlistcontextmenu.h" + +#include <report.h> +#include <utility.h> + +#include "pluginlistview.h" +#include "organizercore.h" + +using namespace MOBase; + +PluginListContextMenu::PluginListContextMenu( + const QModelIndex& index, OrganizerCore& core, PluginListView* view) : + QMenu(view) + , m_core(core) + , m_index(index.model() == view->model() ? view->indexViewToModel(index) : index) + , m_view(view) +{ + if (view->selectionModel()->hasSelection()) { + m_selected = view->indexViewToModel(view->selectionModel()->selectedRows()); + } + else { + m_selected = { index }; + } + + addAction(tr("Enable selected"), [=]() { m_core.pluginList()->setEnabled(m_selected, true); }); + addAction(tr("Disable selected"), [=]() { m_core.pluginList()->setEnabled(m_selected, false); }); + + addSeparator(); + + addAction(tr("Enable all"), [=]() { + if (QMessageBox::question( + m_view->topLevelWidget(), tr("Confirm"), tr("Really enable all plugins?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_core.pluginList()->setEnabledAll(true); + } + }); + addAction(tr("Disable all"), [=]() { + if (QMessageBox::question( + m_view->topLevelWidget(), tr("Confirm"), tr("Really disable all plugins?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_core.pluginList()->setEnabledAll(false); + } + }); + + addSeparator(); + + addMenu(createSendToContextMenu()); + addSeparator(); + + bool hasLocked = false; + bool hasUnlocked = false; + for (auto& idx : m_selected) { + if (m_core.pluginList()->isEnabled(idx.row())) { + if (m_core.pluginList()->isESPLocked(idx.row())) { + hasLocked = true; + } + else { + hasUnlocked = true; + } + } + } + + if (hasLocked) { + addAction(tr("Unlock load order"), [=]() { setESPLock(m_selected, false); }); + } + if (hasUnlocked) { + addAction(tr("Lock load order"), [=]() { setESPLock(m_selected, true); }); + } + + addSeparator(); + + unsigned int modInfoIndex = ModInfo::getIndex(m_core.pluginList()->origin(m_index.data().toString())); + // this is to avoid showing the option on game files like skyrim.esm + if (modInfoIndex != UINT_MAX) { + addAction(tr("Open Origin in Explorer"), [=]() { openOriginExplorer(m_selected); }); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modInfoIndex); + std::vector<ModInfo::EFlag> flags = modInfo->getFlags(); + + if (!modInfo->isForeign() && m_selected.size() == 1) { + QAction* infoAction = addAction(tr("Open Origin Info..."), [=]() { openOriginInformation(index); }); + setDefaultAction(infoAction); + } + } + +} + +QMenu* PluginListContextMenu::createSendToContextMenu() +{ + QMenu* menu = new QMenu(m_view); + menu->setTitle(tr("Send to... ")); + menu->addAction(tr("Top"), [=]() { m_core.pluginList()->sendToPriority(m_selected, 0); }); + menu->addAction(tr("Bottom"), [=]() { m_core.pluginList()->sendToPriority(m_selected, INT_MAX); }); + menu->addAction(tr("Priority..."), [=]() { sendPluginsToPriority(m_selected); }); + return menu; +} + +void PluginListContextMenu::sendPluginsToPriority(const QModelIndexList& indices) +{ + bool ok; + int newPriority = QInputDialog::getInt(m_view->topLevelWidget(), + tr("Set Priority"), tr("Set the priority of the selected plugins"), + 0, 0, INT_MAX, 1, &ok); + if (!ok) return; + + m_core.pluginList()->sendToPriority(m_selected, newPriority); +} + +void PluginListContextMenu::setESPLock(const QModelIndexList& indices, bool locked) +{ + for (auto& idx : indices) { + if (m_core.pluginList()->isEnabled(idx.row())) { + m_core.pluginList()->lockESPIndex(idx.row(), locked); + } + } +} + +void PluginListContextMenu::openOriginExplorer(const QModelIndexList& indices) +{ + for (auto& idx : indices) { + QString fileName = idx.data().toString(); + unsigned int modIndex = ModInfo::getIndex(m_core.pluginList()->origin(fileName)); + if (modIndex == UINT_MAX) { + continue; + } + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + shell::Explore(modInfo->absolutePath()); + } +} + +void PluginListContextMenu::openOriginInformation(const QModelIndex& index) +{ + try { + QString fileName = index.data().toString(); + unsigned int modIndex = ModInfo::getIndex(m_core.pluginList()->origin(fileName)); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + + if (modInfo->isRegular() || modInfo->isOverwrite()) { + emit openModInformation(modIndex); + } + } + catch (const std::exception& e) { + reportError(e.what()); + } +} diff --git a/src/pluginlistcontextmenu.h b/src/pluginlistcontextmenu.h new file mode 100644 index 00000000..0a9a01fe --- /dev/null +++ b/src/pluginlistcontextmenu.h @@ -0,0 +1,54 @@ +#ifndef PLUGINLISTCONTEXTMENU_H +#define PLUGINLISTCONTEXTMENU_H + +#include <QMenu> +#include <QModelIndex> +#include <QTreeView> + +#include "modinfo.h" + +class PluginListView; +class OrganizerCore; + +class PluginListContextMenu : public QMenu +{ + Q_OBJECT + +public: + + // creates a new context menu, the given index is the one for the click and should be valid + // + PluginListContextMenu( + const QModelIndex& index, OrganizerCore& core, PluginListView* modListView); + +signals: + + // emitted to open a mod information + // + void openModInformation(unsigned int modIndex); + +public: + + // create the "Send to... " context menu + // + QMenu* createSendToContextMenu(); + void sendPluginsToPriority(const QModelIndexList& indices); + + // set ESP lock on the given plugins + // + void setESPLock(const QModelIndexList& indices, bool locked); + + // open explorer or mod information for the origin of the plugins + // + void openOriginExplorer(const QModelIndexList& indices); + void openOriginInformation(const QModelIndex& index); + + + OrganizerCore& m_core; + QModelIndex m_index; + QModelIndexList m_selected; + PluginListView* m_view; + +}; + +#endif diff --git a/src/pluginlistsortproxy.cpp b/src/pluginlistsortproxy.cpp index 3d13798e..44285fd1 100644 --- a/src/pluginlistsortproxy.cpp +++ b/src/pluginlistsortproxy.cpp @@ -36,10 +36,6 @@ PluginListSortProxy::PluginListSortProxy(QObject *parent) this->setDynamicSortFilter(true);
}
-
-
-
-
void PluginListSortProxy::setEnabledColumns(unsigned int columns)
{
emit layoutAboutToBeChanged();
@@ -49,33 +45,17 @@ void PluginListSortProxy::setEnabledColumns(unsigned int columns) emit layoutChanged();
}
-
-Qt::ItemFlags PluginListSortProxy::flags(const QModelIndex &modelIndex) const
-{
-/* Qt::ItemFlags flags;
- QModelIndex index = mapToSource(modelIndex);
- if (index.isValid()) {
- flags = sourceModel()->flags(index);
- }
- return flags;*/
-
- return sourceModel()->flags(mapToSource(modelIndex));
-}
-
-
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
{
@@ -109,7 +89,6 @@ bool PluginListSortProxy::lessThan(const QModelIndex &left, }
}
-
bool PluginListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent)
{
@@ -136,7 +115,6 @@ bool PluginListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction act sourceIndex.parent());
}
-
bool PluginListSortProxy::filterMatchesPlugin(const QString &plugin) const
{
if (!m_CurrentFilter.isEmpty()) {
@@ -173,4 +151,3 @@ bool PluginListSortProxy::filterMatchesPlugin(const QString &plugin) const else
return true;
}
-
diff --git a/src/pluginlistsortproxy.h b/src/pluginlistsortproxy.h index 03b079e2..2acf83b3 100644 --- a/src/pluginlistsortproxy.h +++ b/src/pluginlistsortproxy.h @@ -42,8 +42,6 @@ public: void setEnabledColumns(unsigned int columns);
- virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const;
-
virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
bool filterMatchesPlugin(const QString &plugin) const;
diff --git a/src/pluginlistview.cpp b/src/pluginlistview.cpp index a265d5d4..e27d7e66 100644 --- a/src/pluginlistview.cpp +++ b/src/pluginlistview.cpp @@ -1,58 +1,338 @@ #include "pluginlistview.h" -#include <widgetutility.h> + #include <QUrl> #include <QMimeData> -#include <QProxyStyle> +#include <report.h> +#include <widgetutility.h> + +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "organizercore.h" +#include "pluginlistsortproxy.h" +#include "pluginlistcontextmenu.h" +#include "modlistview.h" +#include "copyeventfilter.h" +#include "modlistviewactions.h" +#include "genericicondelegate.h" +#include "modelutils.h" -class PluginListViewStyle : public QProxyStyle { -public: - PluginListViewStyle(QStyle *style, int indentation); +using namespace MOBase; + +PluginListView::PluginListView(QWidget *parent) + : QTreeView(parent) + , m_sortProxy(nullptr) + , m_Scrollbar(new ViewMarkingScrollBar(this, Qt::BackgroundRole)) + , m_didUpdateMasterList(false) +{ + setVerticalScrollBar(m_Scrollbar); + MOBase::setCustomizableColumns(this); + installEventFilter(new CopyEventFilter(this)); +} - void drawPrimitive(PrimitiveElement element, const QStyleOption *option, - QPainter *painter, const QWidget *widget = 0) const; -private: - int m_Indentation; -}; +int PluginListView::sortColumn() const +{ + return m_sortProxy ? m_sortProxy->sortColumn() : -1; +} -PluginListViewStyle::PluginListViewStyle(QStyle *style, int indentation) - : QProxyStyle(style), m_Indentation(indentation) +QModelIndex PluginListView::indexModelToView(const QModelIndex& index) const { + return MOShared::indexModelToView(index, this); } -void PluginListViewStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option, - QPainter *painter, const QWidget *widget) const +QModelIndexList PluginListView::indexModelToView(const QModelIndexList& index) const { - if (element == QStyle::PE_IndicatorItemViewItemDrop && !option->rect.isNull()) { - QStyleOption opt(*option); - opt.rect.setLeft(m_Indentation); - if (widget) { - opt.rect.setRight(widget->width() - 5); // 5 is an arbitrary value that seems to work ok + return MOShared::indexModelToView(index, this); +} + +QModelIndex PluginListView::indexViewToModel(const QModelIndex& index) const +{ + return MOShared::indexViewToModel(index, m_core->pluginList()); +} + +QModelIndexList PluginListView::indexViewToModel(const QModelIndexList& index) const +{ + return MOShared::indexViewToModel(index, m_core->pluginList()); +} + +void PluginListView::updatePluginCount() +{ + int activeMasterCount = 0; + int activeLightMasterCount = 0; + int activeRegularCount = 0; + int masterCount = 0; + int lightMasterCount = 0; + int regularCount = 0; + int activeVisibleCount = 0; + + PluginList* list = m_core->pluginList(); + QString filter = ui.filter->text(); + + for (QString plugin : list->pluginNames()) { + bool active = list->isEnabled(plugin); + bool visible = m_sortProxy->filterMatchesPlugin(plugin); + if (list->isLight(plugin) || list->isLightFlagged(plugin)) { + lightMasterCount++; + activeLightMasterCount += active; + activeVisibleCount += visible && active; + } + else if (list->isMaster(plugin)) { + masterCount++; + activeMasterCount += active; + activeVisibleCount += visible && active; + } + else { + regularCount++; + activeRegularCount += active; + activeVisibleCount += visible && active; } - QProxyStyle::drawPrimitive(element, &opt, painter, widget); + } + + int activeCount = activeMasterCount + activeLightMasterCount + activeRegularCount; + int totalCount = masterCount + lightMasterCount + regularCount; + + ui.counter->display(activeVisibleCount); + ui.counter->setToolTip(tr("<table cellspacing=\"6\">" + "<tr><th>Type</th><th>Active </th><th>Total</th></tr>" + "<tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr>" + "<tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr>" + "<tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr>" + "<tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr>" + "<tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr>" + "</table>") + .arg(activeCount).arg(totalCount) + .arg(activeMasterCount).arg(masterCount) + .arg(activeLightMasterCount).arg(lightMasterCount) + .arg(activeRegularCount).arg(regularCount) + .arg(activeMasterCount + activeRegularCount).arg(masterCount + regularCount) + ); +} + +void PluginListView::onFilterChanged(const QString& filter) +{ + if (!filter.isEmpty()) { + setStyleSheet("QTreeView { border: 2px ridge #f00; }"); + ui.counter->setStyleSheet("QLCDNumber { border: 2px ridge #f00; }"); } else { - QProxyStyle::drawPrimitive(element, option, painter, widget); + setStyleSheet(""); + ui.counter->setStyleSheet(""); } + updatePluginCount(); } -PluginListView::PluginListView(QWidget *parent) - : QTreeView(parent) - , m_Scrollbar(new ViewMarkingScrollBar(this->model(), this)) +void PluginListView::onSortButtonClicked() { - setVerticalScrollBar(m_Scrollbar); - MOBase::setCustomizableColumns(this); + const bool offline = m_core->settings().network().offlineMode(); + + auto r = QMessageBox::No; + + if (offline) { + r = QMessageBox::question( + topLevelWidget(), tr("Sorting plugins"), + tr("Are you sure you want to sort your plugins list?") + "\r\n\r\n" + + tr("Note: You are currently in offline mode and LOOT will not update the master list."), + QMessageBox::Yes | QMessageBox::No); + } + else { + r = QMessageBox::question( + topLevelWidget(), tr("Sorting plugins"), + tr("Are you sure you want to sort your plugins list?"), + QMessageBox::Yes | QMessageBox::No); + } + + if (r != QMessageBox::Yes) { + return; + } + + m_core->savePluginList(); + + topLevelWidget()->setEnabled(false); + Guard g([=]() { topLevelWidget()->setEnabled(true); }); + + // don't try to update the master list in offline mode + const bool didUpdateMasterList = offline ? true : m_didUpdateMasterList; + + if (runLoot(topLevelWidget(), *m_core, didUpdateMasterList)) { + // don't assume the master list was updated in offline mode + if (!offline) { + m_didUpdateMasterList = true; + } + + m_core->refreshESPList(false); + m_core->savePluginList(); + } +} + +std::pair<QModelIndex, QModelIndexList> PluginListView::selected() const +{ + return { indexViewToModel(currentIndex()), indexViewToModel(selectionModel()->selectedRows()) }; } -void PluginListView::dragEnterEvent(QDragEnterEvent *event) +void PluginListView::setSelected(const QModelIndex& current, const QModelIndexList& selected) { - emit dropModeUpdate(event->mimeData()->hasUrls()); + setCurrentIndex(indexModelToView(current)); + for (auto idx : selected) { + selectionModel()->select(indexModelToView(idx), QItemSelectionModel::Select | QItemSelectionModel::Rows); + } +} + + +void PluginListView::setup(OrganizerCore& core, MainWindow* mw, Ui::MainWindow* mwui) +{ + m_core = &core; + ui = { mwui->activePluginsCounter, mwui->espFilterEdit }; + m_modActions = &mwui->modList->actions(); + + m_sortProxy = new PluginListSortProxy(&core); + m_sortProxy->setSourceModel(core.pluginList()); + setModel(m_sortProxy); + + sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder); + setItemDelegateForColumn(PluginList::COL_FLAGS, new GenericIconDelegate(this)); - QTreeView::dragEnterEvent(event); + // counter + connect(core.pluginList(), &PluginList::writePluginsList, [=]{ updatePluginCount(); }); + connect(core.pluginList(), &PluginList::esplist_changed, [=]{ updatePluginCount(); }); + + // sort + connect(mwui->bossButton, &QPushButton::clicked, [=]{ onSortButtonClicked(); }); + + // filter + connect(ui.filter, &QLineEdit::textChanged, m_sortProxy, &PluginListSortProxy::updateFilter); + connect(ui.filter, &QLineEdit::textChanged, this, &PluginListView::onFilterChanged); + + // highligth mod list when selected + connect(selectionModel(), &QItemSelectionModel::selectionChanged, [=](auto&& selected) { + std::vector<unsigned int> pluginIndices; + for (auto& idx : indexViewToModel(selectionModel()->selectedRows())) { + pluginIndices.push_back(idx.row()); + } + mwui->modList->setHighlightedMods(pluginIndices); + }); + + // using a lambda here to avoid storing the mod list actions + connect(this, &QTreeView::customContextMenuRequested, [=](auto&& pos) { onCustomContextMenuRequested(pos); }); + connect(this, &QTreeView::doubleClicked, [=](auto&& index) { onDoubleClicked(index); }); } -void PluginListView::setModel(QAbstractItemModel *model) +void PluginListView::onCustomContextMenuRequested(const QPoint& pos) { - QTreeView::setModel(model); - setVerticalScrollBar(new ViewMarkingScrollBar(model, this)); + try { + PluginListContextMenu menu(indexViewToModel(indexAt(pos)), *m_core, this); + connect(&menu, &PluginListContextMenu::openModInformation, [=](auto&& modIndex) { + m_modActions->displayModInformation(modIndex); }); + menu.exec(viewport()->mapToGlobal(pos)); + } + catch (const std::exception& e) { + reportError(tr("Exception: ").arg(e.what())); + } + catch (...) { + reportError(tr("Unknown exception")); + } +} + +void PluginListView::onDoubleClicked(const QModelIndex& index) +{ + if (!index.isValid()) { + return; + } + + if (m_core->pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { + // don't interpret double click if we only just checked a plugin + return; + } + + try { + if (selectionModel()->hasSelection() && selectionModel()->selectedRows().count() == 1) { + + QModelIndex idx = selectionModel()->currentIndex(); + QString fileName = idx.data().toString(); + + if (ModInfo::getIndex(m_core->pluginList()->origin(fileName)) == UINT_MAX) { + return; + } + + auto modIndex = ModInfo::getIndex(m_core->pluginList()->origin(fileName)); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + + if (modInfo->isRegular() || modInfo->isOverwrite()) { + + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + m_modActions->openExplorer({ m_core->modList()->index(modIndex, 0) }); + } + else { + m_modActions->displayModInformation(ModInfo::getIndex(m_core->pluginList()->origin(fileName))); + } + + // workaround to cancel the editor that might have opened because of + // selection-click + closePersistentEditor(index); + } + } + } + catch (const std::exception& e) { + reportError(e.what()); + } +} + +bool PluginListView::moveSelection(int key) +{ + auto [cindex, sourceRows] = selected(); + + int offset = key == Qt::Key_Up ? -1 : 1; + if (m_sortProxy->sortOrder() == Qt::DescendingOrder) { + offset = -offset; + } + + m_core->pluginList()->shiftPluginsPriority(sourceRows, offset); + + // reset the selection and the index + setSelected(cindex, sourceRows); + + return true; +} + +bool PluginListView::toggleSelectionState() +{ + if (!selectionModel()->hasSelection()) { + return true; + } + m_core->pluginList()->toggleState(indexViewToModel(selectionModel()->selectedRows())); + return true; +} + +bool PluginListView::event(QEvent* event) +{ + auto* profile = m_core->currentProfile(); + if (event->type() == QEvent::KeyPress && profile) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + + if (keyEvent->modifiers() == Qt::ControlModifier + && (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)) { + if (selectionModel()->hasSelection() && selectionModel()->selectedRows().count() == 1) { + QModelIndex idx = selectionModel()->currentIndex(); + QString fileName = idx.data().toString(); + + if (ModInfo::getIndex(m_core->pluginList()->origin(fileName)) == UINT_MAX) { + return false; + } + + auto modIndex = ModInfo::getIndex(m_core->pluginList()->origin(fileName)); + m_modActions->openExplorer({ m_core->modList()->index(modIndex, 0) }); + return true; + } + } + else if (keyEvent->modifiers() == Qt::ControlModifier + && (sortColumn() == PluginList::COL_PRIORITY || sortColumn() == PluginList::COL_MODINDEX) + && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) { + return moveSelection(keyEvent->key()); + } + else if (keyEvent->key() == Qt::Key_Space) { + return toggleSelectionState(); + } + return QTreeView::event(event); + } + return QTreeView::event(event); } diff --git a/src/pluginlistview.h b/src/pluginlistview.h index bdd4ee61..ed6458da 100644 --- a/src/pluginlistview.h +++ b/src/pluginlistview.h @@ -5,20 +5,82 @@ #include <QDragEnterEvent> #include "viewmarkingscrollbar.h" +namespace Ui { + class MainWindow; +} + +class OrganizerCore; +class MainWindow; +class ModListViewActions; +class PluginListSortProxy; + class PluginListView : public QTreeView { Q_OBJECT public: - explicit PluginListView(QWidget *parent = 0); - virtual void dragEnterEvent(QDragEnterEvent *event); - virtual void setModel(QAbstractItemModel *model); -signals: - void dropModeUpdate(bool dropOnRows); + explicit PluginListView(QWidget* parent = nullptr); + + void setup(OrganizerCore& core, MainWindow* mw, Ui::MainWindow* mwui); + + // the column by which the plugin list is currently sorted + // + int sortColumn() const; + + // update the plugin counter + // + void updatePluginCount(); + +protected slots: + + void onCustomContextMenuRequested(const QPoint& pos); + void onDoubleClicked(const QModelIndex& index); + + void onFilterChanged(const QString& filter); + void onSortButtonClicked(); + +protected: + + friend class PluginListContextMenu; + + // 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; + + // method to react to various key events + // + bool moveSelection(int key); + bool toggleSelectionState(); + + // get/set the selected items on the view, this method return/take indices + // from the mod list model, not the view, so it's safe to restore + // + std::pair<QModelIndex, QModelIndexList> selected() const; + void setSelected(const QModelIndex& current, const QModelIndexList& selected); + + bool event(QEvent* event) override; - public slots: private: - ViewMarkingScrollBar *m_Scrollbar; + struct PluginListViewUi + { + // the plguin counter + QLCDNumber* counter; + + // the filter + QLineEdit* filter; + }; + + OrganizerCore* m_core; + PluginListViewUi ui; + + PluginListSortProxy* m_sortProxy; + ModListViewActions* m_modActions; + ViewMarkingScrollBar* m_Scrollbar; + + bool m_didUpdateMasterList; }; #endif // PLUGINLISTVIEW_H diff --git a/src/profile.h b/src/profile.h index b8628276..04452ff6 100644 --- a/src/profile.h +++ b/src/profile.h @@ -250,7 +250,7 @@ public: * * @return map of indexes by priority **/ - std::map<int, unsigned int> getAllIndexesByPriority() { return m_ModIndexByPriority; } + const std::map<int, unsigned int>& getAllIndexesByPriority() { return m_ModIndexByPriority; } /** * retrieve the number of mods for which this object has status information. diff --git a/src/qtgroupingproxy.cpp b/src/qtgroupingproxy.cpp index ff9539d7..a8c7ce1d 100644 --- a/src/qtgroupingproxy.cpp +++ b/src/qtgroupingproxy.cpp @@ -34,42 +34,50 @@ using namespace MOBase; \ingroup model-view
*/
-QtGroupingProxy::QtGroupingProxy(QAbstractItemModel *model, QModelIndex rootNode, int groupedColumn, int groupedRole, unsigned int flags , int aggregateRole)
+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 )
+ , m_rootNode(rootNode)
+ , m_groupedColumn(0)
+ , m_groupedRole(groupedRole)
+ , m_aggregateRole(aggregateRole)
+ , m_flags(flags)
{
- setSourceModel( model );
-
- // signal proxies
- connect( sourceModel(),
- SIGNAL( dataChanged( const QModelIndex&, const QModelIndex& ) ),
- this, SLOT( modelDataChanged( const QModelIndex&, const QModelIndex& ) )
- );
- 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()) );
-
- if( groupedColumn != -1 )
- setGroupedColumn( groupedColumn );
+ 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 )
{
@@ -205,13 +213,6 @@ QtGroupingProxy::buildTree() }
endResetModel();
- // restore expand-state
- for( int row = 0; row < rowCount(); row++ ) {
- QModelIndex idx = index( row, 0, QModelIndex() );
- if (m_expandedItems.contains(idx.data(Qt::UserRole).toString())) {
- emit expandItem(idx);
- }
- }
}
QList<int>
@@ -721,7 +722,6 @@ QtGroupingProxy::flags( const QModelIndex &idx ) const m_rootNode.parent() );
if ( (originalIdx.flags() & Qt::ItemIsUserCheckable) == 0 )
{
- log::debug("row {} is not checkable", originalRow);
checkable = false;
}
}
@@ -1036,14 +1036,3 @@ QtGroupingProxy::dumpGroups() const log::debug("{}", s);
}
-
-
-void QtGroupingProxy::expanded(const QModelIndex &index)
-{
- m_expandedItems.insert(index.data(Qt::UserRole).toString());
-}
-
-void QtGroupingProxy::collapsed(const QModelIndex &index)
-{
- m_expandedItems.remove(index.data(Qt::UserRole).toString());
-}
diff --git a/src/qtgroupingproxy.h b/src/qtgroupingproxy.h index 6567cb0e..c4459297 100644 --- a/src/qtgroupingproxy.h +++ b/src/qtgroupingproxy.h @@ -47,12 +47,13 @@ public: };
public:
- explicit QtGroupingProxy( QAbstractItemModel *model, QModelIndex rootNode = QModelIndex(),
+ 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 */
@@ -79,22 +80,6 @@ public: virtual QModelIndex addEmptyGroup( const RowData &data );
virtual bool removeGroup( const QModelIndex &idx );
- QStringList expandedState();
-
-signals:
- void expandItem(const QModelIndex &index);
-
-public slots:
- /**
- * @brief update expanded state
- * @param index index of the expanded/collapsed item (from the base model!)
- */
- void expanded(const QModelIndex &index);
- /**
- * @brief update expanded state
- * @param index index of the expanded/collapsed item (from the base model!)
- */
- void collapsed(const QModelIndex &index);
protected slots:
virtual void buildTree();
@@ -158,7 +143,6 @@ protected: void dumpGroups() const;
private:
- QSet<QString> m_expandedItems;
unsigned int m_flags;
int m_groupedRole;
diff --git a/src/settings.cpp b/src/settings.cpp index f5ad83a7..e04cd0c1 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -24,12 +24,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "instancemanager.h" #include "shared/appconfig.h" #include "env.h" +#include "modelutils.h" #include "envmetrics.h" #include <expanderwidget.h> #include <utility.h> #include <iplugingame.h> using namespace MOBase; +using namespace MOShared; EndorsementState endorsementStateFromString(const QString& s) @@ -1063,6 +1065,52 @@ WidgetSettings::WidgetSettings(QSettings& s, bool globalInstance) } } +void WidgetSettings::saveTreeCheckState(const QTreeView* tv, int role) +{ + QVariantList data; + for (auto index : flatIndex(tv->model())) { + data.append(index.data(role)); + } + set(m_Settings, "Widgets", indexSettingName(tv), data); +} + +void WidgetSettings::restoreTreeCheckState(QTreeView* tv, int role) const +{ + if (auto states = getOptional<QVariantList>(m_Settings, "Widgets", indexSettingName(tv))) { + auto allIndex = flatIndex(tv->model()); + MOBase::log::debug("restoreTreeCheckState: {}, {}", states->size(), allIndex.size()); + if (states->size() != allIndex.size()) { + return; + } + for (int i = 0; i < states->size(); ++i) { + tv->model()->setData(allIndex[i], states->at(i), role); + } + } +} + +void WidgetSettings::saveTreeExpandState(const QTreeView* tv, int role) +{ + QVariantList expanded; + for (auto index : flatIndex(tv->model())) { + if (tv->isExpanded(index)) { + expanded.append(index.data(role)); + } + } + set(m_Settings, "Widgets", indexSettingName(tv), expanded); +} + +void WidgetSettings::restoreTreeExpandState(QTreeView* tv, int role) const +{ + if (auto expanded = getOptional<QVariantList>(m_Settings, "Widgets", indexSettingName(tv))) { + tv->collapseAll(); + for (auto index : flatIndex(tv->model())) { + if (expanded->contains(index.data(role))) { + tv->expand(index); + } + } + } +} + std::optional<int> WidgetSettings::index(const QComboBox* cb) const { return getOptional<int>(m_Settings, "Widgets", indexSettingName(cb)); @@ -2127,6 +2175,26 @@ void InterfaceSettings::setStyleName(const QString& name) set(m_Settings, "Settings", "style", name); } +bool InterfaceSettings::collapsibleSeparators() const +{ + return get<bool>(m_Settings, "Settings", "collapsible_separators", true); +} + +void InterfaceSettings::setCollapsibleSeparators(bool b) +{ + set(m_Settings, "Settings", "collapsible_separators", b); +} + +bool InterfaceSettings::collapsibleSeparatorsConflicts() const +{ + return get<bool>(m_Settings, "Settings", "collapsible_separators_conflicts", true); +} + +void InterfaceSettings::setCollapsibleSeparatorsConflicts(bool b) +{ + set(m_Settings, "Settings", "collapsible_separators_conflicts", b); +} + bool InterfaceSettings::compactDownloads() const { return get<bool>(m_Settings, "Settings", "compact_downloads", false); diff --git a/src/settings.h b/src/settings.h index 4d1258bf..043b22a4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -202,6 +202,16 @@ public: // WidgetSettings(QSettings& s, bool globalInstance); + // tree item check - this saves the list of expanded items based on the given role + // + void saveTreeCheckState(const QTreeView* tv, int role = Qt::CheckStateRole); + void restoreTreeCheckState(QTreeView* tv, int role = Qt::CheckStateRole) const; + + // tree state - this saves the list of expanded items based on the given role + // + void saveTreeExpandState(const QTreeView* tv, int role = Qt::DisplayRole); + void restoreTreeExpandState(QTreeView* tv, int role = Qt::DisplayRole) const; + // selected index for a combobox // std::optional<int> index(const QComboBox* cb) const; @@ -611,6 +621,16 @@ public: std::optional<QString> styleName() const; void setStyleName(const QString& name); + // whether to use collapsible separators when possible + // + bool collapsibleSeparators() const; + void setCollapsibleSeparators(bool b); + + // whether to display mod conflicts on separators when collapsed + // + bool collapsibleSeparatorsConflicts() const; + void setCollapsibleSeparatorsConflicts(bool b); + // whether to show compact downloads // bool compactDownloads() const; diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 43af5e9f..ae4f4f34 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -23,8 +23,8 @@ <attribute name="title"> <string>General</string> </attribute> - <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,1"> - <item row="5" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,1,0"> + <item row="6" column="0" colspan="2"> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -37,7 +37,55 @@ </property> </spacer> </item> - <item row="0" column="0" rowspan="2"> + <item row="2" column="1"> + <widget class="QGroupBox" name="groupBox_6"> + <property name="title"> + <string>Updates</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QCheckBox" name="checkForUpdates"> + <property name="toolTip"> + <string>Check for Mod Organizer updates on Github on startup.</string> + </property> + <property name="whatsThis"> + <string>Check for Mod Organizer updates on Github on startup.</string> + </property> + <property name="text"> + <string>Check for updates</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="usePrereleaseBox"> + <property name="toolTip"> + <string>Update to non-stable releases.</string> + </property> + <property name="whatsThis"> + <string>Update to non-stable releases.</string> + </property> + <property name="text"> + <string>Install Pre-releases (Betas)</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_9"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item row="0" column="0" rowspan="3"> <widget class="QGroupBox" name="groupBox"> <property name="title"> <string>User Interface</string> @@ -241,7 +289,7 @@ </layout> </widget> </item> - <item row="3" column="0" colspan="2"> + <item row="4" column="0" colspan="2"> <widget class="QGroupBox" name="ModlistGroupBox"> <property name="title"> <string>Colors</string> @@ -342,42 +390,57 @@ </widget> </item> <item row="1" column="1"> - <widget class="QGroupBox" name="groupBox_6"> + <widget class="QGroupBox" name="groupBox_4"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> <property name="title"> - <string>Updates</string> + <string>Mod List</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> + <layout class="QVBoxLayout" name="verticalLayout_17"> <item> - <widget class="QCheckBox" name="checkForUpdates"> + <widget class="QCheckBox" name="collapsibleSeparatorsBox"> <property name="toolTip"> - <string>Check for Mod Organizer updates on Github on startup.</string> - </property> - <property name="whatsThis"> - <string>Check for Mod Organizer updates on Github on startup.</string> + <string>Allow collapsing separators when sorting by ascending priority.</string> </property> <property name="text"> - <string>Check for updates</string> + <string>Use collapsible separators</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <property name="tristate"> + <bool>false</bool> </property> </widget> </item> <item> - <widget class="QCheckBox" name="usePrereleaseBox"> + <widget class="QCheckBox" name="collapsibleSeparatorsConflictsBox"> <property name="toolTip"> - <string>Update to non-stable releases.</string> + <string>Display mod conflicts on separator when collapsed.</string> </property> <property name="whatsThis"> - <string>Update to non-stable releases.</string> + <string>Display mod conflicts on separator when collapsed.</string> </property> <property name="text"> - <string>Install Pre-releases (Betas)</string> + <string>Show conflicts on separators</string> + </property> + <property name="checked"> + <bool>true</bool> </property> </widget> </item> <item> - <spacer name="verticalSpacer_9"> + <spacer name="verticalSpacer_12"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> <property name="sizeHint" stdset="0"> <size> <width>0</width> diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index f29e1d24..7b854260 100644 --- a/src/settingsdialoggeneral.cpp +++ b/src/settingsdialoggeneral.cpp @@ -19,6 +19,11 @@ GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) ui->colorTable->load(s); + // connect before setting to trigger + QObject::connect(ui->collapsibleSeparatorsBox, &QCheckBox::stateChanged, [=](auto&& state) { + ui->collapsibleSeparatorsConflictsBox->setEnabled(state == Qt::Checked); + }); + ui->centerDialogs->setChecked(settings().geometry().centerDialogs()); ui->changeGameConfirmation->setChecked(settings().interface().showChangeGameConfirmation()); ui->doubleClickPreviews->setChecked(settings().interface().doubleClicksOpenPreviews()); @@ -27,6 +32,8 @@ GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) ui->checkForUpdates->setChecked(settings().checkForUpdates()); ui->usePrereleaseBox->setChecked(settings().usePrereleases()); ui->colorSeparatorsBox->setChecked(settings().colors().colorSeparatorScrollbar()); + ui->collapsibleSeparatorsConflictsBox->setChecked(settings().interface().collapsibleSeparatorsConflicts()); + ui->collapsibleSeparatorsBox->setChecked(settings().interface().collapsibleSeparators()); QObject::connect(ui->exploreStyles, &QPushButton::clicked, [&]{ onExploreStyles(); }); @@ -70,6 +77,8 @@ void GeneralSettingsTab::update() settings().setCheckForUpdates(ui->checkForUpdates->isChecked()); settings().setUsePrereleases(ui->usePrereleaseBox->isChecked()); settings().colors().setColorSeparatorScrollbar(ui->colorSeparatorsBox->isChecked()); + settings().interface().setCollapsibleSeparators(ui->collapsibleSeparatorsBox->isChecked()); + settings().interface().setCollapsibleSeparatorsConflicts(ui->collapsibleSeparatorsConflictsBox->isChecked()); } void GeneralSettingsTab::addLanguages() diff --git a/src/settingsutilities.h b/src/settingsutilities.h index ac6aeb29..53eeff87 100644 --- a/src/settingsutilities.h +++ b/src/settingsutilities.h @@ -25,6 +25,14 @@ struct ValueConverter<T, std::enable_if_t<std::is_enum_v<T>>> } }; +template <> +struct ValueConverter<QVariantList> +{ + static QString convert(const QVariantList& t) + { + return QString("%1").arg(QVariant(t).toStringList().join(",")); + } +}; bool shouldLogSetting(const QString& displayName); diff --git a/src/viewmarkingscrollbar.cpp b/src/viewmarkingscrollbar.cpp index 2452d0e3..56754237 100644 --- a/src/viewmarkingscrollbar.cpp +++ b/src/viewmarkingscrollbar.cpp @@ -1,20 +1,32 @@ #include "viewmarkingscrollbar.h"
+#include "modelutils.h"
#include <QStyle>
#include <QStyleOptionSlider>
#include <QPainter>
-ViewMarkingScrollBar::ViewMarkingScrollBar(QAbstractItemModel *model, QWidget *parent, int role)
- : QScrollBar(parent)
- , m_Model(model)
- , m_Role(role)
+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);
}
-void ViewMarkingScrollBar::paintEvent(QPaintEvent *event)
+QColor ViewMarkingScrollBar::color(const QModelIndex& index) const
+{
+ auto data = index.data(m_role);
+ if (data.canConvert<QColor>()) {
+ return data.value<QColor>();
+ }
+ return QColor();
+}
+
+void ViewMarkingScrollBar::paintEvent(QPaintEvent* event)
{
- if (m_Model == nullptr) {
+ if (m_view->model() == nullptr) {
return;
}
QScrollBar::paintEvent(event);
@@ -27,15 +39,16 @@ void ViewMarkingScrollBar::paintEvent(QPaintEvent *event) QRect handleRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption, QStyle::SC_ScrollBarSlider, this);
QRect innerRect = style()->subControlRect(QStyle::CC_ScrollBar, &styleOption, QStyle::SC_ScrollBarGroove, this);
+ auto indices = visibleIndex(m_view, 0);
+
painter.translate(innerRect.topLeft() + QPoint(0, 3));
- qreal scale = static_cast<qreal>(innerRect.height() - 3) / static_cast<qreal>(m_Model->rowCount());
+ qreal scale = static_cast<qreal>(innerRect.height() - 3) / static_cast<qreal>(indices.size());
- for (int i = 0; i < m_Model->rowCount(); ++i) {
- QVariant data = m_Model->data(m_Model->index(i, 0), m_Role);
- if (data.isValid()) {
- QColor col = data.value<QColor>();
- painter.setPen(col);
- painter.setBrush(col);
+ 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 12a297d1..01b5a8c0 100644 --- a/src/viewmarkingscrollbar.h +++ b/src/viewmarkingscrollbar.h @@ -1,21 +1,25 @@ #ifndef VIEWMARKINGSCROLLBAR_H
#define VIEWMARKINGSCROLLBAR_H
+#include <QTreeView>
#include <QScrollBar>
-#include <QAbstractItemModel>
class ViewMarkingScrollBar : public QScrollBar
{
public:
- static const int DEFAULT_ROLE = Qt::UserRole + 42;
-public:
- ViewMarkingScrollBar(QAbstractItemModel *model, QWidget *parent = 0, int role = DEFAULT_ROLE);
+ ViewMarkingScrollBar(QTreeView* view, int role);
+
protected:
- virtual void paintEvent(QPaintEvent *event);
+ void paintEvent(QPaintEvent *event) override;
+
+ // retrieve the color of the marker for the given index
+ //
+ virtual QColor color(const QModelIndex& index) const;
+
private:
- QAbstractItemModel *m_Model;
- int m_Role;
+ QTreeView* m_view;
+ int m_role;
};
|
