summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/aboutdialog.ui17
-rw-r--r--src/createinstancedialogpages.cpp1
-rw-r--r--src/datatab.h5
-rw-r--r--src/downloadlist.cpp80
-rw-r--r--src/downloadlist.h4
-rw-r--r--src/downloadlistsortproxy.cpp122
-rw-r--r--src/downloadlistsortproxy.h58
-rw-r--r--src/downloadlistwidget.cpp18
-rw-r--r--src/downloadlistwidget.h9
-rw-r--r--src/downloadmanager.cpp93
-rw-r--r--src/downloadmanager.h4
-rw-r--r--src/downloadstab.cpp77
-rw-r--r--src/downloadstab.h36
-rw-r--r--src/editexecutablesdialog.cpp66
-rw-r--r--src/editexecutablesdialog.h3
-rw-r--r--src/editexecutablesdialog.ui3
-rw-r--r--src/env.cpp39
-rw-r--r--src/env.h4
-rw-r--r--src/envfs.cpp25
-rw-r--r--src/envfs.h5
-rw-r--r--src/envmodule.cpp23
-rw-r--r--src/envmodule.h4
-rw-r--r--src/instancemanager.cpp4
-rw-r--r--src/instancemanagerdialog.cpp1
-rw-r--r--src/lootdialog.cpp2
-rw-r--r--src/mainwindow.cpp490
-rw-r--r--src/mainwindow.h36
-rw-r--r--src/mainwindow.ui16
-rw-r--r--src/moapplication.cpp8
-rw-r--r--src/modinfodialogconflicts.cpp6
-rw-r--r--src/modinfodialogimages.cpp1
-rw-r--r--src/modinfodialognexus.cpp12
-rw-r--r--src/modinfodialogtextfiles.cpp1
-rw-r--r--src/nxmaccessmanager.cpp25
-rw-r--r--src/organizercore.cpp2
-rw-r--r--src/pch.h1
-rw-r--r--src/profile.cpp8
-rw-r--r--src/profilesdialog.cpp35
-rw-r--r--src/profilesdialog.h10
-rw-r--r--src/profilesdialog.ui355
-rw-r--r--src/sanitychecks.cpp62
-rw-r--r--src/savestab.cpp310
-rw-r--r--src/savestab.h64
-rw-r--r--src/settings.cpp125
-rw-r--r--src/settings.h27
-rw-r--r--src/settingsdialog.ui90
-rw-r--r--src/settingsdialognexus.cpp37
-rw-r--r--src/settingsdialognexus.h7
-rw-r--r--src/shared/appconfig.inc2
-rw-r--r--src/shared/directoryentry.cpp2
-rw-r--r--src/shared/util.cpp5
52 files changed, 1546 insertions, 897 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 117528f8..98bbe05f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -63,7 +63,6 @@ add_filter(NAME src/dialogs GROUPS
add_filter(NAME src/downloads GROUPS
downloadlist
- downloadlistsortproxy
downloadlistwidget
downloadmanager
)
@@ -99,12 +98,14 @@ add_filter(NAME src/loot GROUPS
add_filter(NAME src/mainwindow GROUPS
datatab
+ downloadstab
iconfetcher
filetree
filetreeitem
filetreemodel
filterlist
mainwindow
+ savestab
statusbar
)
diff --git a/src/aboutdialog.ui b/src/aboutdialog.ui
index 7a7627a0..e1f7aef3 100644
--- a/src/aboutdialog.ui
+++ b/src/aboutdialog.ui
@@ -441,12 +441,17 @@
</item>
<item>
<property name="text">
- <string notr="true">DoubleYou</string>
+ <string notr="true">BlueAmulet</string>
</property>
</item>
<item>
<property name="text">
- <string notr="true">Drew Warwick</string>
+ <string notr="true">Bridger</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">Brixified</string>
</property>
</item>
<item>
@@ -456,12 +461,12 @@
</item>
<item>
<property name="text">
- <string notr="true">Bridger</string>
+ <string notr="true">DoubleYou</string>
</property>
</item>
<item>
<property name="text">
- <string notr="true">Brixified</string>
+ <string notr="true">Drew Warwick</string>
</property>
</item>
<item>
@@ -536,12 +541,12 @@
</item>
<item>
<property name="text">
- <string notr="true">z929669</string>
+ <string notr="true">thosrtanner</string>
</property>
</item>
<item>
<property name="text">
- <string notr="true">thosrtanner</string>
+ <string notr="true">z929669</string>
</property>
</item>
</widget>
diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp
index a40338dd..c337ba86 100644
--- a/src/createinstancedialogpages.cpp
+++ b/src/createinstancedialogpages.cpp
@@ -247,7 +247,6 @@ GamePage::GamePage(CreateInstanceDialog& dlg)
fillList();
m_filter.setEdit(ui->gamesFilter);
- m_filter.setUpdateDelay(0);
QObject::connect(&m_filter, &FilterWidget::changed, [&]{ fillList(); });
QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); });
diff --git a/src/datatab.h b/src/datatab.h
index a0f29a5f..32d788f6 100644
--- a/src/datatab.h
+++ b/src/datatab.h
@@ -1,3 +1,6 @@
+#ifndef MODORGANIZER_DATATAB_INCLUDED
+#define MODORGANIZER_DATATAB_INCLUDED
+
#include "modinfodialogfwd.h"
#include "modinfo.h"
#include <filterwidget.h>
@@ -58,3 +61,5 @@ private:
void updateOptions();
void ensureFullyLoaded();
};
+
+#endif // MODORGANIZER_DATATAB_INCLUDED
diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp
index 99347a79..a5c284e6 100644
--- a/src/downloadlist.cpp
+++ b/src/downloadlist.cpp
@@ -41,9 +41,14 @@ void DownloadList::setMetaDisplay(bool metaDisplay)
}
-int DownloadList::rowCount(const QModelIndex&) const
+int DownloadList::rowCount(const QModelIndex& parent) const
{
- return m_Manager->numTotalDownloads() + m_Manager->numPendingDownloads();
+ if (!parent.isValid()) {
+ // root item
+ return m_Manager->numTotalDownloads() + m_Manager->numPendingDownloads();
+ } else {
+ return 0;
+ }
}
@@ -203,3 +208,74 @@ void DownloadList::update(int row)
else
log::error("invalid row {} in download list, update failed", row);
}
+
+bool DownloadList::lessThanPredicate(const QModelIndex &left, const QModelIndex &right)
+{
+ int leftIndex = left.row();
+ int rightIndex = right.row();
+ if ((leftIndex < m_Manager->numTotalDownloads())
+ && (rightIndex < m_Manager->numTotalDownloads())) {
+ if (left.column() == DownloadList::COL_NAME) {
+ return m_Manager->getFileName(left.row()).compare(m_Manager->getFileName(right.row()), Qt::CaseInsensitive) < 0;
+ } else if (left.column() == DownloadList::COL_MODNAME) {
+ QString leftName, rightName;
+
+ if (!m_Manager->isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
+ leftName = info->modName;
+ }
+
+ if (!m_Manager->isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
+ rightName = info->modName;
+ }
+
+ return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
+ } else if (left.column() == DownloadList::COL_VERSION) {
+ MOBase::VersionInfo versionLeft, versionRight;
+
+ if (!m_Manager->isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
+ versionLeft = info->version;
+ }
+
+ if (!m_Manager->isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
+ versionRight = info->version;
+ }
+
+ return versionLeft < versionRight;
+ } else if (left.column() == DownloadList::COL_ID) {
+ int leftID=0, rightID=0;
+
+ if (!m_Manager->isInfoIncomplete(left.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
+ leftID = info->modID;
+ }
+
+ if (!m_Manager->isInfoIncomplete(right.row())) {
+ const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
+ rightID = info->modID;
+ }
+
+ return leftID < rightID;
+ } else if (left.column() == DownloadList::COL_STATUS) {
+ DownloadManager::DownloadState leftState = m_Manager->getState(left.row());
+ DownloadManager::DownloadState rightState = m_Manager->getState(right.row());
+ if (leftState == rightState)
+ return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
+ else
+ return leftState > rightState;
+ } else if (left.column() == DownloadList::COL_SIZE) {
+ return m_Manager->getFileSize(left.row()) < m_Manager->getFileSize(right.row());
+ } else if (left.column() == DownloadList::COL_FILETIME) {
+ return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
+ } else if (left.column() == DownloadList::COL_SOURCEGAME) {
+ return m_Manager->getDisplayGameName(left.row()) < m_Manager->getDisplayGameName(right.row());
+ } else {
+ return leftIndex < rightIndex;
+ }
+ } else {
+ return leftIndex < rightIndex;
+ }
+}
diff --git a/src/downloadlist.h b/src/downloadlist.h
index eb2bbc55..2171c013 100644
--- a/src/downloadlist.h
+++ b/src/downloadlist.h
@@ -85,6 +85,10 @@ public:
**/
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ // used in DownloadsTab as the sorting predicate for the filter widget
+ //
+ bool lessThanPredicate(const QModelIndex &left, const QModelIndex &right);
+
public slots:
/**
diff --git a/src/downloadlistsortproxy.cpp b/src/downloadlistsortproxy.cpp
deleted file mode 100644
index 6209a721..00000000
--- a/src/downloadlistsortproxy.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "downloadlistsortproxy.h"
-#include "downloadlist.h"
-#include "downloadmanager.h"
-#include "settings.h"
-
-DownloadListSortProxy::DownloadListSortProxy(const DownloadManager *manager, QObject *parent)
- : QSortFilterProxyModel(parent), m_Manager(manager), m_CurrentFilter()
-{
-}
-
-void DownloadListSortProxy::updateFilter(const QString &filter)
-{
- m_CurrentFilter = filter;
- invalidateFilter();
-}
-
-
-bool DownloadListSortProxy::lessThan(const QModelIndex &left,
- const QModelIndex &right) const
-{
- int leftIndex = left.row();
- int rightIndex = right.row();
- if ((leftIndex < m_Manager->numTotalDownloads())
- && (rightIndex < m_Manager->numTotalDownloads())) {
- if (left.column() == DownloadList::COL_NAME) {
- return m_Manager->getFileName(left.row()).compare(m_Manager->getFileName(right.row()), Qt::CaseInsensitive) < 0;
- } else if (left.column() == DownloadList::COL_MODNAME) {
- QString leftName, rightName;
-
- if (!m_Manager->isInfoIncomplete(left.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
- leftName = info->modName;
- }
-
- if (!m_Manager->isInfoIncomplete(right.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
- rightName = info->modName;
- }
-
- return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
- } else if (left.column() == DownloadList::COL_VERSION) {
- MOBase::VersionInfo versionLeft, versionRight;
-
- if (!m_Manager->isInfoIncomplete(left.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
- versionLeft = info->version;
- }
-
- if (!m_Manager->isInfoIncomplete(right.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
- versionRight = info->version;
- }
-
- return versionLeft < versionRight;
- } else if (left.column() == DownloadList::COL_ID) {
- int leftID=0, rightID=0;
-
- if (!m_Manager->isInfoIncomplete(left.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(left.row());
- leftID = info->modID;
- }
-
- if (!m_Manager->isInfoIncomplete(right.row())) {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(right.row());
- rightID = info->modID;
- }
-
- return leftID < rightID;
- } else if (left.column() == DownloadList::COL_STATUS) {
- DownloadManager::DownloadState leftState = m_Manager->getState(left.row());
- DownloadManager::DownloadState rightState = m_Manager->getState(right.row());
- if (leftState == rightState)
- return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
- else
- return leftState > rightState;
- } else if (left.column() == DownloadList::COL_SIZE) {
- return m_Manager->getFileSize(left.row()) < m_Manager->getFileSize(right.row());
- } else if (left.column() == DownloadList::COL_FILETIME) {
- return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
- } else if (left.column() == DownloadList::COL_SOURCEGAME) {
- return m_Manager->getDisplayGameName(left.row()) < m_Manager->getDisplayGameName(right.row());
- } else {
- return leftIndex < rightIndex;
- }
- } else {
- return leftIndex < rightIndex;
- }
-}
-
-
-bool DownloadListSortProxy::filterAcceptsRow(int sourceRow, const QModelIndex&) const
-{
- if (m_CurrentFilter.length() == 0) {
- return true;
- } else if (sourceRow < m_Manager->numTotalDownloads()) {
- QString displayedName = Settings::instance().interface().metaDownloads()
- ? m_Manager->getDisplayName(sourceRow)
- : m_Manager->getFileName(sourceRow);
- return displayedName.contains(m_CurrentFilter, Qt::CaseInsensitive);
- } else {
- return false;
- }
-}
diff --git a/src/downloadlistsortproxy.h b/src/downloadlistsortproxy.h
deleted file mode 100644
index 59f46179..00000000
--- a/src/downloadlistsortproxy.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#ifndef DOWNLOADLISTSORTPROXY_H
-#define DOWNLOADLISTSORTPROXY_H
-
-
-#include <QSortFilterProxyModel>
-
-
-class DownloadManager;
-
-
-class DownloadListSortProxy : public QSortFilterProxyModel
-{
- Q_OBJECT
-public:
-
- explicit DownloadListSortProxy(const DownloadManager *manager, QObject *parent = 0);
-
-public slots:
-
- void updateFilter(const QString &filter);
-
-protected:
-
- bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
- bool filterAcceptsRow(int sourceRow, const QModelIndex &source_parent) const;
-
-signals:
-
-public slots:
-
-private:
-
- const DownloadManager *m_Manager;
- QString m_CurrentFilter;
-
-
-};
-
-#endif // DOWNLOADLISTSORTPROXY_H
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp
index 1bb13779..ad9ea63a 100644
--- a/src/downloadlistwidget.cpp
+++ b/src/downloadlistwidget.cpp
@@ -32,9 +32,23 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
-void DownloadProgressDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+DownloadProgressDelegate::DownloadProgressDelegate(
+ DownloadManager* manager, DownloadListWidget* list)
+ : QStyledItemDelegate(list), m_Manager(manager), m_List(list)
{
- QModelIndex sourceIndex = m_SortProxy->mapToSource(index);
+}
+
+void DownloadProgressDelegate::paint(
+ QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ QModelIndex sourceIndex;
+
+ if (auto* proxy=dynamic_cast<QSortFilterProxyModel*>(m_List->model())) {
+ sourceIndex = proxy->mapToSource(index);
+ } else {
+ sourceIndex = index;
+ }
+
bool pendingDownload = (sourceIndex.row() >= m_Manager->numTotalDownloads());
if (sourceIndex.column() == DownloadList::COL_STATUS && !pendingDownload
&& m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_DOWNLOADING) {
diff --git a/src/downloadlistwidget.h b/src/downloadlistwidget.h
index 784bd275..64e1a6e8 100644
--- a/src/downloadlistwidget.h
+++ b/src/downloadlistwidget.h
@@ -22,7 +22,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "downloadmanager.h"
#include "downloadlist.h"
-#include "downloadlistsortproxy.h"
#include <QWidget>
#include <QItemDelegate>
#include <QLabel>
@@ -36,19 +35,21 @@ namespace Ui {
class DownloadListWidget;
}
+class DownloadListWidget;
+
class DownloadProgressDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
- DownloadProgressDelegate(DownloadManager *manager, DownloadListSortProxy *sortProxy, QWidget *parent = 0) : QStyledItemDelegate(parent), m_Manager(manager), m_SortProxy(sortProxy) {}
+ DownloadProgressDelegate(DownloadManager* manager, DownloadListWidget* list);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
private:
- DownloadManager *m_Manager;
- DownloadListSortProxy *m_SortProxy;
+ DownloadManager* m_Manager;
+ DownloadListWidget* m_List;
};
class DownloadListHeader : public QHeaderView
diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp
index 762e3cdb..676b43de 100644
--- a/src/downloadmanager.cpp
+++ b/src/downloadmanager.cpp
@@ -24,11 +24,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "nexusinterface.h"
#include "nxmaccessmanager.h"
#include "iplugingame.h"
+#include "envfs.h"
#include <nxmurl.h>
#include <taskprogressmanager.h>
#include "utility.h"
#include "selectiondialog.h"
#include "bbcode.h"
+#include "shared/util.h"
#include <utility.h>
#include <report.h>
@@ -77,7 +79,9 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const Mo
return info;
}
-DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory)
+DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(
+ const QString &filePath, bool showHidden, const QString outputDirectory,
+ std::optional<uint64_t> fileSize)
{
DownloadInfo *info = new DownloadInfo;
@@ -113,7 +117,7 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(con
info->m_DownloadID = s_NextDownloadID++;
info->m_Output.setFileName(filePath);
- info->m_TotalSize = QFileInfo(filePath).size();
+ info->m_TotalSize = fileSize ? *fileSize : QFileInfo(filePath).size();
info->m_PreResumeSize = info->m_TotalSize;
info->m_CurrentUrl = 0;
info->m_Urls = metaFile.value("url", "").toString().split(";");
@@ -325,13 +329,15 @@ void DownloadManager::refreshList()
}
}
- QStringList supportedExtensions = m_OrganizerCore->installationManager()->getSupportedExtensions();
- QStringList nameFilters(supportedExtensions);
+ const QStringList supportedExtensions = m_OrganizerCore->installationManager()->getSupportedExtensions();
+ std::vector<std::wstring> nameFilters;
for (const auto& extension : supportedExtensions) {
- nameFilters.append("*." + extension);
+ nameFilters.push_back(L"." + extension.toLower().toStdWString());
}
- nameFilters.append(QString("*").append(UNFINISHED));
+ nameFilters.push_back(QString(UNFINISHED).toLower().toStdWString());
+
+
QDir dir(QDir::fromNativeSeparators(m_OutputDirectory));
// find orphaned meta files and delete them (sounds cruel but it's better for everyone)
@@ -348,28 +354,65 @@ void DownloadManager::refreshList()
shellDelete(orphans, true);
}
- // add existing downloads to list
- foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) {
- bool Exists = false;
- for (QVector<DownloadInfo*>::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) {
- if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) {
- Exists = true;
- } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) {
- Exists = true;
- }
- }
- if (Exists) {
- continue;
- }
- QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file;
+ std::set<std::wstring> seen;
- DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory);
- if (info != nullptr) {
- m_ActiveDownloads.push_front(info);
- }
+ struct Context
+ {
+ DownloadManager& self;
+ std::set<std::wstring>& seen;
+ std::vector<std::wstring>& extensions;
+ };
+
+ Context cx = {*this, seen, nameFilters};
+
+ for (auto&& d : m_ActiveDownloads) {
+ cx.seen.insert(d->m_FileName.toLower().toStdWString());
+ cx.seen.insert(QFileInfo(d->m_Output.fileName()).fileName().toLower().toStdWString());
}
+
+ env::forEachEntry(
+ QDir::toNativeSeparators(m_OutputDirectory).toStdWString(), &cx, nullptr, nullptr,
+ [](void* data, std::wstring_view f, FILETIME, uint64_t size) {
+ auto& cx = *static_cast<Context*>(data);
+
+ std::wstring lc = MOShared::ToLowerCopy(f);
+
+ bool interestingExt = false;
+ for (auto&& ext : cx.extensions) {
+ if (lc.ends_with(ext)) {
+ interestingExt = true;
+ break;
+ }
+ }
+
+ if (!interestingExt) {
+ return;
+ }
+
+ if (cx.seen.contains(lc)) {
+ return;
+ }
+
+ QString fileName =
+ QDir::fromNativeSeparators(cx.self.m_OutputDirectory) + "/" +
+ QString::fromWCharArray(f.data(), f.size());
+
+ DownloadInfo *info = DownloadInfo::createFromMeta(
+ fileName, cx.self.m_ShowHidden, cx.self.m_OutputDirectory, size);
+
+ if (info == nullptr) {
+ return;
+ }
+
+ cx.self.m_ActiveDownloads.push_front(info);
+ cx.seen.insert(std::move(lc));
+ cx.seen.insert(QFileInfo(info->m_Output.fileName()).fileName().toLower().toStdWString());
+ });
+
+ log::debug("saw {} downloads", m_ActiveDownloads.size());
+
emit update(-1);
//let watcher trigger refreshes again
@@ -1066,7 +1109,7 @@ void DownloadManager::visitOnNexus(int index)
QString gameName = info->m_FileInfo->gameName;
if (modID > 0) {
- QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName)));
+ shell::Open(QUrl(m_NexusInterface->getModURL(modID, gameName)));
}
else {
emit showMessage(tr("Nexus ID for this Mod is unknown"));
diff --git a/src/downloadmanager.h b/src/downloadmanager.h
index 953ab88b..5e126f4b 100644
--- a/src/downloadmanager.h
+++ b/src/downloadmanager.h
@@ -104,7 +104,9 @@ private:
bool m_Hidden;
static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs);
- static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory);
+ static DownloadInfo *createFromMeta(
+ const QString &filePath, bool showHidden, const QString outputDirectory,
+ std::optional<uint64_t> fileSize={});
/**
* @brief rename the file
diff --git a/src/downloadstab.cpp b/src/downloadstab.cpp
new file mode 100644
index 00000000..a0602ede
--- /dev/null
+++ b/src/downloadstab.cpp
@@ -0,0 +1,77 @@
+#include "downloadstab.h"
+#include "downloadlist.h"
+#include "downloadlistwidget.h"
+#include "organizercore.h"
+#include "ui_mainwindow.h"
+
+DownloadsTab::DownloadsTab(OrganizerCore& core, Ui::MainWindow* mwui)
+ : m_core(core), ui{
+ mwui->btnRefreshDownloads, mwui->downloadView, mwui->showHiddenBox,
+ mwui->downloadFilterEdit}
+{
+ DownloadList *sourceModel = new DownloadList(
+ m_core.downloadManager(), ui.list);
+
+ ui.list->setModel(sourceModel);
+ ui.list->setManager(m_core.downloadManager());
+ ui.list->setItemDelegate(new DownloadProgressDelegate(
+ m_core.downloadManager(), ui.list));
+
+ update();
+
+ m_filter.setEdit(ui.filter);
+ m_filter.setList(ui.list);
+ m_filter.setSortPredicate([sourceModel](auto&& left, auto&& right) {
+ return sourceModel->lessThanPredicate(left, right);
+ });
+
+ connect(ui.refresh, &QPushButton::clicked, [&]{ refresh(); });
+ connect(ui.list, SIGNAL(installDownload(int)), &m_core, SLOT(installDownload(int)));
+ connect(ui.list, SIGNAL(queryInfo(int)), m_core.downloadManager(), SLOT(queryInfo(int)));
+ connect(ui.list, SIGNAL(queryInfoMd5(int)), m_core.downloadManager(), SLOT(queryInfoMd5(int)));
+ connect(ui.list, SIGNAL(visitOnNexus(int)), m_core.downloadManager(), SLOT(visitOnNexus(int)));
+ connect(ui.list, SIGNAL(openFile(int)), m_core.downloadManager(), SLOT(openFile(int)));
+ connect(ui.list, SIGNAL(openMetaFile(int)), m_core.downloadManager(), SLOT(openMetaFile(int)));
+ connect(ui.list, SIGNAL(openInDownloadsFolder(int)), m_core.downloadManager(), SLOT(openInDownloadsFolder(int)));
+ connect(ui.list, SIGNAL(removeDownload(int, bool)), m_core.downloadManager(), SLOT(removeDownload(int, bool)));
+ connect(ui.list, SIGNAL(restoreDownload(int)), m_core.downloadManager(), SLOT(restoreDownload(int)));
+ connect(ui.list, SIGNAL(cancelDownload(int)), m_core.downloadManager(), SLOT(cancelDownload(int)));
+ connect(ui.list, SIGNAL(pauseDownload(int)), m_core.downloadManager(), SLOT(pauseDownload(int)));
+ connect(ui.list, &DownloadListWidget::resumeDownload, [&](int i){ resumeDownload(i); });
+}
+
+void DownloadsTab::update()
+{
+ // this means downloadTab initialization hasn't happened yet
+ if (ui.list->model() == nullptr) {
+ return;
+ }
+
+ // set the view attribute and default row sizes
+ if (m_core.settings().interface().compactDownloads()) {
+ ui.list->setProperty("downloadView", "compact");
+ ui.list->setStyleSheet("DownloadListWidget::item { padding: 4px 2px; }");
+ } else {
+ ui.list->setProperty("downloadView", "standard");
+ ui.list->setStyleSheet("DownloadListWidget::item { padding: 16px 4px; }");
+ }
+
+ ui.list->setMetaDisplay(m_core.settings().interface().metaDownloads());
+ ui.list->style()->unpolish(ui.list);
+ ui.list->style()->polish(ui.list);
+ qobject_cast<DownloadListHeader*>(ui.list->header())->customResizeSections();
+
+ m_core.downloadManager()->refreshList();
+}
+
+void DownloadsTab::refresh()
+{
+ m_core.downloadManager()->refreshList();
+}
+
+void DownloadsTab::resumeDownload(int downloadIndex)
+{
+ m_core.loggedInAction(ui.list, [this, downloadIndex] {
+ m_core.downloadManager()->resumeDownload(downloadIndex);
+ });
+}
diff --git a/src/downloadstab.h b/src/downloadstab.h
new file mode 100644
index 00000000..ac0cf0e2
--- /dev/null
+++ b/src/downloadstab.h
@@ -0,0 +1,36 @@
+#ifndef MODORGANIZER_DOWNLOADTAB_INCLUDED
+#define MODORGANIZER_DOWNLOADTAB_INCLUDED
+
+#include <filterwidget.h>
+
+namespace Ui { class MainWindow; }
+class OrganizerCore;
+class DownloadListWidget;
+
+class DownloadsTab : public QObject
+{
+ Q_OBJECT;
+
+public:
+ DownloadsTab(OrganizerCore& core, Ui::MainWindow* ui);
+
+ void update();
+
+private:
+ struct DownloadsTabUi
+ {
+ QPushButton* refresh;
+ DownloadListWidget* list;
+ QCheckBox* showHidden;
+ QLineEdit* filter;
+ };
+
+ OrganizerCore& m_core;
+ DownloadsTabUi ui;
+ MOBase::FilterWidget m_filter;
+
+ void refresh();
+ void resumeDownload(int downloadIndex);
+};
+
+#endif // MODORGANIZER_DOWNLOADTAB_INCLUDED
diff --git a/src/editexecutablesdialog.cpp b/src/editexecutablesdialog.cpp
index cd11d5fb..6e3cecff 100644
--- a/src/editexecutablesdialog.cpp
+++ b/src/editexecutablesdialog.cpp
@@ -71,6 +71,9 @@ EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, int sel, QWidget
loadCustomOverwrites();
loadForcedLibraries();
+
+ QStringList modNames;
+
for (auto&& m : m_organizerCore.modList()->allMods()) {
auto mod = ModInfo::getByName(m);
if (!mod->hasAnyOfTheseFlags({ ModInfo::FLAG_FOREIGN,
@@ -78,9 +81,16 @@ EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, int sel, QWidget
ModInfo::FLAG_OVERWRITE,
ModInfo::FLAG_SEPARATOR })) {
ui->mods->addItem(m);
+ modNames.push_back(m);
}
}
+ auto* c = new QCompleter(modNames);
+ c->setCaseSensitivity(Qt::CaseInsensitive);
+ c->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
+ ui->mods->setCompleter(c);
+
+
fillList();
setDirty(false);
@@ -170,9 +180,51 @@ EditExecutablesDialog::getForcedLibraries() const
return m_forcedLibraries;
}
-void EditExecutablesDialog::commitChanges()
+bool EditExecutablesDialog::checkOutputMods(const ExecutablesList& exes)
+{
+ // make sure the output mods for exes exist since the combobox is editable
+ //
+ // it'd be convenient for users to automatically create mods here if they're
+ // not found, but this is a can of worms: it would require a refresh
+ // because getIndex() still won't find it after calling
+ // OrganizerCore::createMod(), which is a problem if the user just clicked
+ // Apply and continued doing things
+ //
+ // triggering a refresh while this dialog is up doesn't sound like a very
+ // smart thing to do for now, so this just shows an error
+
+ for (const auto& e : exes) {
+ auto modName = m_customOverwrites.find(e.title());
+
+ if (modName && modName->enabled) {
+ if (modName->value.isEmpty()) {
+ QMessageBox::critical(
+ this, tr("Empty output mod"),
+ tr("The output mod for %2 is empty.").arg(e.title()));
+
+ return false;
+ } else if (ModInfo::getIndex(modName->value) == UINT_MAX) {
+ QMessageBox::critical(
+ this, tr("Output mod not found"),
+ tr("The output mod '%1' for %2 does not exist.")
+ .arg(modName->value).arg(e.title()));
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool EditExecutablesDialog::commitChanges()
{
const auto newExecutables = getExecutablesList();
+
+ if (!checkOutputMods(newExecutables)) {
+ return false;
+ }
+
auto* profile = m_organizerCore.currentProfile();
// remove all the custom overwrites and forced libraries
@@ -201,6 +253,8 @@ void EditExecutablesDialog::commitChanges()
m_organizerCore.setExecutablesList(newExecutables);
setDirty(false);
+
+ return true;
}
void EditExecutablesDialog::setDirty(bool b)
@@ -651,6 +705,11 @@ void EditExecutablesDialog::on_createFilesInMod_toggled(bool checked)
ui->mods->setEnabled(checked);
save();
+
+ if (checked) {
+ ui->mods->lineEdit()->selectAll();
+ ui->mods->setFocus();
+ }
}
void EditExecutablesDialog::on_forceLoadLibraries_toggled(bool checked)
@@ -776,8 +835,9 @@ void EditExecutablesDialog::on_configureLibraries_clicked()
void EditExecutablesDialog::on_buttons_clicked(QAbstractButton* b)
{
if (b == ui->buttons->button(QDialogButtonBox::Ok)) {
- commitChanges();
- accept();
+ if (commitChanges()) {
+ accept();
+ }
} else if (b == ui->buttons->button(QDialogButtonBox::Apply)) {
commitChanges();
} else {
diff --git a/src/editexecutablesdialog.h b/src/editexecutablesdialog.h
index c8be0bf6..8da53e09 100644
--- a/src/editexecutablesdialog.h
+++ b/src/editexecutablesdialog.h
@@ -225,9 +225,10 @@ private:
bool canMove(const QListWidgetItem* item, int direction);
void move(QListWidgetItem* item, int direction);
bool isTitleConflicting(const QString& s);
- void commitChanges();
+ bool commitChanges();
void setDirty(bool b);
void selectIndex(int i);
+ bool checkOutputMods(const ExecutablesList& exes);
void addFromFile();
void addEmpty();
diff --git a/src/editexecutablesdialog.ui b/src/editexecutablesdialog.ui
index 6f50b3b3..d90fd8d4 100644
--- a/src/editexecutablesdialog.ui
+++ b/src/editexecutablesdialog.ui
@@ -387,6 +387,9 @@ Right now the only case I know of where this needs to be overwritten is for the
<property name="enabled">
<bool>false</bool>
</property>
+ <property name="editable">
+ <bool>true</bool>
+ </property>
</widget>
</item>
</layout>
diff --git a/src/env.cpp b/src/env.cpp
index 98d2e6ea..76473dfb 100644
--- a/src/env.cpp
+++ b/src/env.cpp
@@ -6,6 +6,7 @@
#include "envwindows.h"
#include "envdump.h"
#include "settings.h"
+#include "shared/util.h"
#include <log.h>
#include <utility.h>
@@ -344,7 +345,9 @@ void Environment::dump(const Settings& s) const
log::debug("modules loaded in process:");
for (const auto& m : loadedModules()) {
- log::debug(" . {}", m.toString());
+ if (m.interesting()) {
+ log::debug(" . {}", m.toString());
+ }
}
log::debug("displays:");
@@ -1009,7 +1012,7 @@ bool registryValueExists(const QString& keyName, const QString& valueName)
// returns the filename of the given process or the current one
//
-std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
+std::filesystem::path processPath(HANDLE process=INVALID_HANDLE_VALUE)
{
// double the buffer size 10 times
const int MaxTries = 10;
@@ -1045,7 +1048,7 @@ std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
const std::wstring s(buffer.get(), writtenSize);
const std::filesystem::path path(s);
- return path.filename().native();
+ return path;
}
}
@@ -1062,6 +1065,21 @@ std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
return {};
}
+std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
+{
+ const auto p = processPath(process);
+ if (p.empty()) {
+ return {};
+ }
+
+ return p.filename().native();
+}
+
+std::filesystem::path thisProcessPath()
+{
+ return processPath();
+}
+
DWORD findOtherPid()
{
const std::wstring defaultName = L"ModOrganizer.exe";
@@ -1126,6 +1144,19 @@ std::wstring tempDir()
return std::wstring(buffer, buffer + written);
}
+std::wstring safeVersion()
+{
+ try
+ {
+ // this can throw
+ return MOShared::createVersionInfo().displayString(3).toStdWString() + L"-";
+ }
+ catch(...)
+ {
+ return {};
+ }
+}
+
HandlePtr tempFile(const std::wstring dir)
{
// maximum tries of incrementing the counter
@@ -1139,7 +1170,7 @@ HandlePtr tempFile(const std::wstring dir)
// i can go until MaxTries
std::wostringstream oss;
oss
- << L"ModOrganizer-"
+ << L"ModOrganizer-" << safeVersion()
<< std::setw(4) << (1900 + tm->tm_year)
<< std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1)
<< std::setw(2) << std::setfill(L'0') << tm->tm_mday << "T"
diff --git a/src/env.h b/src/env.h
index e9cb7a36..2152a40d 100644
--- a/src/env.h
+++ b/src/env.h
@@ -309,6 +309,10 @@ bool registryValueExists(const QString& key, const QString& value);
//
void deleteRegistryKeyIfEmpty(const QString& name);
+// returns the path to this process
+//
+std::filesystem::path thisProcessPath();
+
} // namespace env
#endif // ENV_ENV_H
diff --git a/src/envfs.cpp b/src/envfs.cpp
index 20ba4229..9317eb70 100644
--- a/src/envfs.cpp
+++ b/src/envfs.cpp
@@ -298,14 +298,17 @@ void forEachEntryImpl(
ObjectName.MaximumLength = ObjectName.Length;
if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
- dirStartF(cx, toStringView(&oa));
- forEachEntryImpl(cx, hc, buffers, &oa, depth+1, dirStartF, dirEndF, fileF);
- dirEndF(cx, toStringView(&oa));
+ if (dirStartF && dirEndF) {
+ dirStartF(cx, toStringView(&oa));
+ forEachEntryImpl(cx, hc, buffers, &oa, depth+1, dirStartF, dirEndF, fileF);
+ dirEndF(cx, toStringView(&oa));
+ }
} else {
FILETIME ft;
ft.dwLowDateTime = DirInfo->LastWriteTime.LowPart;
ft.dwHighDateTime = DirInfo->LastWriteTime.HighPart;
- fileF(cx, toStringView(&oa), ft);
+
+ fileF(cx, toStringView(&oa), ft, DirInfo->AllocationSize.QuadPart);
}
}
@@ -380,20 +383,20 @@ Directory getFilesAndDirs(const std::wstring& path)
cx->current.pop();
},
- [](void* pcx, std::wstring_view path, FILETIME ft) {
+ [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t s) {
Context* cx = (Context*)pcx;
- cx->current.top()->files.push_back(File(path, ft));
+ cx->current.top()->files.push_back(File(path, ft, s));
}
);
return root;
}
-File::File(std::wstring_view n, FILETIME ft) :
+File::File(std::wstring_view n, FILETIME ft, uint64_t s) :
name(n.begin(), n.end()),
lcname(MOShared::ToLowerCopy(name)),
- lastModified(ft)
+ lastModified(ft), size(s)
{
}
@@ -429,7 +432,11 @@ void getFilesAndDirsWithFindImpl(const std::wstring& path, Directory& d)
getFilesAndDirsWithFindImpl(newPath, d.dirs.back());
}
} else {
- d.files.push_back(File(findData.cFileName, findData.ftLastWriteTime));
+ const auto size =
+ (findData.nFileSizeHigh * (MAXDWORD+1)) + findData.nFileSizeLow;
+
+ d.files.push_back(File(
+ findData.cFileName, findData.ftLastWriteTime, size));
}
result = ::FindNextFileW(searchHandle, &findData);
diff --git a/src/envfs.h b/src/envfs.h
index e4d98d71..62540e18 100644
--- a/src/envfs.h
+++ b/src/envfs.h
@@ -12,8 +12,9 @@ struct File
std::wstring name;
std::wstring lcname;
FILETIME lastModified;
+ uint64_t size;
- File(std::wstring_view name, FILETIME ft);
+ File(std::wstring_view name, FILETIME ft, uint64_t size);
};
struct Directory
@@ -174,7 +175,7 @@ private:
using DirStartF = void (void*, std::wstring_view);
using DirEndF = void (void*, std::wstring_view);
-using FileF = void (void*, std::wstring_view, FILETIME);
+using FileF = void (void*, std::wstring_view, FILETIME, uint64_t);
void setHandleCloserThreadCount(std::size_t n);
diff --git a/src/envmodule.cpp b/src/envmodule.cpp
index 5f2a5f5a..81cbad8a 100644
--- a/src/envmodule.cpp
+++ b/src/envmodule.cpp
@@ -308,6 +308,29 @@ QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds));
}
+bool Module::interesting() const
+{
+ static const auto windir = []() -> QString {
+ try
+ {
+ return QDir::toNativeSeparators(
+ MOBase::getKnownFolder(FOLDERID_Windows).path()) + "\\";
+ }
+ catch(...)
+ {
+ return "c:\\windows\\";
+ }
+ }();
+
+
+ if (m_path.startsWith(windir, Qt::CaseInsensitive)) {
+ return false;
+ }
+
+ return true;
+}
+
+
QString Module::getMD5() const
{
static const std::set<QString> ignore = {
diff --git a/src/envmodule.h b/src/envmodule.h
index c2829b32..4b85d737 100644
--- a/src/envmodule.h
+++ b/src/envmodule.h
@@ -66,6 +66,10 @@ public:
//
QString timestampString() const;
+ // returns false for modules in the Windows root directory
+ //
+ bool interesting() const;
+
// returns a string with all the above information on one line
//
QString toString() const;
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp
index 21470afa..18604bf1 100644
--- a/src/instancemanager.cpp
+++ b/src/instancemanager.cpp
@@ -831,7 +831,7 @@ SetupInstanceResults selectGame(Instance& instance, PluginContainer& pc)
if (dlg.exec() != QDialog::Accepted) {
// cancelled
- return SetupInstanceResults::Exit;
+ return SetupInstanceResults::SelectAnother;
}
// this info will be used instead of the ini, which should fix this
@@ -868,7 +868,7 @@ SetupInstanceResults selectVariant(Instance& instance, PluginContainer& pc)
dlg.raise();
if (dlg.exec() != QDialog::Accepted) {
- return SetupInstanceResults::Exit;
+ return SetupInstanceResults::SelectAnother;
}
// this info will be used instead of the ini, which should fix this
diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp
index 00dc57f7..ecc08c1f 100644
--- a/src/instancemanagerdialog.cpp
+++ b/src/instancemanagerdialog.cpp
@@ -158,7 +158,6 @@ InstanceManagerDialog::InstanceManagerDialog(
m_filter.setEdit(ui->filter);
m_filter.setList(ui->list);
- m_filter.setUpdateDelay(false);
m_filter.setFilteredBorder(false);
updateInstances();
diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp
index 58699d6e..3673c5c6 100644
--- a/src/lootdialog.cpp
+++ b/src/lootdialog.cpp
@@ -56,7 +56,7 @@ bool MarkdownPage::acceptNavigationRequest(const QUrl &url, NavigationType, bool
static const QStringList allowed = {"qrc", "data"};
if (!allowed.contains(url.scheme())) {
- QDesktopServices::openUrl(url);
+ shell::Open(url);
return false;
}
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index a270fd10..cfce9bef 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -49,12 +49,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "categoriesdialog.h"
#include "modinfodialog.h"
#include "overwriteinfodialog.h"
-#include "activatemodsdialog.h"
#include "downloadlist.h"
#include "downloadlistwidget.h"
#include "messagedialog.h"
#include "installationmanager.h"
-#include "downloadlistsortproxy.h"
#include "motddialog.h"
#include "filedialogmemory.h"
#include "tutorialmanager.h"
@@ -76,6 +74,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "statusbar.h"
#include "filterlist.h"
#include "datatab.h"
+#include "downloadstab.h"
+#include "savestab.h"
#include "instancemanagerdialog.h"
#include <utility.h>
#include <dataarchives.h>
@@ -260,7 +260,6 @@ MainWindow::MainWindow(Settings &settings
, m_ContextItem(nullptr)
, m_ContextAction(nullptr)
, m_ContextRow(-1)
- , m_CurrentSaveView(nullptr)
, m_OrganizerCore(organizerCore)
, m_PluginContainer(pluginContainer)
, m_DidUpdateMasterList(false)
@@ -286,6 +285,10 @@ MainWindow::MainWindow(Settings &settings
QWebEngineProfile::defaultProfile()->setCachePath(settings.paths().cache());
QWebEngineProfile::defaultProfile()->setPersistentStoragePath(settings.paths().cache());
+ // qt resets the thread name somewhere within the QWebEngineProfile calls
+ // above
+ MOShared::SetThisThreadName("main");
+
ui->setupUi(this);
languageChange(settings.interface().language());
ui->statusBar->setup(ui, settings);
@@ -347,11 +350,11 @@ MainWindow::MainWindow(Settings &settings
ui->bsaList->setLocalMoveOnly(true);
ui->bsaList->setHeaderHidden(true);
- initDownloadView();
-
const bool pluginListAdjusted =
settings.geometry().restoreState(ui->espList->header());
+
+ // data tab
m_DataTab.reset(new DataTab(m_OrganizerCore, m_PluginContainer, this, ui));
m_DataTab->restoreState(settings);
@@ -365,6 +368,13 @@ MainWindow::MainWindow(Settings &settings
m_DataTab.get(), &DataTab::displayModInformation,
[&](auto&& m, auto&& i, auto&& tab){ displayModInformation(m, i, tab); });
+
+ // downloads tab
+ m_DownloadsTab.reset(new DownloadsTab(m_OrganizerCore, ui));
+
+ // saves tab
+ m_SavesTab.reset(new SavesTab(this, m_OrganizerCore, ui));
+
// Hide stuff we do not need:
IPluginGame const* game = m_OrganizerCore.managedGame();
if (!game->feature<GamePlugins>()) {
@@ -394,9 +404,6 @@ MainWindow::MainWindow(Settings &settings
ui->openFolderMenu->setMenu(openFolderMenu());
- ui->savegameList->installEventFilter(this);
- ui->savegameList->setMouseTracking(true);
-
// don't allow mouse wheel to switch grouping, too many people accidentally
// turn on grouping and then don't understand what happened
EventFilter *noWheel
@@ -414,8 +421,6 @@ MainWindow::MainWindow(Settings &settings
connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, SLOT(scheduleCheckForProblems()));
- connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*)));
-
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)));
@@ -430,11 +435,6 @@ MainWindow::MainWindow(Settings &settings
this, &MainWindow::refresherProgress);
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString)));
- m_SavesWatcherTimer.setSingleShot(true);
- m_SavesWatcherTimer.setInterval(500);
- connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [this]() { m_SavesWatcherTimer.start(); });
- connect(&m_SavesWatcherTimer, &QTimer::timeout, this, &MainWindow::refreshSavesIfOpen);
-
connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString)));
connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString)));
@@ -488,8 +488,6 @@ MainWindow::MainWindow(Settings &settings
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated()));
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated()));
- new QShortcut(QKeySequence::Refresh, this, SLOT(refreshProfile_activated()));
-
setFilterShortcuts(ui->modList, ui->modFilterEdit);
setFilterShortcuts(ui->espList, ui->espFilterEdit);
setFilterShortcuts(ui->downloadView, ui->downloadFilterEdit);
@@ -718,7 +716,6 @@ MainWindow::~MainWindow()
}
}
-
void MainWindow::updateWindowTitle(const APIUserAccount& user)
{
//"\xe2\x80\x93" is an "em dash", a longer "-"
@@ -1265,15 +1262,6 @@ void MainWindow::espFilterChanged(const QString &filter)
updatePluginCount();
}
-void MainWindow::downloadFilterChanged(const QString &filter)
-{
- if (!filter.isEmpty()) {
- ui->downloadView->setStyleSheet("QTreeView { border: 2px ridge #f00; }");
- } else {
- ui->downloadView->setStyleSheet("");
- }
-}
-
void MainWindow::expandModList(const QModelIndex &index)
{
QAbstractItemModel *model = ui->modList->model();
@@ -1512,79 +1500,9 @@ void MainWindow::cleanup()
m_MetaSave.waitForFinished();
}
-void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
-{
- // don't display the widget if the main window doesn't have focus
- //
- // this goes against the standard behaviour for tooltips, which are displayed
- // on hover regardless of focus, but this widget is so large and busy that
- // it's probably better this way
- if (!isActiveWindow()){
- return;
- }
-
- if (m_CurrentSaveView == nullptr) {
- IPluginGame const *game = m_OrganizerCore.managedGame();
- SaveGameInfo const *info = game->feature<SaveGameInfo>();
- if (info != nullptr) {
- m_CurrentSaveView = info->getSaveGameWidget(this);
- }
- if (m_CurrentSaveView == nullptr) {
- return;
- }
- }
- m_CurrentSaveView->setSave(*m_SaveGames[ui->savegameList->row(newItem)]);
-
- QWindow *window = m_CurrentSaveView->window()->windowHandle();
- QRect screenRect;
- if (window == nullptr)
- screenRect = QGuiApplication::primaryScreen()->geometry();
- else
- screenRect = window->screen()->geometry();
-
- QPoint pos = QCursor::pos();
- if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
- pos.rx() -= (m_CurrentSaveView->width() + 2);
- } else {
- pos.rx() += 5;
- }
-
- if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
- pos.ry() -= (m_CurrentSaveView->height() + 10);
- } else {
- pos.ry() += 20;
- }
- m_CurrentSaveView->move(pos);
-
- m_CurrentSaveView->show();
- m_CurrentSaveView->setProperty("displayItem", QVariant::fromValue(static_cast<void *>(newItem)));
-}
-
-
-void MainWindow::saveSelectionChanged(QListWidgetItem *newItem)
-{
- if (newItem == nullptr) {
- hideSaveGameInfo();
- } else if (m_CurrentSaveView == nullptr || newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
- displaySaveGameInfo(newItem);
- }
-}
-
-
-void MainWindow::hideSaveGameInfo()
-{
- if (m_CurrentSaveView != nullptr) {
- m_CurrentSaveView->deleteLater();
- m_CurrentSaveView = nullptr;
- }
-}
-
bool MainWindow::eventFilter(QObject *object, QEvent *event)
{
- if ((object == ui->savegameList) &&
- ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) {
- hideSaveGameInfo();
- } else if (event->type() == QEvent::StatusTip && object != this) {
+ if (event->type() == QEvent::StatusTip && object != this) {
QMainWindow::event(event);
return true;
}
@@ -1682,7 +1600,7 @@ void MainWindow::registerModPage(IPluginModPage *modPage)
m_IntegratedBrowser->openUrl(modPage->pageURL());
}
else {
- QDesktopServices::openUrl(QUrl(modPage->pageURL()));
+ shell::Open(QUrl(modPage->pageURL()));
}
}, Qt::QueuedConnection);
@@ -1759,7 +1677,7 @@ void MainWindow::activateSelectedProfile()
m_ModListSortProxy->setProfile(m_OrganizerCore.currentProfile());
- refreshSaveList();
+ m_SavesTab->refreshSaveList();
m_OrganizerCore.refresh();
updateModCount();
updatePluginCount();
@@ -1795,9 +1713,22 @@ void MainWindow::on_profileBox_currentIndexChanged(int index)
if (ui->profileBox->currentIndex() == 0) {
ui->profileBox->setCurrentIndex(previousIndex);
- ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore, this).exec();
+
+ std::optional<QString> newSelection;
+
+ ProfilesDialog dlg(ui->profileBox->currentText(), m_OrganizerCore, this);
+ dlg.exec();
+ newSelection = dlg.selectedProfile();
+
while (!refreshProfiles()) {
- ProfilesDialog(ui->profileBox->currentText(), m_OrganizerCore, this).exec();
+ ProfilesDialog dlg(ui->profileBox->currentText(), m_OrganizerCore, this);
+ dlg.exec();
+ newSelection = dlg.selectedProfile();
+ }
+
+ if (newSelection) {
+ ui->profileBox->setCurrentText(*newSelection);
+ activateSelectedProfile();
}
} else {
activateSelectedProfile();
@@ -1805,14 +1736,16 @@ void MainWindow::on_profileBox_currentIndexChanged(int index)
LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature<LocalSavegames>();
if (saveGames != nullptr) {
- if (saveGames->prepareProfile(m_OrganizerCore.currentProfile()))
- refreshSaveList();
+ if (saveGames->prepareProfile(m_OrganizerCore.currentProfile())) {
+ m_SavesTab->refreshSaveList();
+ }
}
BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
if (invalidation != nullptr) {
- if (invalidation->prepareProfile(m_OrganizerCore.currentProfile()))
+ if (invalidation->prepareProfile(m_OrganizerCore.currentProfile())) {
QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh()));
+ }
}
}
}
@@ -1898,78 +1831,6 @@ void MainWindow::refreshExecutablesList()
ui->executablesListBox->setEnabled(true);
}
-void MainWindow::refreshSavesIfOpen()
-{
- if (ui->tabWidget->currentWidget() == ui->savesTab) {
- refreshSaveList();
- }
-}
-
-QDir MainWindow::currentSavesDir() const
-{
- QDir savesDir;
- if (m_OrganizerCore.currentProfile()->localSavesEnabled()) {
- savesDir.setPath(m_OrganizerCore.currentProfile()->savePath());
- } else {
- auto iniFiles = m_OrganizerCore.managedGame()->iniFiles();
-
- if (iniFiles.isEmpty()) {
- return m_OrganizerCore.managedGame()->savesDirectory();
- }
-
- QString iniPath = m_OrganizerCore.currentProfile()->absoluteIniFilePath(iniFiles[0]);
-
- wchar_t path[MAX_PATH];
- if (::GetPrivateProfileStringW(
- L"General", L"SLocalSavePath", L"",
- path, MAX_PATH,
- iniPath.toStdWString().c_str()
- )) {
- savesDir.setPath(m_OrganizerCore.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path)));
- }
- else {
- savesDir = m_OrganizerCore.managedGame()->savesDirectory();
- }
- }
-
- return savesDir;
-}
-
-void MainWindow::startMonitorSaves()
-{
- stopMonitorSaves();
-
- QDir savesDir = currentSavesDir();
-
- m_SavesWatcher.addPath(savesDir.absolutePath());
-}
-
-void MainWindow::stopMonitorSaves()
-{
- if (m_SavesWatcher.directories().length() > 0) {
- m_SavesWatcher.removePaths(m_SavesWatcher.directories());
- }
-}
-
-void MainWindow::refreshSaveList()
-{
- TimeThis tt("MainWindow::refreshSaveList()");
-
- startMonitorSaves(); // re-starts monitoring
-
- QDir savesDir = currentSavesDir();
- MOBase::log::debug("reading save games from {}", savesDir.absolutePath());
- m_SaveGames = m_OrganizerCore.managedGame()->listSaves(savesDir);
- std::sort(m_SaveGames.begin(), m_SaveGames.end(), [](auto const& lhs, auto const& rhs) {
- return lhs->getCreationTime() > rhs->getCreationTime();
- });
-
- ui->savegameList->clear();
- for (auto& save: m_SaveGames) {
- ui->savegameList->addItem(savesDir.relativeFilePath(save->getFilepath()));
- }
-}
-
static bool BySortValue(const std::pair<UINT32, QTreeWidgetItem*> &LHS, const std::pair<UINT32, QTreeWidgetItem*> &RHS)
{
return LHS.first < RHS.first;
@@ -2310,11 +2171,6 @@ QMainWindow* MainWindow::mainWindow()
return this;
}
-void MainWindow::on_btnRefreshDownloads_clicked()
-{
- m_OrganizerCore.downloadManager()->refreshList();
-}
-
void MainWindow::on_tabWidget_currentChanged(int index)
{
QWidget* currentWidget = ui->tabWidget->widget(index);
@@ -2325,7 +2181,7 @@ void MainWindow::on_tabWidget_currentChanged(int index)
} else if (currentWidget == ui->dataTab) {
m_DataTab->activated();
} else if (currentWidget == ui->savesTab) {
- refreshSaveList();
+ m_SavesTab->refreshSaveList();
}
}
@@ -2417,17 +2273,17 @@ void MainWindow::helpTriggered()
void MainWindow::wikiTriggered()
{
- QDesktopServices::openUrl(QUrl("https://modorganizer2.github.io/"));
+ shell::Open(QUrl("https://modorganizer2.github.io/"));
}
void MainWindow::discordTriggered()
{
- QDesktopServices::openUrl(QUrl("https://discord.gg/cYwdcxj"));
+ shell::Open(QUrl("https://discord.gg/cYwdcxj"));
}
void MainWindow::issueTriggered()
{
- QDesktopServices::openUrl(QUrl("https://github.com/Modorganizer2/modorganizer/issues"));
+ shell::Open(QUrl("https://github.com/Modorganizer2/modorganizer/issues"));
}
void MainWindow::tutorialTriggered()
@@ -2448,6 +2304,11 @@ void MainWindow::on_actionInstallMod_triggered()
installMod();
}
+void MainWindow::on_action_Refresh_triggered()
+{
+ refreshProfile_activated();
+}
+
void MainWindow::on_actionAdd_Profile_triggered()
{
for (;;) {
@@ -2457,9 +2318,9 @@ void MainWindow::on_actionAdd_Profile_triggered()
// workaround: need to disable monitoring of the saves directory, otherwise the active
// profile directory is locked
- stopMonitorSaves();
+ m_SavesTab->stopMonitorSaves();
profilesDialog.exec();
- refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
+ m_SavesTab->refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
if (refreshProfiles() && !profilesDialog.failed()) {
break;
@@ -2468,14 +2329,16 @@ void MainWindow::on_actionAdd_Profile_triggered()
LocalSavegames *saveGames = m_OrganizerCore.managedGame()->feature<LocalSavegames>();
if (saveGames != nullptr) {
- if (saveGames->prepareProfile(m_OrganizerCore.currentProfile()))
- refreshSaveList();
+ if (saveGames->prepareProfile(m_OrganizerCore.currentProfile())) {
+ m_SavesTab->refreshSaveList();
+ }
}
BSAInvalidation *invalidation = m_OrganizerCore.managedGame()->feature<BSAInvalidation>();
if (invalidation != nullptr) {
- if (invalidation->prepareProfile(m_OrganizerCore.currentProfile()))
+ if (invalidation->prepareProfile(m_OrganizerCore.currentProfile())) {
QTimer::singleShot(5, &m_OrganizerCore, SLOT(profileRefresh()));
+ }
}
}
@@ -2703,7 +2566,9 @@ void MainWindow::restoreBackup_clicked()
if (!modDir.rename(modInfo->absolutePath(), destinationPath)) {
reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath));
}
+
m_OrganizerCore.refresh();
+ updateModCount();
}
}
}
@@ -2853,14 +2718,9 @@ void MainWindow::backupMod_clicked()
QMessageBox::information(this, tr("Failed"),
tr("Failed to create backup."));
}
- m_OrganizerCore.refresh();
-}
-void MainWindow::resumeDownload(int downloadIndex)
-{
- m_OrganizerCore.loggedInAction(this, [this, downloadIndex] {
- m_OrganizerCore.downloadManager()->resumeDownload(downloadIndex);
- });
+ m_OrganizerCore.refresh();
+ updateModCount();
}
@@ -3134,41 +2994,51 @@ void MainWindow::displayModInformation(int row, ModInfoTabIDs tabID)
void MainWindow::ignoreMissingData_clicked()
{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
+ 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);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
+ changed.push_back(info);
+ }
- emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1));
+ 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);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
-
- emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1));
+ m_OrganizerCore.modList()->notifyChange(m_ContextRow);
}
}
void MainWindow::markConverted_clicked()
{
- QItemSelectionModel *selection = ui->modList->selectionModel();
- if (selection->hasSelection() && selection->selectedRows().count() > 1) {
- for (QModelIndex idx : selection->selectedRows()) {
+ 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);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
- emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1));
+ 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);
- connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex)));
- emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1));
+ m_OrganizerCore.modList()->notifyChange(m_ContextRow);
}
}
@@ -3468,6 +3338,8 @@ void MainWindow::refreshProfile_activated()
void MainWindow::updateModCount()
{
+ TimeThis tt("updateModCount");
+
int activeCount = 0;
int visActiveCount = 0;
int backupCount = 0;
@@ -4925,100 +4797,6 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos)
}
}
-
-void MainWindow::deleteSavegame_clicked()
-{
- SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
-
- QString savesMsgLabel;
- QStringList deleteFiles;
-
- int count = 0;
-
- for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) {
-
- auto& saveGame = m_SaveGames[idx.row()];
-
- if (count < 10) {
- savesMsgLabel += "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
- }
- ++count;
-
- deleteFiles += saveGame->allFiles();
- }
-
- if (count > 10) {
- savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
- }
-
- if (QMessageBox::question(this, tr("Confirm"),
- tr("Are you sure you want to remove the following %n save(s)?<br>"
- "<ul>%1</ul><br>"
- "Removed saves will be sent to the Recycle Bin.", "", count)
- .arg(savesMsgLabel),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- shellDelete(deleteFiles, true); // recycle bin delete.
- refreshSaveList();
- }
-}
-
-
-void MainWindow::fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets)
-{
- ActivateModsDialog dialog(missingAssets, this);
- if (dialog.exec() == QDialog::Accepted) {
- // activate the required mods, then enable all esps
- std::set<QString> modsToActivate = dialog.getModsToActivate();
- for (std::set<QString>::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) {
- if ((*iter != "<data>") && (*iter != "<overwrite>")) {
- unsigned int modIndex = ModInfo::getIndex(*iter);
- m_OrganizerCore.currentProfile()->setModEnabled(modIndex, true);
- }
- }
-
- m_OrganizerCore.currentProfile()->writeModlist();
- m_OrganizerCore.refreshLists();
-
- std::set<QString> espsToActivate = dialog.getESPsToActivate();
- for (std::set<QString>::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) {
- m_OrganizerCore.pluginList()->enableESP(*iter);
- }
- m_OrganizerCore.saveCurrentLists();
- }
-}
-
-
-void MainWindow::on_savegameList_customContextMenuRequested(const QPoint& pos)
-{
- QItemSelectionModel* selection = ui->savegameList->selectionModel();
-
- if (!selection->hasSelection()) {
- return;
- }
-
- QMenu menu;
-
- SaveGameInfo const* info = this->m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
- if (info != nullptr) {
- QAction* action = menu.addAction(tr("Enable Mods..."));
- action->setEnabled(false);
- if (selection->selectedIndexes().count() == 1) {
- auto& save = m_SaveGames[selection->selectedIndexes()[0].row()];
- SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save);
- if (missing.size() != 0) {
- connect(action, &QAction::triggered, this, [this, missing] { fixMods_clicked(missing); });
- action->setEnabled(true);
- }
- }
- }
-
- QString deleteMenuLabel = tr("Delete %n save(s)", "", selection->selectedIndexes().count());
-
- menu.addAction(deleteMenuLabel, this, SLOT(deleteSavegame_clicked()));
-
- menu.exec(ui->savegameList->viewport()->mapToGlobal(pos));
-}
-
void MainWindow::linkToolbar()
{
Executable* exe = getSelectedExecutable();
@@ -5161,7 +4939,7 @@ void MainWindow::on_actionSettings_triggered()
}
ui->statusBar->checkSettings(m_OrganizerCore.settings());
- updateDownloadView();
+ m_DownloadsTab->update();
m_OrganizerCore.setLogLevel(settings.diagnostics().logLevel());
@@ -5184,7 +4962,7 @@ void MainWindow::onPluginRegistrationChanged()
{
updateModPageMenu();
scheduleCheckForProblems();
- updateDownloadView();
+ m_DownloadsTab->update();
}
void MainWindow::on_actionNexus_triggered()
@@ -5193,13 +4971,13 @@ void MainWindow::on_actionNexus_triggered()
QString gameName = game->gameShortName();
if (game->gameNexusName().isEmpty() && game->primarySources().count())
gameName = game->primarySources()[0];
- QDesktopServices::openUrl(QUrl(NexusInterface::instance().getGameURL(gameName)));
+ shell::Open(QUrl(NexusInterface::instance().getGameURL(gameName)));
}
void MainWindow::linkClicked(const QString &url)
{
- QDesktopServices::openUrl(QUrl(url));
+ shell::Open(QUrl(url));
}
@@ -5240,7 +5018,9 @@ void MainWindow::languageChange(const QString &newLanguage)
createHelpMenu();
- updateDownloadView();
+ if (m_DownloadsTab) {
+ m_DownloadsTab->update();
+ }
QMenu *listOptionsMenu = new QMenu(ui->listOptionsBtn);
initModListContextMenu(listOptionsMenu);
@@ -5375,63 +5155,12 @@ void MainWindow::actionWontEndorseMO()
}
}
-void MainWindow::initDownloadView()
-{
- DownloadList *sourceModel = new DownloadList(m_OrganizerCore.downloadManager(), ui->downloadView);
- DownloadListSortProxy *sortProxy = new DownloadListSortProxy(m_OrganizerCore.downloadManager(), ui->downloadView);
- sortProxy->setSourceModel(sourceModel);
- connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString)));
- connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(downloadFilterChanged(QString)));
-
- ui->downloadView->setSourceModel(sourceModel);
- ui->downloadView->setModel(sortProxy);
- ui->downloadView->setManager(m_OrganizerCore.downloadManager());
- ui->downloadView->setItemDelegate(new DownloadProgressDelegate(m_OrganizerCore.downloadManager(), sortProxy, ui->downloadView));
- updateDownloadView();
-
- connect(ui->downloadView, SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int)));
- connect(ui->downloadView, SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int)));
- connect(ui->downloadView, SIGNAL(queryInfoMd5(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfoMd5(int)));
- connect(ui->downloadView, SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int)));
- connect(ui->downloadView, SIGNAL(openFile(int)), m_OrganizerCore.downloadManager(), SLOT(openFile(int)));
- connect(ui->downloadView, SIGNAL(openMetaFile(int)), m_OrganizerCore.downloadManager(), SLOT(openMetaFile(int)));
- connect(ui->downloadView, SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int)));
- connect(ui->downloadView, SIGNAL(removeDownload(int, bool)), m_OrganizerCore.downloadManager(), SLOT(removeDownload(int, bool)));
- connect(ui->downloadView, SIGNAL(restoreDownload(int)), m_OrganizerCore.downloadManager(), SLOT(restoreDownload(int)));
- connect(ui->downloadView, SIGNAL(cancelDownload(int)), m_OrganizerCore.downloadManager(), SLOT(cancelDownload(int)));
- connect(ui->downloadView, SIGNAL(pauseDownload(int)), m_OrganizerCore.downloadManager(), SLOT(pauseDownload(int)));
- connect(ui->downloadView, SIGNAL(resumeDownload(int)), this, SLOT(resumeDownload(int)));
-}
-
-void MainWindow::updateDownloadView()
+void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs)
{
- // this means downlaodTab initialization hasnt happened yet
- if (ui->downloadView->model() == nullptr) {
+ if (m_OrganizerCore.settings().network().offlineMode()) {
return;
}
- // set the view attribute and default row sizes
- if (m_OrganizerCore.settings().interface().compactDownloads()) {
- ui->downloadView->setProperty("downloadView", "compact");
- setStyleSheet("DownloadListWidget::item { padding: 4px 2px; }");
- } else {
- ui->downloadView->setProperty("downloadView", "standard");
- setStyleSheet("DownloadListWidget::item { padding: 16px 4px; }");
- }
- //setStyleSheet("DownloadListWidget::item:hover { padding: 0px; }");
- //setStyleSheet("DownloadListWidget::item:selected { padding: 0px; }");
-
- // reapply global stylesheet on the widget level (!) to override the defaults
- //ui->downloadView->setStyleSheet(styleSheet());
- ui->downloadView->setMetaDisplay(m_OrganizerCore.settings().interface().metaDownloads());
- ui->downloadView->style()->unpolish(ui->downloadView);
- ui->downloadView->style()->polish(ui->downloadView);
- qobject_cast<DownloadListHeader*>(ui->downloadView->header())->customResizeSections();
- m_OrganizerCore.downloadManager()->refreshList();
-}
-
-void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs)
-{
if (NexusInterface::instance().getAccessManager()->validated()) {
ModInfo::manualUpdateCheck(this, IDs);
} else {
@@ -6246,10 +5975,22 @@ void MainWindow::on_showHiddenBox_toggled(bool checked)
void MainWindow::on_bossButton_clicked()
{
- const auto r = QMessageBox::question(
- this, tr("Sorting plugins"),
- tr("Are you sure you want to sort your plugins list?"),
- QMessageBox::Yes | QMessageBox::No);
+ 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;
@@ -6261,8 +6002,15 @@ void MainWindow::on_bossButton_clicked()
setEnabled(false);
ON_BLOCK_EXIT([&] () { setEnabled(true); });
- if (runLoot(this, m_OrganizerCore, m_DidUpdateMasterList)) {
- m_DidUpdateMasterList = 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();
}
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 7a1c683d..f8910361 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -27,7 +27,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "iuserinterface.h"
#include "modinfo.h"
#include "modlistsortproxy.h"
-#include "savegameinfo.h"
#include "tutorialcontrol.h"
#include "plugincontainer.h" //class PluginContainer;
#include "iplugingame.h" //namespace MOBase { class IPluginGame; }
@@ -39,6 +38,8 @@ class CategoryFactory;
class OrganizerCore;
class FilterList;
class DataTab;
+class DownloadsTab;
+class SavesTab;
class BrowserDialog;
class PluginListSortProxy;
@@ -46,7 +47,6 @@ namespace BSA { class Archive; }
namespace MOBase { class IPluginModPage; }
namespace MOBase { class IPluginTool; }
-namespace MOBase { class ISaveGame; }
namespace MOShared { class DirectoryEntry; }
@@ -122,7 +122,6 @@ public:
bool addProfile();
void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives);
- void refreshSaveList();
void setModListSorting(int index);
void setESPListSorting(int index);
@@ -234,9 +233,6 @@ private:
bool populateMenuCategories(QMenu *menu, int targetID);
- void initDownloadView();
- void updateDownloadView();
-
// remove invalid category-references from mods
void fixCategories();
@@ -247,8 +243,6 @@ private:
void setCategoryListVisible(bool visible);
- void displaySaveGameInfo(QListWidgetItem *newItem);
-
bool errorReported(QString &logFile);
void updateESPLock(bool locked);
@@ -265,11 +259,6 @@ private:
QMenu *openFolderMenu();
- QDir currentSavesDir() const;
-
- void startMonitorSaves();
- void stopMonitorSaves();
-
void dropLocalFile(const QUrl &url, const QString &outputDir, bool move);
void sendSelectedModsToPriority(int newPriority);
@@ -298,6 +287,8 @@ private:
std::unique_ptr<FilterList> m_Filters;
std::unique_ptr<DataTab> m_DataTab;
+ std::unique_ptr<DownloadsTab> m_DownloadsTab;
+ std::unique_ptr<SavesTab> m_SavesTab;
int m_OldProfileIndex;
@@ -325,9 +316,6 @@ private:
QFuture<void> m_MetaSave;
QTime m_StartTime;
- //SaveGameInfoWidget *m_CurrentSaveView;
- std::vector<std::shared_ptr<const MOBase::ISaveGame>> m_SaveGames;
- MOBase::ISaveGameInfoWidget *m_CurrentSaveView;
OrganizerCore &m_OrganizerCore;
PluginContainer &m_PluginContainer;
@@ -337,9 +325,6 @@ private:
std::unique_ptr<BrowserDialog> m_IntegratedBrowser;
- QTimer m_SavesWatcherTimer;
- QFileSystemWatcher m_SavesWatcher;
-
QByteArray m_ArchiveListHash;
bool m_DidUpdateMasterList;
@@ -361,7 +346,6 @@ private:
Executable* getSelectedExecutable();
private slots:
-
void updateWindowTitle(const APIUserAccount& user);
void showMessage(const QString &message);
void showError(const QString &message);
@@ -411,9 +395,6 @@ private slots:
void sendSelectedModsToBottom_clicked();
void sendSelectedModsToPriority_clicked();
void sendSelectedModsToSeparator_clicked();
- // savegame context menu
- void deleteSavegame_clicked();
- void fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets);
// data-tree context menu
// pluginlist context menu
@@ -428,7 +409,6 @@ private slots:
void linkMenu();
void languageChange(const QString &newLanguage);
- void saveSelectionChanged(QListWidgetItem *newItem);
void windowTutorialFinished(const QString &windowName);
@@ -493,12 +473,9 @@ private slots:
void modRenamed(const QString &oldName, const QString &newName);
void modRemoved(const QString &fileName);
- void hideSaveGameInfo();
-
void hookUpWindowTutorials();
bool shouldStartTutorial() const;
- void resumeDownload(int downloadIndex);
void endorseMod(ModInfo::Ptr mod);
void unendorseMod(ModInfo::Ptr mod);
void trackMod(ModInfo::Ptr mod, bool doTrack);
@@ -545,7 +522,6 @@ private slots:
void modFilterActive(bool active);
void espFilterChanged(const QString &filter);
- void downloadFilterChanged(const QString &filter);
void expandModList(const QModelIndex &index);
@@ -565,7 +541,6 @@ private slots:
void ignoreUpdate();
void unignoreUpdate();
- void refreshSavesIfOpen();
void about();
void modListSortIndicatorChanged(int column, Qt::SortOrder order);
@@ -582,6 +557,7 @@ private slots: // ui slots
// actions
void on_actionAdd_Profile_triggered();
void on_actionInstallMod_triggered();
+ void on_action_Refresh_triggered();
void on_actionModify_Executables_triggered();
void on_actionNexus_triggered();
void on_actionNotifications_triggered();
@@ -602,14 +578,12 @@ private slots: // ui slots
void on_centralWidget_customContextMenuRequested(const QPoint &pos);
void on_bsaList_customContextMenuRequested(const QPoint &pos);
void on_clearFiltersButton_clicked();
- void on_btnRefreshDownloads_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_savegameList_customContextMenuRequested(const QPoint &pos);
void on_startButton_clicked();
void on_tabWidget_currentChanged(int index);
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index dbc6013d..14cbdda1 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -1361,7 +1361,7 @@ p, li { white-space: pre-wrap; }
</spacer>
</item>
<item>
- <widget class="MOBase::LineEditClear" name="downloadFilterEdit">
+ <widget class="QLineEdit" name="downloadFilterEdit">
<property name="toolTip">
<string>Filter the list of downloads.</string>
</property>
@@ -1413,6 +1413,7 @@ p, li { white-space: pre-wrap; }
<addaction name="actionNexus"/>
<addaction name="actionModPage"/>
<addaction name="actionAdd_Profile"/>
+ <addaction name="action_Refresh"/>
<addaction name="actionModify_Executables"/>
<addaction name="actionTool"/>
<addaction name="actionSettings"/>
@@ -1482,6 +1483,7 @@ p, li { white-space: pre-wrap; }
<addaction name="actionToolBarTextOnly"/>
<addaction name="actionToolBarIconsAndText"/>
</widget>
+ <addaction name="action_Refresh"/>
<addaction name="menuToolbars"/>
<addaction name="actionViewLog"/>
<addaction name="separator"/>
@@ -1896,6 +1898,18 @@ p, li { white-space: pre-wrap; }
<string>Log</string>
</property>
</action>
+ <action name="action_Refresh">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;Refresh</string>
+ </property>
+ <property name="shortcut">
+ <string>F5</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index e7ac3926..fb6a7121 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -254,7 +254,10 @@ int MOApplication::doOneRun(MOMultiProcess& multiProcess)
sanity::checkEnvironment(env);
const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) {
- log::debug("loaded module {}", m.toString());
+ if (m.interesting()) {
+ log::debug("loaded module {}", m.toString());
+ }
+
sanity::checkIncompatibleModule(m);
});
@@ -356,9 +359,6 @@ int MOApplication::doOneRun(MOMultiProcess& multiProcess)
tt.start("MOApplication::doOneRun() MainWindow setup");
MainWindow mainWindow(settings, organizer, *pluginContainer);
- // qt resets the thread name somewhere when creating the main window
- MOShared::SetThisThreadName("main");
-
// the nexus interface can show dialogs, make sure they're parented to the
// main window
ni.getAccessManager()->setTopLevelWidget(&mainWindow);
diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp
index cf7f6340..1bfb3218 100644
--- a/src/modinfodialogconflicts.cpp
+++ b/src/modinfodialogconflicts.cpp
@@ -560,17 +560,14 @@ GeneralConflictsTab::GeneralConflictsTab(
m_filterOverwrite.setEdit(ui->overwriteLineEdit);
m_filterOverwrite.setList(ui->overwriteTree);
m_filterOverwrite.setUseSourceSort(true);
- m_filterOverwrite.setUpdateDelay(false);
m_filterOverwritten.setEdit(ui->overwrittenLineEdit);
m_filterOverwritten.setList(ui->overwrittenTree);
m_filterOverwritten.setUseSourceSort(true);
- m_filterOverwritten.setUpdateDelay(false);
m_filterNoConflicts.setEdit(ui->noConflictLineEdit);
m_filterNoConflicts.setList(ui->noConflictTree);
m_filterNoConflicts.setUseSourceSort(true);
- m_filterNoConflicts.setUpdateDelay(false);
QObject::connect(
ui->overwriteTree, &QTreeView::doubleClicked,
@@ -644,7 +641,7 @@ bool GeneralConflictsTab::update()
bool archive = false;
const int fileOrigin = file->getOrigin(archive);
-
+
++m_counts.numTotalFiles;
const auto& alternatives = file->getAlternatives();
@@ -895,7 +892,6 @@ AdvancedConflictsTab::AdvancedConflictsTab(
m_filter.setEdit(ui->conflictsAdvancedFilter);
m_filter.setList(ui->conflictsAdvancedList);
m_filter.setUseSourceSort(true);
- m_filter.setUpdateDelay(false);
// left-elide the overwrites column so that the nearest are visible
ui->conflictsAdvancedList->setItemDelegateForColumn(
diff --git a/src/modinfodialogimages.cpp b/src/modinfodialogimages.cpp
index e27e9686..8a22045e 100644
--- a/src/modinfodialogimages.cpp
+++ b/src/modinfodialogimages.cpp
@@ -63,7 +63,6 @@ ImagesTab::ImagesTab(ModInfoDialogTabContext cx) :
ui->imagesShowDDS->setEnabled(m_ddsAvailable);
m_filter.setEdit(ui->imagesFilter);
- m_filter.setUpdateDelay(false);
connect(&m_filter, &FilterWidget::changed, [&]{ onFilterChanged(); });
connect(ui->imagesExplore, &QAbstractButton::clicked, [&]{ onExplore(); });
diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp
index 1ee3cefc..f81cfa89 100644
--- a/src/modinfodialognexus.cpp
+++ b/src/modinfodialognexus.cpp
@@ -217,11 +217,11 @@ void NexusTab::onModChanged()
padding-top: 20px;
padding-bottom: 20px;
}
-
+
img {
max-width: 100%;
}
-
+
figure.quote {
position: relative;
padding: 24px;
@@ -257,7 +257,7 @@ void NexusTab::onModChanged()
display: inline-block;
cursor: pointer;
}
-
+
details summary::-webkit-details-marker {
display:none;
}
@@ -268,7 +268,7 @@ void NexusTab::onModChanged()
a
{
- /*should avoid overflow with long links forcing wordwrap regardless of spaces*/
+ /*should avoid overflow with long links forcing wordwrap regardless of spaces*/
overflow-wrap: break-word;
word-wrap: break-word;
@@ -325,7 +325,7 @@ void NexusTab::onSourceGameChanged()
for (auto game : plugin().plugins<MOBase::IPluginGame>()) {
if (game->gameName() == ui->sourceGame->currentText()) {
- mod().setGameName(game->gameShortName());
+ mod().setGameName(game->gameShortName());
mod().setLastNexusQuery(QDateTime::fromSecsSinceEpoch(0));
refreshData(mod().nexusId());
return;
@@ -411,7 +411,7 @@ void NexusTab::onCustomURLChanged()
void NexusTab::onVisitCustomURL()
{
- const auto url = mod().parseCustomURL();
+ const QUrl url = mod().parseCustomURL();
if (url.isValid()) {
shell::Open(url);
}
diff --git a/src/modinfodialogtextfiles.cpp b/src/modinfodialogtextfiles.cpp
index 26ca3eb8..564c2cf7 100644
--- a/src/modinfodialogtextfiles.cpp
+++ b/src/modinfodialogtextfiles.cpp
@@ -118,7 +118,6 @@ GenericFilesTab::GenericFilesTab(
m_filter.setEdit(filter);
m_filter.setList(m_list);
- m_filter.setUpdateDelay(false);
QObject::connect(
m_list->selectionModel(), &QItemSelectionModel::currentRowChanged,
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index 6c8b9054..001e1bb1 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -303,7 +303,7 @@ void NexusSSOLogin::onMessage(const QString& s)
// first answer
// open browser
- const auto url = NexusSSOPage.arg(m_guid);
+ const QUrl url = NexusSSOPage.arg(m_guid);
shell::Open(url);
m_timeout.stop();
@@ -376,7 +376,7 @@ void ValidationAttempt::start(NXMAccessManager& m, const QString& key)
m_timeout.start();
log::debug(
- "validator: attempt started with timeout of {} seconds", timeout().count());
+ "nexus: attempt started with timeout of {} seconds", timeout().count());
}
bool ValidationAttempt::sendRequest(
@@ -458,11 +458,11 @@ void ValidationAttempt::onFinished()
return;
}
- log::debug("validator attempt: request has finished");
+ log::debug("nexus: request has finished");
if (!m_reply) {
// shouldn't happen
- log::error("validator attempt: reply is null");
+ log::error("nexus: reply is null");
setFailure(HardError, QObject::tr("Internal error"));
return;
}
@@ -472,7 +472,7 @@ void ValidationAttempt::onFinished()
if (code == 0) {
// request wasn't even sent
- log::error("validator attempt: code is 0");
+ log::error("nexus: code is 0");
setFailure(SoftError, m_reply->errorString());
return;
}
@@ -539,7 +539,7 @@ void ValidationAttempt::onFinished()
void ValidationAttempt::onSslErrors(const QList<QSslError>& errors)
{
- log::error("validator attempt: ssl errors");
+ log::error("nexus: ssl errors");
for (auto& e : errors) {
log::error(" . {}", e.errorString());
@@ -557,7 +557,7 @@ void ValidationAttempt::setFailure(Result r, const QString& error)
{
if (r != Cancelled) {
// don't spam the log
- log::error("validator attempt: {}", error);
+ log::error("nexus: {}", error);
}
cleanup();
@@ -572,7 +572,7 @@ void ValidationAttempt::setFailure(Result r, const QString& error)
void ValidationAttempt::setSuccess(const APIUserAccount& user)
{
- log::debug("validator attempt successful");
+ log::debug("nexus connection successful");
cleanup();
m_result = Success;
@@ -617,7 +617,7 @@ std::vector<std::chrono::seconds> NexusKeyValidator::getTimeouts() const
void NexusKeyValidator::start(const QString& key, Behaviour b)
{
if (isActive()) {
- log::debug("validator: trying to start while ongoing; ignoring");
+ log::debug("nexus: trying to start while ongoing; ignoring");
return;
}
@@ -655,7 +655,7 @@ void NexusKeyValidator::createAttempts(
void NexusKeyValidator::cancel()
{
- log::debug("validator: cancelled");
+ log::debug("nexus: connection cancelled");
for (auto&& a : m_attempts) {
a->cancel();
@@ -935,6 +935,11 @@ void NXMAccessManager::apiCheck(const QString &apiKey, bool force)
return;
}
+ if (m_Settings && m_Settings->network().offlineMode()) {
+ m_validationState = NotChecked;
+ return;
+ }
+
if (force) {
m_validationState = NotChecked;
}
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index 631e49be..569faccb 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -1430,7 +1430,7 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function<void ()> f)
{
if (NexusInterface::instance().getAccessManager()->validated()) {
f();
- } else {
+ } else if (!m_Settings.network().offlineMode()) {
QString apiKey;
if (GlobalSettings::nexusApiKey(apiKey)) {
doAfterLogin([f]{ f(); });
diff --git a/src/pch.h b/src/pch.h
index 2a2246ba..bfc7f2d2 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -89,6 +89,7 @@
#include <QColorDialog>
#include <QComboBox>
#include <QCommandLinkButton>
+#include <QCompleter>
#include <QContextMenuEvent>
#include <QCoreApplication>
#include <QCryptographicHash>
diff --git a/src/profile.cpp b/src/profile.cpp
index 92f2abce..3cc1a2a3 100644
--- a/src/profile.cpp
+++ b/src/profile.cpp
@@ -1123,10 +1123,6 @@ void Profile::debugDump() const
for (const auto& status : m_ModStatus) {
- if (status.m_Overwrite) {
- continue;
- }
-
auto index = m_ModIndexByPriority.find(status.m_Priority);
if (index == m_ModIndexByPriority.end()) {
log::error("mod with priority {} not in priority map", status.m_Priority);
@@ -1139,6 +1135,10 @@ void Profile::debugDump() const
continue;
}
+ if (m->hasFlag(ModInfo::FLAG_OVERWRITE)) {
+ continue;
+ }
+
add(total, status);
if (m->hasFlag(ModInfo::FLAG_BACKUP)) {
diff --git a/src/profilesdialog.cpp b/src/profilesdialog.cpp
index 14bf0da5..d71a4531 100644
--- a/src/profilesdialog.cpp
+++ b/src/profilesdialog.cpp
@@ -115,11 +115,29 @@ void ProfilesDialog::showEvent(QShowEvent *event)
}
}
-void ProfilesDialog::on_closeButton_clicked()
+void ProfilesDialog::on_close_clicked()
{
close();
}
+void ProfilesDialog::on_select_clicked()
+{
+ const Profile::Ptr currentProfile = ui->profilesList->currentItem()
+ ->data(Qt::UserRole)
+ .value<Profile::Ptr>();
+
+ if (!currentProfile) {
+ return;
+ }
+
+ m_Selected = currentProfile->name();
+ close();
+}
+
+std::optional<QString> ProfilesDialog::selectedProfile() const
+{
+ return m_Selected;
+}
QListWidgetItem *ProfilesDialog::addItem(const QString &name)
{
@@ -142,6 +160,7 @@ void ProfilesDialog::createProfile(const QString &name, bool useDefaultSettings)
newItem->setData(Qt::UserRole, QVariant::fromValue(profile));
ui->profilesList->addItem(newItem);
m_FailState = false;
+ ui->profilesList->setCurrentItem(newItem);
emit profileCreated(profile.get());
} catch (const std::exception&) {
m_FailState = true;
@@ -157,6 +176,7 @@ void ProfilesDialog::createProfile(const QString &name, const Profile &reference
newItem->setData(Qt::UserRole, QVariant::fromValue(profile));
ui->profilesList->addItem(newItem);
m_FailState = false;
+ ui->profilesList->setCurrentItem(newItem);
emit profileCreated(profile.get());
} catch (const std::exception&) {
m_FailState = true;
@@ -250,6 +270,12 @@ void ProfilesDialog::on_renameButton_clicked()
{
Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
+ if (currentProfile->name() == m_ActiveProfileName) {
+ QMessageBox::warning(this, tr("Renaming active profile"),
+ tr("The active profile cannot be renamed. Please change to a different profile first."));
+ return;
+ }
+
bool valid = false;
QString name;
@@ -265,7 +291,7 @@ void ProfilesDialog::on_renameButton_clicked()
}
ui->profilesList->currentItem()->setText(name);
-
+
QString oldName = currentProfile->name();
currentProfile->rename(name);
@@ -342,6 +368,11 @@ void ProfilesDialog::on_profilesList_currentItemChanged(QListWidgetItem *current
}
}
+void ProfilesDialog::on_profilesList_itemActivated(QListWidgetItem* item)
+{
+ on_select_clicked();
+}
+
void ProfilesDialog::on_localSavesBox_stateChanged(int state)
{
Profile::Ptr currentProfile = ui->profilesList->currentItem()->data(Qt::UserRole).value<Profile::Ptr>();
diff --git a/src/profilesdialog.h b/src/profilesdialog.h
index 08298d0e..005bc33e 100644
--- a/src/profilesdialog.h
+++ b/src/profilesdialog.h
@@ -63,6 +63,11 @@ public:
**/
bool failed() const { return m_FailState; }
+ // if the dialog was closed with the 'select' button, returns the name of the
+ // selected profile; if the dialog was closed with 'cancel', returns empty
+ //
+ std::optional<QString> selectedProfile() const;
+
signals:
/**
@@ -95,7 +100,8 @@ private:
private slots:
- void on_closeButton_clicked();
+ void on_close_clicked();
+ void on_select_clicked();
void on_addProfileButton_clicked();
@@ -104,6 +110,7 @@ private slots:
void on_copyProfileButton_clicked();
void on_profilesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
+ void on_profilesList_itemActivated(QListWidgetItem* item);
void on_removeProfileButton_clicked();
@@ -119,6 +126,7 @@ private:
bool m_FailState;
MOBase::IPluginGame const *m_Game;
QString m_ActiveProfileName;
+ std::optional<QString> m_Selected;
};
#endif // PROFILESDIALOG_H
diff --git a/src/profilesdialog.ui b/src/profilesdialog.ui
index 644b0720..44ab0ee9 100644
--- a/src/profilesdialog.ui
+++ b/src/profilesdialog.ui
@@ -13,59 +13,74 @@
<property name="windowTitle">
<string>Profiles</string>
</property>
- <layout class="QHBoxLayout" name="horizontalLayout">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
<item>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QListWidget" name="profilesList">
- <property name="toolTip">
- <string>List of Profiles</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QListWidget" name="profilesList">
+ <property name="toolTip">
+ <string>List of Profiles</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8.25pt;&quot;&gt;This is the list of profiles. Each profile contains its own list and installation order of enabled mods (from a shared pool), its own list and load order of enabled plugins (esps/esms), a copy of the games ini-file and an optional savegame filter.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="localSavesBox">
- <property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, save games are stored locally to this profile and will not appear when starting with a different profile.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="whatsThis">
- <string>If checked, save games are local to this profile and will not appear when starting with a different profile.</string>
- </property>
- <property name="text">
- <string>Use profile-specific Save Games</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="localIniFilesBox">
- <property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked MO2 will use his own profile-specific game INI files, so that the &amp;quot;Global&amp;quot; ones in MyGames can be left vanilla. This different set of INI files is then offered to the game instead of the default one.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="whatsThis">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, MO2 will use a local set of game INI files (configuration and settings files), different from the default ones found in MyGames. This way changes to the INI settings will only affect this profile and the Global INI files can remain vanilla. MO2 will then show the profile INI files to the game instead of the Global ones.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>Use profile-specific Game INI Files</string>
- </property>
- <property name="checked">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="invalidationBox">
- <property name="toolTip">
- <string>This ensures data files from mods are actually used. You want to enable this unless you use a different tool for Archive Invalidation.</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="localSavesBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, save games are stored locally to this profile and will not appear when starting with a different profile.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="whatsThis">
+ <string>If checked, save games are local to this profile and will not appear when starting with a different profile.</string>
+ </property>
+ <property name="text">
+ <string>Use profile-specific Save Games</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="localIniFilesBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked MO2 will use his own profile-specific game INI files, so that the &amp;quot;Global&amp;quot; ones in MyGames can be left vanilla. This different set of INI files is then offered to the game instead of the default one.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, MO2 will use a local set of game INI files (configuration and settings files), different from the default ones found in MyGames. This way changes to the INI settings will only affect this profile and the Global INI files can remain vanilla. MO2 will then show the profile INI files to the game instead of the Global ones.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Use profile-specific Game INI Files</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="invalidationBox">
+ <property name="toolTip">
+ <string>This ensures data files from mods are actually used. You want to enable this unless you use a different tool for Archive Invalidation.</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
@@ -73,111 +88,149 @@ p, li { white-space: pre-wrap; }
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The Mod Organizer uses a workaround called &amp;quot;BSA redirection&amp;quot; (google is your friend) to fix this issue reliably and without further work. Simply activate and forget.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;&quot;&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;With Skyrim this bug seems to be fixed to a degree but whether a mod becomes active still depends on file dates. Therefore, it still makes sense to activate this.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>Automatic Archive Invalidation</string>
- </property>
- </widget>
- </item>
- </layout>
+ </property>
+ <property name="text">
+ <string>Automatic Archive Invalidation</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QPushButton" name="addProfileButton">
+ <property name="toolTip">
+ <string>Create a new profile from scratch</string>
+ </property>
+ <property name="whatsThis">
+ <string>Create a new profile from scratch</string>
+ </property>
+ <property name="text">
+ <string>Create</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="copyProfileButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Clone the selected profile</string>
+ </property>
+ <property name="whatsThis">
+ <string>This creates a new profile with the same settings and active mods as the selected one.</string>
+ </property>
+ <property name="text">
+ <string>Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="removeProfileButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Delete the selected Profile. This can not be un-done!</string>
+ </property>
+ <property name="whatsThis">
+ <string>Delete the selected Profile. This can not be un-done!</string>
+ </property>
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="renameButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="transferButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Transfer save games to the selected profile.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Transfer save games to the selected profile.</string>
+ </property>
+ <property name="text">
+ <string>Transfer Saves</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QPushButton" name="addProfileButton">
- <property name="toolTip">
- <string>Create a new profile from scratch</string>
- </property>
- <property name="whatsThis">
- <string>Create a new profile from scratch</string>
- </property>
- <property name="text">
- <string>Create</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="copyProfileButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Clone the selected profile</string>
- </property>
- <property name="whatsThis">
- <string>This creates a new profile with the same settings and active mods as the selected one.</string>
- </property>
- <property name="text">
- <string>Copy</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="removeProfileButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Delete the selected Profile. This can not be un-done!</string>
- </property>
- <property name="whatsThis">
- <string>Delete the selected Profile. This can not be un-done!</string>
- </property>
- <property name="text">
- <string>Remove</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="renameButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="text">
- <string>Rename</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="transferButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Transfer save games to the selected profile.</string>
- </property>
- <property name="whatsThis">
- <string>Transfer save games to the selected profile.</string>
- </property>
- <property name="text">
- <string>Transfer Saves</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="closeButton">
- <property name="toolTip">
- <string/>
- </property>
- <property name="text">
- <string>Close</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="select">
+ <property name="text">
+ <string>Select</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="close">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
</layout>
</widget>
diff --git a/src/sanitychecks.cpp b/src/sanitychecks.cpp
index 613e4b26..1e673ff9 100644
--- a/src/sanitychecks.cpp
+++ b/src/sanitychecks.cpp
@@ -220,46 +220,53 @@ int checkMissingFiles()
int checkBadOSDs(const env::Module& m)
{
- // these dlls seems to interfere mostly with dialogs, like the mod info
- // dialog: it renders dialogs fully white and makes it impossible to interact
- // with them
+ // these dlls seem to interfere mostly with dialogs, like the mod info
+ // dialog: they render some dialogs fully white and make it impossible to
+ // interact with them
//
- // the dlls is usually loaded on startup, but there has been some reports
- // where it got loaded later, so this is also called every time a new module
+ // the dlls is usually loaded on startup, but there has been some reports
+ // where they got loaded later, so this is also called every time a new module
// is loaded into this process
- const char* nahimic =
- "Nahimic (also known as SonicSuite, SonicRadar, SteelSeries, A-Volute, etc.)";
+ const std::string nahimic =
+ "Nahimic (also known as SonicSuite, SonicRadar, "
+ "SteelSeries, A-Volute, etc.)";
- static const std::map<QString, QString> names = {
- {"NahimicOSD.dll", nahimic},
- {"nahimicmsiosd.dll", nahimic},
- {"cassinimlkosd.dll", nahimic},
- {"SS3DevProps.dll", nahimic},
- {"ss2devprops.dll", nahimic},
- {"ss2osd.dll", nahimic},
- {"RTSSHooks64.dll", "RivaTuner Statistics Server"},
- {"SSAudioOSD.dll", "SteelSeries Audio"},
- {"specialk64.dll", "SpecialK"},
- {"corsairosdhook.x64.dll", "Corsair Utility Engine"},
- {"gtii-osd64-vk.dll", "ASUS GPU Tweak 2"},
- {"easyhook64.dll", "Razer Cortex"},
- {"k_fps64.dll", "Razer Cortex"}
+ auto p = [](std::string re, std::string s) {
+ return std::make_pair(std::regex(re, std::regex::icase), s);
+ };
+
+ static const std::vector<std::pair<std::regex, std::string>> list = {
+ p("nahimic(.*)osd\\.dll", nahimic),
+ p("cassini(.*)osd\\.dll", nahimic),
+ p(".+devprops.*.dll", nahimic),
+ p("ss2osd\\.dll", nahimic),
+ p("RTSSHooks64\\.dll", "RivaTuner Statistics Server"),
+ p("SSAudioOSD\\.dll", "SteelSeries Audio"),
+ p("specialk64\\.dll", "SpecialK"),
+ p("corsairosdhook\\.x64\\.dll", "Corsair Utility Engine"),
+ p("gtii-osd64-vk\\.dll", "ASUS GPU Tweak 2"),
+ p("easyhook64\\.dll", "Razer Cortex"),
+ p("k_fps64\\.dll", "Razer Cortex"),
+ p("fw1fontwrapper\\.dll", "Gigabyte 3D OSD"),
+ p("gfxhook64\\.dll", "Gigabyte 3D OSD")
};
const QFileInfo file(m.path());
int n = 0;
- for (auto&& p : names) {
- if (file.fileName().compare(p.first, Qt::CaseInsensitive) == 0) {
+ for (auto&& p : list) {
+ std::smatch m;
+ const auto filename = file.fileName().toStdString();
+
+ if (std::regex_match(filename, m, p.first)) {
log::warn("{}", QObject::tr(
"%1 is loaded.\nThis program is known to cause issues with "
"Mod Organizer, such as freezing or blank windows. Consider "
"uninstalling it.")
- .arg(p.second));
+ .arg(QString::fromStdString(p.second)));
log::warn("{}", file.absoluteFilePath());
-
++n;
}
}
@@ -272,7 +279,8 @@ int checkUsvfsIncompatibilites(const env::Module& m)
// these dlls seems to interfere with usvfs
static const std::map<QString, QString> names = {
- {"mactype64.dll", "Mactype"}
+ {"mactype64.dll", "Mactype"},
+ {"epclient64.dll", "Citrix ICA Client"}
};
const QFileInfo file(m.path());
@@ -283,7 +291,7 @@ int checkUsvfsIncompatibilites(const env::Module& m)
log::warn("{}", QObject::tr(
"%1 is loaded. This program is known to cause issues with "
"Mod Organizer and its virtual filesystem, such script extenders "
- "refusing to run. Consider uninstalling it.")
+ "or others programs refusing to run. Consider uninstalling it.")
.arg(p.second));
log::warn("{}", file.absoluteFilePath());
diff --git a/src/savestab.cpp b/src/savestab.cpp
new file mode 100644
index 00000000..e2c796b6
--- /dev/null
+++ b/src/savestab.cpp
@@ -0,0 +1,310 @@
+#include "savestab.h"
+#include "ui_mainwindow.h"
+#include "organizercore.h"
+#include "activatemodsdialog.h"
+#include <iplugingame.h>
+#include <isavegameinfowidget.h>
+
+using namespace MOBase;
+
+SavesTab::SavesTab(QWidget* window, OrganizerCore& core, Ui::MainWindow* mwui)
+ : m_window(window), m_core(core), m_CurrentSaveView(nullptr), ui{
+ mwui->tabWidget, mwui->savesTab, mwui->savegameList}
+{
+ m_SavesWatcherTimer.setSingleShot(true);
+ m_SavesWatcherTimer.setInterval(500);
+
+ ui.list->installEventFilter(this);
+ ui.list->setMouseTracking(true);
+
+ connect(
+ &m_SavesWatcher, &QFileSystemWatcher::directoryChanged,
+ [&]{ m_SavesWatcherTimer.start(); });
+
+ connect(
+ &m_SavesWatcherTimer, &QTimer::timeout,
+ [&]{ refreshSavesIfOpen(); });
+
+ connect(
+ ui.list, &QWidget::customContextMenuRequested,
+ [&](auto pos){ onContextMenu(pos); });
+
+ connect(
+ ui.list, &QListWidget::itemEntered,
+ [&](auto* item){ saveSelectionChanged(item); });
+
+ ui.list->installEventFilter(this);
+}
+
+bool SavesTab::eventFilter(QObject* object, QEvent* e)
+{
+ if (object == ui.list) {
+ if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) {
+ hideSaveGameInfo();
+ } else if (e->type() == QEvent::KeyPress) {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent*>(e);
+ if (keyEvent->key() == Qt::Key_Delete) {
+ deleteSavegame();
+ }
+ }
+ }
+
+ return false;
+}
+
+void SavesTab::displaySaveGameInfo(QListWidgetItem *newItem)
+{
+ // don't display the widget if the main window doesn't have focus
+ //
+ // this goes against the standard behaviour for tooltips, which are displayed
+ // on hover regardless of focus, but this widget is so large and busy that
+ // it's probably better this way
+ if (!m_window->isActiveWindow()){
+ return;
+ }
+
+ if (m_CurrentSaveView == nullptr) {
+ const IPluginGame* game = m_core.managedGame();
+ const SaveGameInfo* info = game->feature<SaveGameInfo>();
+
+ if (info != nullptr) {
+ m_CurrentSaveView = info->getSaveGameWidget(m_window);
+ }
+
+ if (m_CurrentSaveView == nullptr) {
+ return;
+ }
+ }
+
+ m_CurrentSaveView->setSave(*m_SaveGames[ui.list->row(newItem)]);
+
+ QWindow *window = m_CurrentSaveView->window()->windowHandle();
+ QRect screenRect;
+ if (window == nullptr)
+ screenRect = QGuiApplication::primaryScreen()->geometry();
+ else
+ screenRect = window->screen()->geometry();
+
+ QPoint pos = QCursor::pos();
+ if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
+ pos.rx() -= (m_CurrentSaveView->width() + 2);
+ } else {
+ pos.rx() += 5;
+ }
+
+ if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
+ pos.ry() -= (m_CurrentSaveView->height() + 10);
+ } else {
+ pos.ry() += 20;
+ }
+ m_CurrentSaveView->move(pos);
+
+ m_CurrentSaveView->show();
+ m_CurrentSaveView->setProperty("displayItem", QVariant::fromValue(static_cast<void *>(newItem)));
+}
+
+
+void SavesTab::saveSelectionChanged(QListWidgetItem *newItem)
+{
+ if (newItem == nullptr) {
+ hideSaveGameInfo();
+ } else if (m_CurrentSaveView == nullptr || newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
+ displaySaveGameInfo(newItem);
+ }
+}
+
+
+void SavesTab::hideSaveGameInfo()
+{
+ if (m_CurrentSaveView != nullptr) {
+ m_CurrentSaveView->deleteLater();
+ m_CurrentSaveView = nullptr;
+ }
+}
+
+void SavesTab::refreshSavesIfOpen()
+{
+ if (ui.mainTabs->currentWidget() == ui.tab) {
+ refreshSaveList();
+ }
+}
+
+QDir SavesTab::currentSavesDir() const
+{
+ QDir savesDir;
+ if (m_core.currentProfile()->localSavesEnabled()) {
+ savesDir.setPath(m_core.currentProfile()->savePath());
+ } else {
+ auto iniFiles = m_core.managedGame()->iniFiles();
+
+ if (iniFiles.isEmpty()) {
+ return m_core.managedGame()->savesDirectory();
+ }
+
+ QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]);
+
+ wchar_t path[MAX_PATH];
+ if (::GetPrivateProfileStringW(
+ L"General", L"SLocalSavePath", L"",
+ path, MAX_PATH,
+ iniPath.toStdWString().c_str()
+ )) {
+ savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path)));
+ }
+ else {
+ savesDir = m_core.managedGame()->savesDirectory();
+ }
+ }
+
+ return savesDir;
+}
+
+void SavesTab::startMonitorSaves()
+{
+ stopMonitorSaves();
+
+ QDir savesDir = currentSavesDir();
+
+ m_SavesWatcher.addPath(savesDir.absolutePath());
+}
+
+void SavesTab::stopMonitorSaves()
+{
+ if (m_SavesWatcher.directories().length() > 0) {
+ m_SavesWatcher.removePaths(m_SavesWatcher.directories());
+ }
+}
+
+void SavesTab::refreshSaveList()
+{
+ TimeThis tt("MainWindow::refreshSaveList()");
+
+ startMonitorSaves(); // re-starts monitoring
+
+ try
+ {
+ QDir savesDir = currentSavesDir();
+ MOBase::log::debug("reading save games from {}", savesDir.absolutePath());
+ m_SaveGames = m_core.managedGame()->listSaves(savesDir);
+ std::sort(m_SaveGames.begin(), m_SaveGames.end(), [](auto const& lhs, auto const& rhs) {
+ return lhs->getCreationTime() > rhs->getCreationTime();
+ });
+
+ ui.list->clear();
+ for (auto& save: m_SaveGames) {
+ ui.list->addItem(savesDir.relativeFilePath(save->getFilepath()));
+ }
+ }
+ catch(std::exception& e)
+ {
+ // listSaves() can throw
+ log::error("{}", e.what());
+ }
+}
+
+void SavesTab::deleteSavegame()
+{
+ SaveGameInfo const *info = m_core.managedGame()->feature<SaveGameInfo>();
+
+ QString savesMsgLabel;
+ QStringList deleteFiles;
+
+ int count = 0;
+
+ for (const QModelIndex &idx : ui.list->selectionModel()->selectedIndexes()) {
+
+ auto& saveGame = m_SaveGames[idx.row()];
+
+ if (count < 10) {
+ savesMsgLabel += "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
+ }
+ ++count;
+
+ deleteFiles += saveGame->allFiles();
+ }
+
+ if (count > 10) {
+ savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
+ }
+
+ if (QMessageBox::question(m_window, tr("Confirm"),
+ tr("Are you sure you want to remove the following %n save(s)?<br>"
+ "<ul>%1</ul><br>"
+ "Removed saves will be sent to the Recycle Bin.", "", count)
+ .arg(savesMsgLabel),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ shellDelete(deleteFiles, true); // recycle bin delete.
+ refreshSaveList();
+ }
+}
+
+
+void SavesTab::onContextMenu(const QPoint& pos)
+{
+ QItemSelectionModel* selection = ui.list->selectionModel();
+
+ if (!selection->hasSelection()) {
+ return;
+ }
+
+ QMenu menu;
+
+ SaveGameInfo const* info = this->m_core.managedGame()->feature<SaveGameInfo>();
+ if (info != nullptr) {
+ QAction* action = menu.addAction(tr("Fix enabled mods..."));
+ action->setEnabled(false);
+ if (selection->selectedIndexes().count() == 1) {
+ auto& save = m_SaveGames[selection->selectedIndexes()[0].row()];
+ SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save);
+ if (missing.size() != 0) {
+ connect(action, &QAction::triggered, this, [this, missing] { fixMods(missing); });
+ action->setEnabled(true);
+ }
+ }
+ }
+
+ QString deleteMenuLabel = tr("Delete %n save(s)", "", selection->selectedIndexes().count());
+ menu.addAction(deleteMenuLabel, [&]{ deleteSavegame(); });
+
+ menu.addAction(tr("Open in Explorer..."), [&]{ openInExplorer(); });
+
+ menu.exec(ui.list->viewport()->mapToGlobal(pos));
+}
+
+void SavesTab::fixMods(SaveGameInfo::MissingAssets const &missingAssets)
+{
+ ActivateModsDialog dialog(missingAssets, m_window);
+ if (dialog.exec() == QDialog::Accepted) {
+ // activate the required mods, then enable all esps
+ std::set<QString> modsToActivate = dialog.getModsToActivate();
+ for (std::set<QString>::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) {
+ if ((*iter != "<data>") && (*iter != "<overwrite>")) {
+ unsigned int modIndex = ModInfo::getIndex(*iter);
+ m_core.currentProfile()->setModEnabled(modIndex, true);
+ }
+ }
+
+ m_core.currentProfile()->writeModlist();
+ m_core.refreshLists();
+
+ std::set<QString> espsToActivate = dialog.getESPsToActivate();
+ for (std::set<QString>::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) {
+ m_core.pluginList()->enableESP(*iter);
+ }
+
+ m_core.saveCurrentLists();
+ }
+}
+
+void SavesTab::openInExplorer()
+{
+ const SaveGameInfo* info = m_core.managedGame()->feature<SaveGameInfo>();
+
+ const auto sel = ui.list->selectionModel()->selectedIndexes();
+ if (sel.empty()) {
+ return;
+ }
+
+ auto& saveGame = m_SaveGames[sel[0].row()];
+ shell::Explore(saveGame->getFilepath());
+}
diff --git a/src/savestab.h b/src/savestab.h
new file mode 100644
index 00000000..db82b816
--- /dev/null
+++ b/src/savestab.h
@@ -0,0 +1,64 @@
+#ifndef MODORGANIZER_SAVESTAB_INCLUDED
+#define MODORGANIZER_SAVESTAB_INCLUDED
+
+#include "savegameinfo.h"
+#include <filterwidget.h>
+
+namespace Ui { class MainWindow; }
+
+namespace MOBase
+{
+ class ISaveGame;
+ class ISaveGameInfoWidget;
+}
+
+class MainWindow;
+class OrganizerCore;
+
+
+class SavesTab : public QObject
+{
+ Q_OBJECT;
+
+public:
+ SavesTab(QWidget* window, OrganizerCore& core, Ui::MainWindow* ui);
+
+ void refreshSaveList();
+ void displaySaveGameInfo(QListWidgetItem *newItem);
+
+ QDir currentSavesDir() const;
+
+ void startMonitorSaves();
+ void stopMonitorSaves();
+ void hideSaveGameInfo();
+
+protected:
+ bool eventFilter(QObject* object, QEvent* e) override;
+
+private:
+ struct SavesTabUi
+ {
+ QTabWidget* mainTabs;
+ QWidget* tab;
+ QListWidget* list;
+ };
+
+ QWidget* m_window;
+ OrganizerCore& m_core;
+ SavesTabUi ui;
+ MOBase::FilterWidget m_filter;
+ std::vector<std::shared_ptr<const MOBase::ISaveGame>> m_SaveGames;
+ MOBase::ISaveGameInfoWidget *m_CurrentSaveView;
+
+ QTimer m_SavesWatcherTimer;
+ QFileSystemWatcher m_SavesWatcher;
+
+ void onContextMenu(const QPoint &pos);
+ void deleteSavegame();
+ void saveSelectionChanged(QListWidgetItem *newItem);
+ void fixMods(SaveGameInfo::MissingAssets const &missingAssets);
+ void refreshSavesIfOpen();
+ void openInExplorer();
+};
+
+#endif // MODORGANIZER_SAVESTAB_INCLUDED
diff --git a/src/settings.cpp b/src/settings.cpp
index 85aa5f2f..f5ad83a7 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "settingsutilities.h"
#include "serverinfo.h"
#include "executableslist.h"
+#include "instancemanager.h"
#include "shared/appconfig.h"
#include "env.h"
#include "envmetrics.h"
@@ -65,7 +66,8 @@ Settings::Settings(const QString& path, bool globalInstance) :
m_Settings(path, QSettings::IniFormat),
m_Game(m_Settings), m_Geometry(m_Settings),
m_Widgets(m_Settings, globalInstance), m_Colors(m_Settings),
- m_Plugins(m_Settings), m_Paths(m_Settings), m_Network(m_Settings),
+ m_Plugins(m_Settings), m_Paths(m_Settings),
+ m_Network(m_Settings, globalInstance),
m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings),
m_Interface(m_Settings), m_Diagnostics(m_Settings)
{
@@ -468,7 +470,34 @@ const DiagnosticsSettings& Settings::diagnostics() const
QSettings::Status Settings::sync() const
{
m_Settings.sync();
- return m_Settings.status();
+
+ const auto s = m_Settings.status();
+
+ // there's a bug in Qt at least until 5.15.0 where a utf-8 bom in the ini is
+ // handled correctly but still sets FormatError
+ //
+ // see qsettings.cpp, in QConfFileSettingsPrivate::readIniFile(), there's a
+ // specific check for utf-8, which adjusts `dataPos` so it's skipped, but
+ // the FLUSH_CURRENT_SECTION() macro uses `currentSectionStart`, and that one
+ // isn't adjusted when changing `dataPos` on the first line and so stays 0
+ //
+ // this puts the bom in `unparsedIniSections` and eventually sets FormatError
+ // somewhere
+ //
+ //
+ // the other problem is that the status is never reset, not even when calling
+ // sync(), so the FormatError that's returned here is actually from reading
+ // the ini, not writing it
+ //
+ //
+ // since it's impossible to get a FormatError on write, it's considered to
+ // be a NoError here
+
+ if (s == QSettings::FormatError) {
+ return QSettings::NoError;
+ } else {
+ return s;
+ }
}
QSettings::Status Settings::iniStatus() const
@@ -479,7 +508,9 @@ QSettings::Status Settings::iniStatus() const
void Settings::dump() const
{
static const QStringList ignore({
- "username", "password", "nexus_api_key", "nexus_username", "nexus_password"
+ "username", "password",
+ "nexus_api_key", "nexus_username", "nexus_password",
+ "steam_username"
});
log::debug("settings:");
@@ -497,6 +528,7 @@ void Settings::dump() const
}
m_Network.dump();
+ m_Nexus.dump();
}
void Settings::managedGameChanged(IPluginGame const *gamePlugin)
@@ -1524,7 +1556,7 @@ std::map<QString, QString> PathSettings::recent() const
if (name.isValid() && dir.isValid()) {
map.emplace(name.toString(), dir.toString());
}
- });
+ });
return map;
}
@@ -1666,9 +1698,21 @@ void PathSettings::setOverwrite(const QString& path)
}
-NetworkSettings::NetworkSettings(QSettings& settings)
+NetworkSettings::NetworkSettings(QSettings& settings, bool globalInstance)
: m_Settings(settings)
{
+ if (globalInstance) {
+ updateCustomBrowser();
+ }
+}
+
+void NetworkSettings::updateCustomBrowser()
+{
+ if (useCustomBrowser()) {
+ MOBase::shell::SetUrlHandler(customBrowserCommand());
+ } else {
+ MOBase::shell::SetUrlHandler("");
+ }
}
bool NetworkSettings::offlineMode() const
@@ -1800,6 +1844,28 @@ void NetworkSettings::updateFromOldMap()
updateServers(servers);
}
+bool NetworkSettings::useCustomBrowser() const
+{
+ return get<bool>(m_Settings, "Settings", "use_custom_browser", false);
+}
+
+void NetworkSettings::setUseCustomBrowser(bool b)
+{
+ set(m_Settings, "Settings", "use_custom_browser", b);
+ updateCustomBrowser();
+}
+
+QString NetworkSettings::customBrowserCommand() const
+{
+ return get<QString>(m_Settings, "Settings", "custom_browser", "");
+}
+
+void NetworkSettings::setCustomBrowserCommand(const QString& s)
+{
+ set(m_Settings, "Settings", "custom_browser", s);
+ updateCustomBrowser();
+}
+
ServerList NetworkSettings::serversFromOldMap() const
{
// for 2.2.1 and before
@@ -1891,7 +1957,10 @@ void NexusSettings::setTrackedIntegration(bool b) const
void NexusSettings::registerAsNXMHandler(bool force)
{
- const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe";
+ const auto nxmPath =
+ QCoreApplication::applicationDirPath() + "/" +
+ QString::fromStdWString(AppConfig::nxmHandlerExe());
+
const auto executable = QCoreApplication::applicationFilePath();
QString mode = force ? "forcereg" : "reg";
@@ -1944,6 +2013,50 @@ std::vector<std::chrono::seconds> NexusSettings::validationTimeouts() const
return v;
}
+void NexusSettings::dump() const
+{
+ const auto iniPath =
+ InstanceManager::singleton().globalInstancesRootPath() + "/" +
+ QString::fromStdWString(AppConfig::nxmHandlerIni());
+
+ if (!QFileInfo(iniPath).exists()) {
+ log::debug("nxm ini not found at {}", iniPath);
+ return;
+ }
+
+ QSettings s(iniPath, QSettings::IniFormat);
+ if (const auto st=s.status(); st != QSettings::NoError) {
+ log::debug("can't read nxm ini from {}", iniPath);
+ return;
+ }
+
+ log::debug("nxmhandler settings:");
+
+ QSettings handler("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", QSettings::NativeFormat);
+ log::debug(" . primary: {}", handler.value("shell/open/command/Default").toString());
+
+ const auto noregister = getOptional<bool>(s, "General", "noregister");
+
+ if (noregister) {
+ log::debug(" . noregister: {}", *noregister);
+ } else {
+ log::debug(" . noregister: (not found)");
+ }
+
+ ScopedReadArray sra(s, "handlers");
+
+ sra.for_each([&] {
+ const auto games = sra.get<QVariant>("games");
+ const auto executable = sra.get<QVariant>("executable");
+ const auto arguments = sra.get<QVariant>("arguments");
+
+ log::debug(" . handler:");
+ log::debug(" . games: {}", games.toString());
+ log::debug(" . executable: {}", executable.toString());
+ log::debug(" . arguments: {}", arguments.toString());
+ });
+}
+
SteamSettings::SteamSettings(Settings& parent, QSettings& settings)
: m_Parent(parent), m_Settings(settings)
diff --git a/src/settings.h b/src/settings.h
index 48aefc27..4d1258bf 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -447,7 +447,10 @@ private:
class NetworkSettings
{
public:
- NetworkSettings(QSettings& settings);
+ // globalInstance is forwarded from the Settings constructor; NetworkSettings
+ // will set the global URL custom command
+ //
+ NetworkSettings(QSettings& settings, bool globalInstance);
// whether the user has disabled online features
//
@@ -477,6 +480,16 @@ public:
//
void updateFromOldMap();
+ // whether to use a custom command to open links
+ //
+ bool useCustomBrowser() const;
+ void setUseCustomBrowser(bool b);
+
+ // custom command to open links
+ //
+ QString customBrowserCommand() const;
+ void setCustomBrowserCommand(const QString& s);
+
void dump() const;
private:
@@ -485,6 +498,10 @@ private:
// for pre 2.2.1 ini files
//
ServerList serversFromOldMap() const;
+
+ // sets the custom command in uibase, called when the settings change
+ //
+ void updateCustomBrowser();
};
@@ -528,6 +545,10 @@ public:
std::vector<std::chrono::seconds> validationTimeouts() const;
+ // dumps nxmhandler stuff
+ //
+ void dump() const;
+
private:
Settings& m_Parent;
QSettings& m_Settings;
@@ -689,8 +710,8 @@ public:
// any Settings object created with globalInstance==false won't set the
// singleton
//
- // only WidgetSettings need to know whether it created from a globalInstance,
- // see its constructor
+ // some sub-objects like WidgetSettings and NetworkSettings need to know
+ // whether they were created from a globalInstance, see their constructor
//
// @param path path to an ini file
// @param globalInsance whether this is the global instance; creates the
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index 8670752b..43af5e9f 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -836,7 +836,7 @@
</widget>
</item>
<item>
- <layout class="QGridLayout" name="gridLayout_8">
+ <layout class="QGridLayout" name="gridLayout_8" columnstretch="1,1">
<item row="1" column="0">
<widget class="QCheckBox" name="proxyBox">
<property name="statusTip">
@@ -850,16 +850,13 @@
</property>
</widget>
</item>
- <item row="0" column="0">
- <widget class="QCheckBox" name="offlineBox">
- <property name="statusTip">
- <string>Disable automatic internet features</string>
- </property>
- <property name="whatsThis">
- <string>Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)</string>
- </property>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="trackedBox">
<property name="text">
- <string>Offline Mode</string>
+ <string>Tracked Integration</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
</property>
</widget>
</item>
@@ -873,6 +870,19 @@
</property>
</widget>
</item>
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="offlineBox">
+ <property name="statusTip">
+ <string>Disable automatic internet features</string>
+ </property>
+ <property name="whatsThis">
+ <string>Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)</string>
+ </property>
+ <property name="text">
+ <string>Offline Mode</string>
+ </property>
+ </widget>
+ </item>
<item row="2" column="0">
<widget class="QCheckBox" name="hideAPICounterBox">
<property name="toolTip">
@@ -886,14 +896,58 @@
</property>
</widget>
</item>
- <item row="1" column="1">
- <widget class="QCheckBox" name="trackedBox">
- <property name="text">
- <string>Tracked Integration</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
+ <item row="2" column="1">
+ <widget class="QWidget" name="widget_7" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="useCustomBrowser">
+ <property name="toolTip">
+ <string>Use &quot;%1&quot; as a placeholder for the URL.</string>
+ </property>
+ <property name="statusTip">
+ <string>Use &quot;%1&quot; as a placeholder for the URL.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Use &quot;%1&quot; as a placeholder for the URL.</string>
+ </property>
+ <property name="text">
+ <string>Custom browser</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="browserCommand">
+ <property name="toolTip">
+ <string>Use &quot;%1&quot; as a placeholder for the URL.</string>
+ </property>
+ <property name="statusTip">
+ <string>Use &quot;%1&quot; as a placeholder for the URL.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Use &quot;%1&quot; as a placeholder for the URL.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="browseCustomBrowser">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
</layout>
diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp
index 461ff8d6..48827a0e 100644
--- a/src/settingsdialognexus.cpp
+++ b/src/settingsdialognexus.cpp
@@ -300,6 +300,8 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d)
ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration());
ui->trackedBox->setChecked(settings().nexus().trackedIntegration());
ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter());
+ ui->useCustomBrowser->setChecked(settings().network().useCustomBrowser());
+ ui->browserCommand->setText(settings().network().customBrowserCommand());
// display server preferences
for (const auto& server : s.network().servers()) {
@@ -345,10 +347,13 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d)
[&]{ dialog().setExitNeeded(Exit::Restart); });
- QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ on_clearCacheButton_clicked(); });
- QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ on_associateButton_clicked(); });
+ QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ clearCache(); });
+ QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ associate(); });
+ QObject::connect(ui->useCustomBrowser, &QCheckBox::clicked, [&]{ updateCustomBrowser(); });
+ QObject::connect(ui->browseCustomBrowser, &QPushButton::clicked, [&]{ browseCustomBrowser(); });
updateNexusData();
+ updateCustomBrowser();
}
void NexusSettingsTab::update()
@@ -358,6 +363,8 @@ void NexusSettingsTab::update()
settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked());
settings().nexus().setTrackedIntegration(ui->trackedBox->isChecked());
settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked());
+ settings().network().setUseCustomBrowser(ui->useCustomBrowser->isChecked());
+ settings().network().setCustomBrowserCommand(ui->browserCommand->text());
auto servers = settings().network().servers();
@@ -407,13 +414,13 @@ void NexusSettingsTab::update()
settings().network().updateServers(servers);
}
-void NexusSettingsTab::on_clearCacheButton_clicked()
+void NexusSettingsTab::clearCache()
{
QDir(Settings::instance().paths().cache()).removeRecursively();
NexusInterface::instance().clearCache();
}
-void NexusSettingsTab::on_associateButton_clicked()
+void NexusSettingsTab::associate()
{
Settings::instance().nexus().registerAsNXMHandler(true);
}
@@ -442,3 +449,25 @@ void NexusSettingsTab::updateNexusData()
ui->nexusHourlyRequests->setText(QObject::tr("N/A"));
}
}
+
+void NexusSettingsTab::updateCustomBrowser()
+{
+ ui->browserCommand->setEnabled(ui->useCustomBrowser->isChecked());
+}
+
+void NexusSettingsTab::browseCustomBrowser ()
+{
+ const QString Filters =
+ QObject::tr("Executables (*.exe)") + ";;" +
+ QObject::tr("All Files (*.*)");
+
+ QString file = QFileDialog::getOpenFileName(
+ parentWidget(), QObject::tr("Select the browser executable"),
+ ui->browserCommand->text(), Filters);
+
+ if (file.isNull() || file == "") {
+ return;
+ }
+
+ ui->browserCommand->setText(file + " \"%1\"");
+}
diff --git a/src/settingsdialognexus.h b/src/settingsdialognexus.h
index c915accf..a9bd46c7 100644
--- a/src/settingsdialognexus.h
+++ b/src/settingsdialognexus.h
@@ -65,9 +65,12 @@ public:
private:
std::unique_ptr<NexusConnectionUI> m_connectionUI;
- void on_clearCacheButton_clicked();
- void on_associateButton_clicked();
+ void clearCache();
+ void associate();
+
void updateNexusData();
+ void updateCustomBrowser();
+ void browseCustomBrowser();
};
#endif // SETTINGSDIALOGNEXUS_H
diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc
index 5839c2f4..9058bf18 100644
--- a/src/shared/appconfig.inc
+++ b/src/shared/appconfig.inc
@@ -18,6 +18,8 @@ APPPARAM(std::wstring, proxyDLLOrig, L"steam_api_orig.dll") // needs to be ident
APPPARAM(std::wstring, proxyDLLSource, L"proxy.dll")
APPPARAM(std::wstring, vfs32DLLName, L"usvfs_x86.dll")
APPPARAM(std::wstring, vfs64DLLName, L"usvfs_x64.dll")
+APPPARAM(std::wstring, nxmHandlerExe, L"nxmhandler.exe")
+APPPARAM(std::wstring, nxmHandlerIni, L"nxmhandler.ini")
APPPARAM(std::wstring, portableLockFileName, L"portable.txt")
APPPARAM(const wchar_t*, localSavePlaceholder, L"__MOProfileSave__\\")
diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp
index c4b467d6..0d275844 100644
--- a/src/shared/directoryentry.cpp
+++ b/src/shared/directoryentry.cpp
@@ -636,7 +636,7 @@ void DirectoryEntry::addFiles(
onDirectoryEnd((Context*)pcx, path);
},
- [](void* pcx, std::wstring_view path, FILETIME ft)
+ [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t)
{
onFile((Context*)pcx, path, ft);
}
diff --git a/src/shared/util.cpp b/src/shared/util.cpp
index f316549e..54c6b841 100644
--- a/src/shared/util.cpp
+++ b/src/shared/util.cpp
@@ -210,12 +210,13 @@ std::wstring GetFileVersionString(const std::wstring &fileName)
VersionInfo createVersionInfo()
{
- VS_FIXEDFILEINFO version = GetFileVersion(QApplication::applicationFilePath().toStdWString());
+ VS_FIXEDFILEINFO version = GetFileVersion(env::thisProcessPath().native());
if (version.dwFileFlags & VS_FF_PRERELEASE)
{
// Pre-release builds need annotating
- QString versionString = QString::fromStdWString(GetFileVersionString(QApplication::applicationFilePath().toStdWString()));
+ QString versionString = QString::fromStdWString(
+ GetFileVersionString(env::thisProcessPath().native()));
// The pre-release flag can be set without the string specifying what type of pre-release
bool noLetters = true;