summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2020-02-09 08:52:03 -0500
committerGitHub <noreply@github.com>2020-02-09 08:52:03 -0500
commit1f1f99f253f981ae3d11e9df177a144d00dbce0e (patch)
tree01d38125329508f5628f9221f797e303180c8fc3 /src
parent718c2a56f91f824c5eab69d8cc16294f77243370 (diff)
parent3a80685189967fbf4cfa002ac2b8a4fa421306ca (diff)
Merge pull request #994 from isanae/generic-file-list
New file tree
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt33
-rw-r--r--src/datatab.cpp153
-rw-r--r--src/datatab.h60
-rw-r--r--src/downloadlistwidget.cpp1
-rw-r--r--src/env.h34
-rw-r--r--src/envmodule.cpp11
-rw-r--r--src/envshell.cpp676
-rw-r--r--src/envshell.h86
-rw-r--r--src/filetree.cpp852
-rw-r--r--src/filetree.h74
-rw-r--r--src/filetreeitem.cpp392
-rw-r--r--src/filetreeitem.h320
-rw-r--r--src/filetreemodel.cpp1092
-rw-r--r--src/filetreemodel.h167
-rw-r--r--src/iconfetcher.cpp159
-rw-r--r--src/iconfetcher.h76
-rw-r--r--src/loglist.cpp19
-rw-r--r--src/loglist.h6
-rw-r--r--src/main.cpp2
-rw-r--r--src/mainwindow.cpp690
-rw-r--r--src/mainwindow.h50
-rw-r--r--src/mainwindow.ui329
-rw-r--r--src/moapplication.cpp6
-rw-r--r--src/modinfodialog.cpp15
-rw-r--r--src/modinfodialogfwd.h3
-rw-r--r--src/modlist.cpp5
-rw-r--r--src/organizercore.cpp10
-rw-r--r--src/organizercore.h12
-rw-r--r--src/pch.h2
-rw-r--r--src/pluginlist.cpp5
-rw-r--r--src/shared/directoryentry.cpp1331
-rw-r--r--src/shared/directoryentry.h461
-rw-r--r--src/shared/util.cpp142
-rw-r--r--src/shared/util.h25
-rw-r--r--src/spawn.cpp9
-rw-r--r--src/spawn.h2
-rw-r--r--src/statusbar.cpp30
-rw-r--r--src/statusbar.h2
-rw-r--r--src/stylesheets/vs15 Dark-Green.qss13
-rw-r--r--src/stylesheets/vs15 Dark-Orange.qss15
-rw-r--r--src/stylesheets/vs15 Dark-Purple.qss15
-rw-r--r--src/stylesheets/vs15 Dark-Red.qss15
-rw-r--r--src/stylesheets/vs15 Dark-Yellow.qss16
-rw-r--r--src/stylesheets/vs15 Dark.qss13
-rw-r--r--src/version.rc2
45 files changed, 5693 insertions, 1738 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5d81f9da..ad8f74c0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -135,6 +135,7 @@ SET(organizer_SRCS
envmetrics.cpp
envmodule.cpp
envsecurity.cpp
+ envshell.cpp
envshortcut.cpp
envwindows.cpp
colortable.cpp
@@ -144,6 +145,11 @@ SET(organizer_SRCS
loot.cpp
lootdialog.cpp
filterlist.cpp
+ datatab.cpp
+ filetree.cpp
+ filetreemodel.cpp
+ filetreeitem.cpp
+ iconfetcher.cpp
shared/windows_error.cpp
shared/error_report.cpp
@@ -259,6 +265,7 @@ SET(organizer_HDRS
envmetrics.h
envmodule.h
envsecurity.h
+ envshell.h
envshortcut.h
envwindows.h
colortable.h
@@ -268,6 +275,11 @@ SET(organizer_HDRS
lootdialog.h
json.h
filterlist.h
+ datatab.h
+ filetree.h
+ filetreeitem.h
+ filetreemodel.h
+ iconfetcher.h
shared/windows_error.h
shared/error_report.h
@@ -320,16 +332,13 @@ SET(organizer_RCS
source_group(src REGULAR_EXPRESSION ".*\\.(h|cpp|ui)")
set(application
- filterlist
iuserinterface
main
- mainwindow
moapplication
moshortcut
sanitychecks
selfupdater
singleinstance
- statusbar
)
set(browser
@@ -386,6 +395,7 @@ set(env
envmetrics
envmodule
envsecurity
+ envshell
envshortcut
envwindows
)
@@ -400,6 +410,17 @@ set(loot
lootdialog
)
+set(mainwindow
+ datatab
+ iconfetcher
+ filetree
+ filetreeitem
+ filetreemodel
+ filterlist
+ mainwindow
+ statusbar
+)
+
set(modinfo
modinfo
modinfobackup
@@ -496,9 +517,9 @@ set(widgets
)
set(src_filters
- application core browser dialogs downloads env executables loot modinfo
- modinfo\\dialog modlist plugins previews profiles settings settingsdialog
- utilities widgets
+ application core browser dialogs downloads env executables loot mainwindow
+ modinfo modinfo\\dialog modlist plugins previews profiles settings
+ settingsdialog utilities widgets
)
foreach(filter in list ${src_filters})
diff --git a/src/datatab.cpp b/src/datatab.cpp
new file mode 100644
index 00000000..f263c2c6
--- /dev/null
+++ b/src/datatab.cpp
@@ -0,0 +1,153 @@
+#include "datatab.h"
+#include "ui_mainwindow.h"
+#include "settings.h"
+#include "organizercore.h"
+#include "directoryentry.h"
+#include "messagedialog.h"
+#include "filetree.h"
+#include "filetreemodel.h"
+#include <log.h>
+#include <report.h>
+
+using namespace MOShared;
+using namespace MOBase;
+
+// in mainwindow.cpp
+QString UnmanagedModName();
+
+
+DataTab::DataTab(
+ OrganizerCore& core, PluginContainer& pc,
+ QWidget* parent, Ui::MainWindow* mwui) :
+ m_core(core), m_pluginContainer(pc), m_parent(parent),
+ ui{
+ mwui->dataTabRefresh, mwui->dataTree,
+ mwui->dataTabShowOnlyConflicts, mwui->dataTabShowFromArchives},
+ m_firstActivation(true)
+{
+ m_filetree.reset(new FileTree(core, m_pluginContainer, ui.tree));
+ m_filter.setEdit(mwui->dataTabFilter);
+ m_filter.setList(mwui->dataTree);
+ m_filter.setUseSourceSort(true);
+ m_filter.setFilterColumn(FileTreeModel::FileName);
+
+ if (auto* m=m_filter.proxyModel()) {
+ m->setDynamicSortFilter(false);
+ }
+
+ connect(
+ &m_filter, &FilterWidget::aboutToChange,
+ [&]{ ensureFullyLoaded(); });
+
+ connect(
+ ui.refresh, &QPushButton::clicked,
+ [&]{ onRefresh(); });
+
+ connect(
+ ui.conflicts, &QCheckBox::toggled,
+ [&]{ onConflicts(); });
+
+ connect(
+ ui.archives, &QCheckBox::toggled,
+ [&]{ onArchives(); });
+
+ connect(
+ m_filetree.get(), &FileTree::executablesChanged,
+ this, &DataTab::executablesChanged);
+
+ connect(
+ m_filetree.get(), &FileTree::originModified,
+ this, &DataTab::originModified);
+
+ connect(
+ m_filetree.get(), &FileTree::displayModInformation,
+ this, &DataTab::displayModInformation);
+}
+
+void DataTab::saveState(Settings& s) const
+{
+ s.geometry().saveState(ui.tree->header());
+ s.widgets().saveChecked(ui.conflicts);
+ s.widgets().saveChecked(ui.archives);
+}
+
+void DataTab::restoreState(const Settings& s)
+{
+ s.geometry().restoreState(ui.tree->header());
+
+ // prior to 2.3, the list was not sortable, and this remembered in the
+ // widget state, for whatever reason
+ ui.tree->setSortingEnabled(true);
+
+ s.widgets().restoreChecked(ui.conflicts);
+ s.widgets().restoreChecked(ui.archives);
+}
+
+void DataTab::activated()
+{
+ if (m_firstActivation) {
+ m_firstActivation = false;
+ updateTree();
+ }
+}
+
+void DataTab::onRefresh()
+{
+ if (QGuiApplication::keyboardModifiers() & Qt::ShiftModifier) {
+ m_filetree->model()->setEnabled(false);
+ m_filetree->clear();
+ }
+
+ m_core.refreshDirectoryStructure();
+}
+
+void DataTab::updateTree()
+{
+ m_filetree->model()->setEnabled(true);
+ m_filetree->refresh();
+
+ if (!m_filter.empty()) {
+ ensureFullyLoaded();
+
+ if (auto* m=m_filter.proxyModel()) {
+ m->invalidate();
+ }
+ }
+}
+
+void DataTab::ensureFullyLoaded()
+{
+ if (!m_filetree->fullyLoaded()) {
+ m_filter.proxyModel()->setRecursiveFilteringEnabled(false);
+ m_filetree->ensureFullyLoaded();
+ m_filter.proxyModel()->setRecursiveFilteringEnabled(true);
+ }
+}
+
+void DataTab::onConflicts()
+{
+ updateOptions();
+}
+
+void DataTab::onArchives()
+{
+ updateOptions();
+}
+
+void DataTab::updateOptions()
+{
+ using M = FileTreeModel;
+
+ M::Flags flags = M::NoFlags;
+
+ if (ui.conflicts->isChecked()) {
+ flags |= M::ConflictsOnly | M::PruneDirectories;
+ }
+
+ if (ui.archives->isChecked()) {
+ flags |= M::Archives;
+ }
+
+ m_filetree->model()->setFlags(flags);
+ updateTree();
+}
diff --git a/src/datatab.h b/src/datatab.h
new file mode 100644
index 00000000..a0f29a5f
--- /dev/null
+++ b/src/datatab.h
@@ -0,0 +1,60 @@
+#include "modinfodialogfwd.h"
+#include "modinfo.h"
+#include <filterwidget.h>
+#include <QPushButton>
+#include <QTreeWidget>
+#include <QCheckBox>
+
+namespace Ui { class MainWindow; }
+class OrganizerCore;
+class Settings;
+class PluginContainer;
+class FileTree;
+
+namespace MOShared { class DirectoryEntry; }
+
+class DataTab : public QObject
+{
+ Q_OBJECT;
+
+public:
+ DataTab(
+ OrganizerCore& core, PluginContainer& pc,
+ QWidget* parent, Ui::MainWindow* ui);
+
+ void saveState(Settings& s) const;
+ void restoreState(const Settings& s);
+ void activated();
+
+ void updateTree();
+
+signals:
+ void executablesChanged();
+ void originModified(int originID);
+ void displayModInformation(ModInfo::Ptr m, unsigned int i, ModInfoTabIDs tab);
+
+private:
+ struct DataTabUi
+ {
+ QPushButton* refresh;
+ QTreeView* tree;
+ QCheckBox* conflicts;
+ QCheckBox* archives;
+ };
+
+ OrganizerCore& m_core;
+ PluginContainer& m_pluginContainer;
+ QWidget* m_parent;
+ DataTabUi ui;
+ std::unique_ptr<FileTree> m_filetree;
+ std::vector<QTreeWidgetItem*> m_removeLater;
+ MOBase::FilterWidget m_filter;
+ bool m_firstActivation;
+
+ void onRefresh();
+ void onItemExpanded(QTreeWidgetItem* item);
+ void onConflicts();
+ void onArchives();
+ void updateOptions();
+ void ensureFullyLoaded();
+};
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp
index ac37b0ee..5ca6cb51 100644
--- a/src/downloadlistwidget.cpp
+++ b/src/downloadlistwidget.cpp
@@ -103,6 +103,7 @@ DownloadListWidget::DownloadListWidget(QWidget *parent)
{
setHeader(new DownloadListHeader(Qt::Horizontal, this));
+ header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
header()->setSectionsMovable(true);
header()->setContextMenuPolicy(Qt::CustomContextMenu);
header()->setCascadingSectionResizes(true);
diff --git a/src/env.h b/src/env.h
index 16f8039e..5c7492c6 100644
--- a/src/env.h
+++ b/src/env.h
@@ -30,6 +30,23 @@ struct DesktopDCReleaser
using DesktopDCPtr = std::unique_ptr<HDC, DesktopDCReleaser>;
+// used by HMenuPtr, calls DestroyMenu() as the deleter
+//
+struct HMenuFreer
+{
+ using pointer = HMENU;
+
+ void operator()(HMENU h)
+ {
+ if (h != 0) {
+ ::DestroyMenu(h);
+ }
+ }
+};
+
+using HMenuPtr = std::unique_ptr<HMENU, HMenuFreer>;
+
+
// used by LibraryPtr, calls FreeLibrary as the deleter
//
struct LibraryFreer
@@ -94,6 +111,23 @@ template <class T>
using LocalPtr = std::unique_ptr<T, LocalFreer<T>>;
+// used by the CoTaskMemPtr, calls CoTaskMemFree() as the deleter
+//
+template <class T>
+struct CoTaskMemFreer
+{
+ using pointer = T;
+
+ void operator()(T p)
+ {
+ ::CoTaskMemFree(p);
+ }
+};
+
+template <class T>
+using CoTaskMemPtr = std::unique_ptr<T, CoTaskMemFreer<T>>;
+
+
// creates a console in the constructor and destroys it in the destructor,
// also redirects standard streams
//
diff --git a/src/envmodule.cpp b/src/envmodule.cpp
index 5be52de6..2ff5027d 100644
--- a/src/envmodule.cpp
+++ b/src/envmodule.cpp
@@ -8,6 +8,12 @@ namespace env
using namespace MOBase;
+// the rationale for logging md5 was to make sure the various files were the
+// same as in the released version; this turned out to be of dubious interest,
+// while adding to the startup time
+constexpr bool UseMD5 = false;
+
+
Module::Module(QString path, std::size_t fileSize)
: m_path(std::move(path)), m_fileSize(fileSize)
{
@@ -16,7 +22,10 @@ Module::Module(QString path, std::size_t fileSize)
m_version = getVersion(fi.ffi);
m_timestamp = getTimestamp(fi.ffi);
m_versionString = fi.fileDescription;
- m_md5 = getMD5();
+
+ if (UseMD5) {
+ m_md5 = getMD5();
+ }
}
const QString& Module::path() const
diff --git a/src/envshell.cpp b/src/envshell.cpp
new file mode 100644
index 00000000..33ec0624
--- /dev/null
+++ b/src/envshell.cpp
@@ -0,0 +1,676 @@
+#include "envshell.h"
+#include <log.h>
+#include <utility.h>
+#include <windowsx.h>
+
+namespace env
+{
+
+using namespace MOBase;
+
+const int QCM_FIRST = 1;
+const int QCM_LAST = 0x7ff;
+
+class MenuFailed : public std::runtime_error
+{
+public:
+ MenuFailed(HRESULT r, const std::string& what)
+ : runtime_error(fmt::format(
+ "{}, {}",
+ what, QString::fromStdWString(formatSystemMessage(r)).toStdString()))
+ {
+ }
+};
+
+
+class DummyMenu
+{
+public:
+ DummyMenu(QString s)
+ : m_what(s)
+ {
+ }
+
+ const QString& what() const
+ {
+ return m_what;
+ }
+
+private:
+ QString m_what;
+};
+
+
+struct IdlsFreer
+{
+ const std::vector<LPCITEMIDLIST>& v;
+
+ IdlsFreer(const std::vector<LPCITEMIDLIST>& v)
+ : v(v)
+ {
+ }
+
+ ~IdlsFreer()
+ {
+ for (auto&& idl : v) {
+ ::CoTaskMemFree(const_cast<LPITEMIDLIST>(idl));
+ }
+ }
+};
+
+
+class WndProcFilter : public QAbstractNativeEventFilter
+{
+public:
+ using function_type = std::function<
+ bool (HWND hwnd, UINT m, WPARAM wp, LPARAM lp, LRESULT* out)>;
+
+ WndProcFilter(function_type f)
+ : m_f(std::move(f))
+ {
+ }
+
+ bool nativeEventFilter(const QByteArray& type, void* m, long* lresultOut) override
+ {
+ MSG* msg = (MSG*)m;
+ if (!msg) {
+ return false;
+ }
+
+ LRESULT lr = 0;
+
+ const bool r = m_f(msg->hwnd, msg->message, msg->wParam, msg->lParam, &lr);
+
+ if (lresultOut) {
+ *lresultOut = lr;
+ }
+
+ return r;
+ }
+
+private:
+ function_type m_f;
+};
+
+
+
+
+HWND getHWND(QMainWindow* mw)
+{
+ if (mw) {
+ return (HWND)mw->winId();
+ } else {
+ return 0;
+ }
+}
+
+// adapted from
+// https://devblogs.microsoft.com/oldnewthing/20040928-00/?p=37723
+//
+HRESULT IContextMenu_GetCommandString(
+ IContextMenu *pcm, UINT_PTR idCmd, UINT uFlags,
+ UINT *pwReserved, LPWSTR pszName, UINT cchMax)
+{
+ // Callers are expected to be using Unicode.
+ if (!(uFlags & GCS_UNICODE)) {
+ return E_INVALIDARG;
+ }
+
+ // Some context menu handlers have off-by-one bugs and will
+ // overflow the output buffer. Let’s artificially reduce the
+ // buffer size so a one-character overflow won’t corrupt memory.
+ if (cchMax <= 1) {
+ return E_FAIL;
+ }
+
+ cchMax--;
+
+ // First try the Unicode message. Preset the output buffer
+ // with a known value because some handlers return S_OK without
+ // doing anything.
+ pszName[0] = L'\0';
+
+ HRESULT hr = pcm->GetCommandString(
+ idCmd, uFlags, pwReserved, (LPSTR)pszName, cchMax);
+
+ if (SUCCEEDED(hr) && pszName[0] == L'\0') {
+ // Rats, a buggy IContextMenu handler that returned success
+ // even though it failed.
+ hr = E_NOTIMPL;
+ }
+
+ if (FAILED(hr)) {
+ // try again with ANSI – pad the buffer with one extra character
+ // to compensate for context menu handlers that overflow by
+ // one character.
+ LPSTR pszAnsi = (LPSTR)LocalAlloc(
+ LMEM_FIXED, (cchMax + 1) * sizeof(CHAR));
+
+ if (pszAnsi) {
+ pszAnsi[0] = '\0';
+
+ hr = pcm->GetCommandString(
+ idCmd, uFlags & ~GCS_UNICODE, pwReserved, pszAnsi, cchMax);
+
+ if (SUCCEEDED(hr) && pszAnsi[0] == '\0') {
+ // Rats, a buggy IContextMenu handler that returned success
+ // even though it failed.
+ hr = E_NOTIMPL;
+ }
+
+ if (SUCCEEDED(hr)) {
+ if (MultiByteToWideChar(CP_ACP, 0, pszAnsi, -1, pszName, cchMax) == 0) {
+ hr = E_FAIL;
+ }
+ }
+
+ LocalFree(pszAnsi);
+
+ } else {
+ hr = E_OUTOFMEMORY;
+ }
+ }
+
+ return hr;
+}
+
+
+ShellMenu::ShellMenu(QMainWindow* mw)
+ : m_mw(mw)
+{
+}
+
+void ShellMenu::addFile(QFileInfo fi)
+{
+ m_files.emplace_back(std::move(fi));
+}
+
+int ShellMenu::fileCount() const
+{
+ return static_cast<int>(m_files.size());
+}
+
+void ShellMenu::exec(const QPoint& pos)
+{
+ HMENU menu = getMenu();
+ if (!menu) {
+ return;
+ }
+
+ try
+ {
+ const auto hwnd = getHWND(m_mw);
+
+ auto filter = std::make_unique<WndProcFilter>(
+ [&](HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out) {
+ return wndProc(h, m, wp, lp, out);
+ });
+
+ QCoreApplication::instance()->installNativeEventFilter(filter.get());
+
+ const int cmd = TrackPopupMenuEx(
+ menu, TPM_RETURNCMD, pos.x(), pos.y(), hwnd, nullptr);
+
+ if (m_mw) {
+ if (auto* sb=m_mw->statusBar()) {
+ sb->clearMessage();
+ }
+ }
+
+ if (cmd <= 0) {
+ return;
+ }
+
+ invoke(pos, cmd - QCM_FIRST);
+ }
+ catch(MenuFailed& e)
+ {
+ if (m_files.size() == 1) {
+ log::error(
+ "can't exec shell menu for '{}': {}",
+ QDir::toNativeSeparators(m_files[0].absoluteFilePath()), e.what());
+ } else {
+ log::error(
+ "can't exec shell menu for {} files: {}",
+ m_files.size(), e.what());
+ }
+ }
+}
+
+HMENU ShellMenu::getMenu()
+{
+ if (!m_menu) {
+ create();
+ }
+
+ return m_menu.get();
+}
+
+bool ShellMenu::wndProc(HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out)
+{
+ if (m == WM_MENUSELECT) {
+ HANDLE_WM_MENUSELECT(h, wp, lp, onMenuSelect);
+ return true;
+ }
+
+ if (m_cm3) {
+ const auto r = m_cm3->HandleMenuMsg2(m, wp, lp, out);
+
+ if (SUCCEEDED(r)) {
+ return true;
+ }
+ }
+
+ if (m_cm2) {
+ const auto r = m_cm2->HandleMenuMsg(m, wp, lp);
+
+ if (SUCCEEDED(r)) {
+ if (out) {
+ *out = 0;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// adapted from
+// https://devblogs.microsoft.com/oldnewthing/20040928-00/?p=37723
+//
+void ShellMenu::onMenuSelect(
+ HWND hwnd, HMENU hmenu, int item, HMENU hmenuPopup, UINT flags)
+{
+ if (m_cm && item >= QCM_FIRST && item <= QCM_LAST) {
+ WCHAR szBuf[MAX_PATH];
+
+ const auto r = IContextMenu_GetCommandString(
+ m_cm.get(), item - QCM_FIRST, GCS_HELPTEXTW, NULL, szBuf, MAX_PATH);
+
+ if (FAILED(r)) {
+ lstrcpynW(szBuf, L"No help available.", MAX_PATH);
+ }
+
+ if (m_mw) {
+ if (auto* sb=m_mw->statusBar()) {
+ sb->showMessage(QString::fromWCharArray(szBuf));
+ }
+ }
+ }
+}
+
+void ShellMenu::create()
+{
+ if (m_files.empty()) {
+ log::warn("showShellMenu(): no files given");
+ return;
+ }
+
+ try
+ {
+ auto idls = createIdls(m_files);
+
+ if (idls.empty()) {
+ log::error("no idls, can't create context menu");
+ return;
+ }
+
+ IdlsFreer freer(idls);
+
+ auto array = createItemArray(idls);
+
+ createContextMenu(array.get());
+ createPopupMenu(m_cm.get());
+ }
+ catch(DummyMenu& dm)
+ {
+ m_menu = createDummyMenu(dm.what());
+ }
+ catch(MenuFailed& e)
+ {
+ if (m_files.size() == 1) {
+ log::error(
+ "can't create shell menu for '{}': {}",
+ QDir::toNativeSeparators(m_files[0].absoluteFilePath()), e.what());
+ } else {
+ log::error(
+ "can't create shell menu for {} files: {}",
+ m_files.size(), e.what());
+ }
+
+ m_menu = createDummyMenu(QObject::tr("No menu available"));
+ }
+}
+
+HMenuPtr ShellMenu::createDummyMenu(const QString& what)
+{
+ try
+ {
+ HMENU menu = CreatePopupMenu();
+ if (!menu) {
+ const auto e = GetLastError();
+ throw MenuFailed(e, "CreatePopupMenu failed");
+ }
+
+ if (!AppendMenuW(menu, MF_STRING | MF_DISABLED, 0, what.toStdWString().c_str())) {
+ const auto e = GetLastError();
+ throw MenuFailed(e, "AppendMenuW failed");
+ }
+
+ return HMenuPtr(menu);
+ }
+ catch(MenuFailed& e)
+ {
+ log::error("{}", what);
+ log::error("additionally, creating the dummy menu failed: {}", e.what());
+
+ return {};
+ }
+}
+
+std::vector<LPCITEMIDLIST> ShellMenu::createIdls(
+ const std::vector<QFileInfo>& files)
+{
+ std::vector<LPCITEMIDLIST> idls;
+ std::optional<QDir> parent;
+
+ for (auto&& f : files) {
+ const auto path = QDir::toNativeSeparators(f.absoluteFilePath()).toStdWString();
+
+ if (!parent) {
+ parent = f.absoluteDir();
+ } else {
+ if (*parent != f.absoluteDir()) {
+ throw DummyMenu(QObject::tr(
+ "Selected files must be in the same directory"));
+ }
+ }
+
+ auto item = createShellItem(path);
+ auto pidlist = getPersistIDList(item.get());
+ auto absIdl = getIDList(pidlist.get());
+
+ idls.push_back(absIdl.release());
+ }
+
+ return idls;
+}
+
+COMPtr<IShellItemArray> ShellMenu::createItemArray(
+ std::vector<LPCITEMIDLIST>& idls)
+{
+ IShellItemArray* array = nullptr;
+ auto r = SHCreateShellItemArrayFromIDLists(
+ static_cast<UINT>(idls.size()), &idls[0], &array);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, "SHCreateShellItemArrayFromIDLists failed");
+ }
+
+ return COMPtr<IShellItemArray>(array);
+}
+
+void ShellMenu::createContextMenu(IShellItemArray* array)
+{
+ IContextMenu* cm = nullptr;
+
+ auto r = array->BindToHandler(
+ nullptr, BHID_SFUIObject, IID_IContextMenu, (void**)&cm);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, "BindToHandler failed");
+ }
+
+ m_cm.reset(cm);
+
+ {
+ IContextMenu2* cm2 = nullptr;
+ if (SUCCEEDED(m_cm->QueryInterface(IID_IContextMenu2, (void**)&cm2))) {
+ m_cm2.reset(cm2);
+ }
+ }
+
+ {
+ IContextMenu3* cm3 = nullptr;
+ if (SUCCEEDED(m_cm->QueryInterface(IID_IContextMenu3, (void**)&cm3))) {
+ m_cm3.reset(cm3);
+ }
+ }
+}
+
+void ShellMenu::createPopupMenu(IContextMenu* cm)
+{
+ HMENU hmenu = CreatePopupMenu();
+ if (!hmenu) {
+ const auto e = GetLastError();
+ throw MenuFailed(e, "CreatePopupMenu failed");
+ }
+
+ const auto r = cm->QueryContextMenu(
+ hmenu, 0, QCM_FIRST, QCM_LAST, CMF_EXTENDEDVERBS);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, "QueryContextMenu failed");
+ }
+
+ m_menu.reset(hmenu);
+}
+
+COMPtr<IShellItem> ShellMenu::createShellItem(const std::wstring& path)
+{
+ IShellItem* item = nullptr;
+
+ auto r = SHCreateItemFromParsingName(
+ path.c_str(), nullptr, IID_IShellItem, (void**)&item);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, "SHCreateItemFromParsingName failed");
+ }
+
+ return COMPtr<IShellItem>(item);
+}
+
+COMPtr<IPersistIDList> ShellMenu::getPersistIDList(IShellItem* item)
+{
+ IPersistIDList* idl = nullptr;
+ auto r = item->QueryInterface(IID_IPersistIDList, (void**)&idl);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, "QueryInterface IID_IPersistIDList failed");
+ }
+
+ return COMPtr<IPersistIDList>(idl);
+}
+
+CoTaskMemPtr<LPITEMIDLIST> ShellMenu::getIDList(IPersistIDList* pidlist)
+{
+ LPITEMIDLIST absIdl = nullptr;
+ auto r = pidlist->GetIDList(&absIdl);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, "GetIDList failed");
+ }
+
+ return CoTaskMemPtr<LPITEMIDLIST>(absIdl);
+}
+
+void ShellMenu::invoke(const QPoint& p, int cmd)
+{
+ const auto hwnd = getHWND(m_mw);
+
+ CMINVOKECOMMANDINFOEX info = {};
+
+ info.cbSize = sizeof(info);
+ info.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
+ info.hwnd = hwnd;
+ info.lpVerb = MAKEINTRESOURCEA(cmd);
+ info.lpVerbW = MAKEINTRESOURCEW(cmd);
+ info.nShow = SW_SHOWNORMAL;
+ info.ptInvoke = {p.x(), p.y()};
+
+ // note: this calls the query version because the Qt even loop hasn't run
+ // yet and shift is still considered pressed
+ const auto m = QApplication::queryKeyboardModifiers();
+
+ if (m & Qt::ShiftModifier) {
+ info.fMask |= CMIC_MASK_SHIFT_DOWN;
+ }
+
+ if (m & Qt::ControlModifier) {
+ info.fMask |= CMIC_MASK_CONTROL_DOWN;
+ }
+
+ const auto r = m_cm->InvokeCommand((CMINVOKECOMMANDINFO*)&info);
+
+ if (FAILED(r)) {
+ throw MenuFailed(r, fmt::format("InvokeCommand failed, verb={}", cmd));
+ }
+}
+
+
+ShellMenuCollection::ShellMenuCollection(QMainWindow* mw)
+ : m_mw(mw), m_active(nullptr)
+{
+}
+
+void ShellMenuCollection::addDetails(QString s)
+{
+ m_details.emplace_back(std::move(s));
+}
+
+void ShellMenuCollection::add(QString name, ShellMenu m)
+{
+ m_menus.push_back({name, std::move(m)});
+}
+
+void ShellMenuCollection::exec(const QPoint& pos)
+{
+ HMENU menu = ::CreatePopupMenu();
+ if (!menu) {
+ const auto e = GetLastError();
+
+ log::error(
+ "CreatePopupMenu for merged menus failed, {}",
+ formatSystemMessage(e));
+
+ return;
+ }
+
+ if (!m_details.empty()) {
+ for (auto&& d : m_details) {
+ const auto s = d.toStdWString();
+ const auto r = AppendMenuW(menu, MF_STRING|MF_DISABLED, 0, s.c_str());
+
+ if (!r) {
+ const auto e = GetLastError();
+ log::error(
+ "AppendMenuW failed for details '{}', {}",
+ d, formatSystemMessage(e));
+ }
+ }
+
+ const auto r = AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
+ if (!r) {
+ const auto e = GetLastError();
+ log::error(
+ "AppendMenuW failed for separator, {}",
+ formatSystemMessage(e));
+ }
+ }
+
+ for (auto&& m : m_menus) {
+ auto hmenu = m.menu.getMenu();
+ if (!hmenu) {
+ continue;
+ }
+
+ const auto r = AppendMenuW(
+ menu, MF_POPUP | MF_STRING,
+ reinterpret_cast<UINT_PTR>(hmenu), m.name.toStdWString().c_str());
+
+ if (!r) {
+ const auto e = GetLastError();
+
+ log::error(
+ "AppendMenuW failed for merged menu {}, {}",
+ m.name, formatSystemMessage(e));
+
+ continue;
+ }
+ }
+
+ auto hwnd = getHWND(m_mw);
+
+ auto filter = std::make_unique<WndProcFilter>(
+ [&](HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out) {
+ return wndProc(h, m, wp, lp, out);
+ });
+
+ QCoreApplication::instance()->installNativeEventFilter(filter.get());
+
+ const int cmd = TrackPopupMenuEx(
+ menu, TPM_RETURNCMD, pos.x(), pos.y(), hwnd, nullptr);
+
+ if (m_mw) {
+ if (auto* sb=m_mw->statusBar()) {
+ sb->clearMessage();
+ }
+ }
+
+ if (cmd <= 0) {
+ return;
+ }
+
+ if (!m_active) {
+ log::debug("SMC: command {} selected without active submenu");
+ return;
+ }
+
+ const auto realCmd = cmd - QCM_FIRST;
+
+ log::debug("SMC: invoking {} on {}", realCmd, m_active->name);
+ m_active->menu.invoke(pos, realCmd);
+}
+
+bool ShellMenuCollection::wndProc(
+ HWND h, UINT m, WPARAM wp, LPARAM lp, LRESULT* out)
+{
+ if (m == WM_MENUSELECT) {
+ auto* oldActive = m_active;
+ m_active = nullptr;
+
+ HANDLE_WM_MENUSELECT(h, wp, lp, onMenuSelect);
+
+ if (!m_active && oldActive) {
+ // this was not a top level, forward to active
+ m_active = oldActive;
+ } else if (m_active && m_active == oldActive) {
+ // same top level menu was selected twice, ignore
+ return true;
+ } else if (m_active && m_active != oldActive) {
+ // new top level selected
+ log::debug("SMC: switching to {}", m_active->name);
+ }
+ }
+
+ if (!m_active) {
+ // no active menu, forward it to the default handler
+ return false;
+ }
+
+ return m_active->menu.wndProc(h, m, wp, lp, out);
+}
+
+void ShellMenuCollection::onMenuSelect(
+ HWND hwnd, HMENU hmenu, int item, HMENU hmenuPopup, UINT flags)
+{
+ for (auto&& m : m_menus) {
+ if (m.menu.getMenu() == hmenuPopup) {
+ m_active = &m;
+ break;
+ }
+ }
+}
+
+} // namespace
diff --git a/src/envshell.h b/src/envshell.h
new file mode 100644
index 00000000..f9245a37
--- /dev/null
+++ b/src/envshell.h
@@ -0,0 +1,86 @@
+#ifndef ENV_SHELL_H
+#define ENV_SHELL_H
+
+#include "env.h"
+#include <QFileInfo>
+#include <QPoint>
+
+namespace env
+{
+
+class ShellMenu
+{
+public:
+ ShellMenu(QMainWindow* mw);
+
+ // noncopyable
+ ShellMenu(const ShellMenu&) = delete;
+ ShellMenu& operator=(const ShellMenu&) = delete;
+ ShellMenu(ShellMenu&&) = default;
+ ShellMenu& operator=(ShellMenu&&) = default;
+
+ void addFile(QFileInfo fi);
+ int fileCount() const;
+
+ void exec(const QPoint& pos);
+ HMENU getMenu();
+ bool wndProc(HWND hwnd, UINT m, WPARAM wp, LPARAM lp, LRESULT* out);
+ void invoke(const QPoint& p, int cmd);
+
+private:
+ QMainWindow* m_mw;
+ std::vector<QFileInfo> m_files;
+ COMPtr<IContextMenu> m_cm;
+ COMPtr<IContextMenu2> m_cm2;
+ COMPtr<IContextMenu3> m_cm3;
+ HMenuPtr m_menu;
+
+ void create();
+
+ std::vector<LPCITEMIDLIST> createIdls(const std::vector<QFileInfo>& files);
+ COMPtr<IShellItemArray> createItemArray(std::vector<LPCITEMIDLIST>& idls);
+
+ void createContextMenu(IShellItemArray* array);
+ void createPopupMenu(IContextMenu* cm);
+
+ COMPtr<IShellItem> createShellItem(const std::wstring& path);
+ COMPtr<IPersistIDList> getPersistIDList(IShellItem* item);
+ CoTaskMemPtr<LPITEMIDLIST> getIDList(IPersistIDList* pidlist);
+ HMenuPtr createDummyMenu(const QString& what);
+
+ void onMenuSelect(
+ HWND hwnd, HMENU hmenu, int item, HMENU hmenuPopup, UINT flags);
+};
+
+
+class ShellMenuCollection
+{
+public:
+ ShellMenuCollection(QMainWindow* mw);
+
+ void addDetails(QString s);
+ void add(QString name, ShellMenu m);
+
+ void exec(const QPoint& pos);
+
+private:
+ struct MenuInfo
+ {
+ QString name;
+ ShellMenu menu;
+ };
+
+ QMainWindow* m_mw;
+ std::vector<QString> m_details;
+ std::vector<MenuInfo> m_menus;
+ MenuInfo* m_active;
+
+ bool wndProc(HWND hwnd, UINT m, WPARAM wp, LPARAM lp, LRESULT* out);
+
+ void onMenuSelect(
+ HWND hwnd, HMENU hmenu, int item, HMENU hmenuPopup, UINT flags);
+};
+
+} // namespace
+
+#endif // ENV_SHELL_H
diff --git a/src/filetree.cpp b/src/filetree.cpp
new file mode 100644
index 00000000..19f2555a
--- /dev/null
+++ b/src/filetree.cpp
@@ -0,0 +1,852 @@
+#include "filetree.h"
+#include "filetreemodel.h"
+#include "filetreeitem.h"
+#include "organizercore.h"
+#include "envshell.h"
+#include <log.h>
+
+using namespace MOShared;
+using namespace MOBase;
+
+
+bool canPreviewFile(const PluginContainer& pc, const FileEntry& file)
+{
+ return canPreviewFile(
+ pc, file.isFromArchive(), QString::fromStdWString(file.getName()));
+}
+
+bool canRunFile(const FileEntry& file)
+{
+ return canRunFile(file.isFromArchive(), QString::fromStdWString(file.getName()));
+}
+
+bool canOpenFile(const FileEntry& file)
+{
+ return canOpenFile(file.isFromArchive(), QString::fromStdWString(file.getName()));
+}
+
+bool isHidden(const FileEntry& file)
+{
+ return (QString::fromStdWString(file.getName()).endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive));
+}
+
+bool canExploreFile(const FileEntry& file);
+bool canHideFile(const FileEntry& file);
+bool canUnhideFile(const FileEntry& file);
+
+
+class MenuItem
+{
+public:
+ MenuItem(QString s={})
+ : m_action(new QAction(std::move(s)))
+ {
+ }
+
+ MenuItem& caption(const QString& s)
+ {
+ m_action->setText(s);
+ return *this;
+ }
+
+ template <class F>
+ MenuItem& callback(F&& f)
+ {
+ QObject::connect(m_action, &QAction::triggered, std::forward<F>(f));
+ return *this;
+ }
+
+ MenuItem& hint(const QString& s)
+ {
+ m_tooltip = s;
+ return *this;
+ }
+
+ MenuItem& disabledHint(const QString& s)
+ {
+ m_disabledHint = s;
+ return *this;
+ }
+
+ MenuItem& enabled(bool b)
+ {
+ m_action->setEnabled(b);
+ return *this;
+ }
+
+ void addTo(QMenu& menu)
+ {
+ QString s;
+
+ setTips();
+
+ m_action->setParent(&menu);
+ menu.addAction(m_action);
+ }
+
+private:
+ QAction* m_action;
+ QString m_tooltip;
+ QString m_disabledHint;
+
+ void setTips()
+ {
+ if (m_action->isEnabled() || m_disabledHint.isEmpty()) {
+ m_action->setStatusTip(m_tooltip);
+ return;
+ }
+
+ QString s = m_tooltip.trimmed();
+
+ if (!s.isEmpty()) {
+ if (!s.endsWith(".")) {
+ s += ".";
+ }
+
+ s += "\n";
+ }
+
+ s += QObject::tr("Disabled because") + ": " + m_disabledHint.trimmed();
+
+ if (!s.endsWith(".")) {
+ s += ".";
+ }
+
+ m_action->setStatusTip(s);
+ }
+};
+
+
+FileTree::FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree)
+ : m_core(core), m_plugins(pc), m_tree(tree), m_model(new FileTreeModel(core))
+{
+ m_tree->sortByColumn(0, Qt::AscendingOrder);
+ m_tree->setModel(m_model);
+ m_tree->header()->resizeSection(0, 200);
+
+ connect(
+ m_tree, &QTreeView::customContextMenuRequested,
+ [&](auto pos){ onContextMenu(pos); });
+
+ connect(
+ m_tree, &QTreeView::expanded,
+ [&](auto&& index){ onExpandedChanged(index, true); });
+
+ connect(
+ m_tree, &QTreeView::collapsed,
+ [&](auto&& index){ onExpandedChanged(index, false); });
+
+ connect(
+ m_tree, &QTreeView::activated,
+ [&](auto&& index){ onItemActivated(index); });
+
+}
+
+FileTreeModel* FileTree::model()
+{
+ return m_model;
+}
+
+void FileTree::refresh()
+{
+ m_model->refresh();
+}
+
+void FileTree::clear()
+{
+ m_model->clear();
+}
+
+bool FileTree::fullyLoaded() const
+{
+ return m_model->fullyLoaded();
+}
+
+void FileTree::ensureFullyLoaded()
+{
+ m_model->ensureFullyLoaded();
+}
+
+FileTreeItem* FileTree::singleSelection()
+{
+ const auto sel = m_tree->selectionModel()->selectedRows();
+ if (sel.size() == 1) {
+ return m_model->itemFromIndex(proxiedIndex(sel[0]));
+ }
+
+ return nullptr;
+}
+
+void FileTree::open(FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ if (item->isFromArchive() || item->isDirectory()) {
+ return;
+ }
+
+ const QString path = item->realPath();
+ const QFileInfo targetInfo(path);
+
+ m_core.processRunner()
+ .setFromFile(m_tree->window(), targetInfo)
+ .setHooked(false)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
+}
+
+void FileTree::openHooked(FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ if (item->isFromArchive() || item->isDirectory()) {
+ return;
+ }
+
+ const QString path = item->realPath();
+ const QFileInfo targetInfo(path);
+
+ m_core.processRunner()
+ .setFromFile(m_tree->window(), targetInfo)
+ .setHooked(true)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
+}
+
+void FileTree::preview(FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ const QString path = item->dataRelativeFilePath();
+ m_core.previewFileWithAlternatives(m_tree->window(), path);
+}
+
+void FileTree::activate(FileTreeItem* item)
+{
+ if (item->isDirectory()) {
+ // activating a directory should just toggle expansion
+ return;
+ }
+
+ if (item->isFromArchive()) {
+ log::warn("cannot activate file from archive '{}'", item->filename());
+ return;
+ }
+
+ const auto tryPreview =
+ m_core.settings().interface().doubleClicksOpenPreviews();
+
+ if (tryPreview) {
+ const QFileInfo fi(item->realPath());
+ if (m_plugins.previewGenerator().previewSupported(fi.suffix())) {
+ preview(item);
+ return;
+ }
+ }
+
+ open(item);
+}
+
+void FileTree::addAsExecutable(FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ const QString path = item->realPath();
+ const QFileInfo target(path);
+ const auto fec = spawn::getFileExecutionContext(m_tree->window(), target);
+
+ switch (fec.type)
+ {
+ case spawn::FileExecutionTypes::Executable:
+ {
+ const QString name = QInputDialog::getText(
+ m_tree->window(), tr("Enter Name"),
+ tr("Enter a name for the executable"), QLineEdit::Normal,
+ target.completeBaseName());
+
+ if (!name.isEmpty()) {
+ //Note: If this already exists, you'll lose custom settings
+ m_core.executablesList()->setExecutable(Executable()
+ .title(name)
+ .binaryInfo(fec.binary)
+ .arguments(fec.arguments)
+ .workingDirectory(target.absolutePath()));
+
+ emit executablesChanged();
+ }
+
+ break;
+ }
+
+ case spawn::FileExecutionTypes::Other: // fall-through
+ default:
+ {
+ QMessageBox::information(
+ m_tree->window(), tr("Not an executable"),
+ tr("This is not a recognized executable."));
+
+ break;
+ }
+ }
+}
+
+void FileTree::exploreOrigin(FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ if (item->isFromArchive() || item->isDirectory()) {
+ return;
+ }
+
+ const auto path = item->realPath();
+
+ log::debug("opening in explorer: {}", path);
+ shell::Explore(path);
+}
+
+void FileTree::openModInfo(FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ const auto originID = item->originID();
+
+ if (originID == 0) {
+ // unmanaged
+ return;
+ }
+
+ const auto& origin = m_core.directoryStructure()->getOriginByID(originID);
+ const auto& name = QString::fromStdWString(origin.getName());
+
+ unsigned int index = ModInfo::getIndex(name);
+ if (index == UINT_MAX) {
+ log::error("can't open mod info, mod '{}' not found", name);
+ return;
+ }
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ if (modInfo) {
+ emit displayModInformation(modInfo, index, ModInfoTabIDs::None);
+ }
+}
+
+void FileTree::toggleVisibility(bool visible, FileTreeItem* item)
+{
+ if (!item) {
+ item = singleSelection();
+
+ if (!item) {
+ return;
+ }
+ }
+
+ const QString currentName = item->realPath();
+ QString newName;
+
+ if (visible) {
+ if (!currentName.endsWith(ModInfo::s_HiddenExt)) {
+ log::error(
+ "cannot unhide '{}', doesn't end with '{}'",
+ currentName, ModInfo::s_HiddenExt);
+
+ return;
+ }
+
+ newName = currentName.left(currentName.size() - ModInfo::s_HiddenExt.size());
+ } else {
+ if (currentName.endsWith(ModInfo::s_HiddenExt)) {
+ log::error(
+ "cannot hide '{}', already ends with '{}'",
+ currentName, ModInfo::s_HiddenExt);
+
+ return;
+ }
+
+ newName = currentName + ModInfo::s_HiddenExt;
+ }
+
+ log::debug("attempting to rename '{}' to '{}'", currentName, newName);
+
+ FileRenamer renamer(
+ m_tree->window(),
+ (visible ? FileRenamer::UNHIDE : FileRenamer::HIDE));
+
+ if (renamer.rename(currentName, newName) == FileRenamer::RESULT_OK) {
+ emit originModified(item->originID());
+ refresh();
+ }
+}
+
+void FileTree::hide(FileTreeItem* item)
+{
+ toggleVisibility(false, item);
+}
+
+void FileTree::unhide(FileTreeItem* item)
+{
+ toggleVisibility(true, item);
+}
+
+class DumpFailed {};
+
+void FileTree::dumpToFile() const
+{
+ log::debug("dumping filetree to file");
+
+ QString file = QFileDialog::getSaveFileName(m_tree->window());
+ if (file.isEmpty()) {
+ log::debug("user canceled");
+ return;
+ }
+
+ QFile out(file);
+
+ if (!out.open(QIODevice::WriteOnly)) {
+ QMessageBox::critical(
+ m_tree->window(), tr("Error"), tr("Failed to open file '%1': %2")
+ .arg(file)
+ .arg(out.errorString()));
+
+ return;
+ }
+
+ try
+ {
+ dumpToFile(out, "Data", *m_core.directoryStructure());
+ }
+ catch(DumpFailed&)
+ {
+ // try to remove it silently
+ if (out.exists()) {
+ if (!out.remove()) {
+ log::error("failed to remove '{}', ignoring", file);
+ }
+ }
+ }
+}
+
+void FileTree::dumpToFile(
+ QFile& out, const QString& parentPath, const DirectoryEntry& entry) const
+{
+ entry.forEachFile([&](auto&& file) {
+ bool isArchive = false;
+ const int originID = file.getOrigin(isArchive);
+
+ if (isArchive) {
+ // TODO: don't list files from archives. maybe make this an option?
+ return true;
+ }
+
+ const auto& origin = m_core.directoryStructure()->getOriginByID(originID);
+ const auto originName = QString::fromStdWString(origin.getName());
+
+ const QString path =
+ parentPath + "\\" + QString::fromStdWString(file.getName());
+
+ if (out.write(path.toUtf8() + "\t(" + originName.toUtf8() + ")\r\n") == -1) {
+ QMessageBox::critical(
+ m_tree->window(), tr("Error"), tr("Failed to write to file %1: %2")
+ .arg(out.fileName())
+ .arg(out.errorString()));
+
+ throw DumpFailed();
+ }
+
+ return true;
+ });
+
+ entry.forEachDirectory([&](auto&& dir) {
+ const auto newParentPath =
+ parentPath + "\\" + QString::fromStdWString(dir.getName());
+
+ dumpToFile(out, newParentPath, dir);
+ return true;
+ });
+}
+
+void FileTree::onExpandedChanged(const QModelIndex& index, bool expanded)
+{
+ if (auto* item=m_model->itemFromIndex(proxiedIndex(index))) {
+ item->setExpanded(expanded);
+ }
+}
+
+void FileTree::onItemActivated(const QModelIndex& index)
+{
+ auto* item = m_model->itemFromIndex(proxiedIndex(index));
+ if (!item) {
+ return;
+ }
+
+ activate(item);
+}
+
+void FileTree::onContextMenu(const QPoint &pos)
+{
+ const auto m = QApplication::keyboardModifiers();
+
+ if (m & Qt::ShiftModifier) {
+ // if no shell menu was available, continue on and show the regular
+ // context menu
+ if (showShellMenu(pos)) {
+ return;
+ }
+ }
+
+ QMenu menu;
+
+ if (auto* item=singleSelection()) {
+ if (item->isDirectory()) {
+ addDirectoryMenus(menu, *item);
+ } else {
+ const auto file = m_core.directoryStructure()->searchFile(
+ item->dataRelativeFilePath().toStdWString(), nullptr);
+
+ if (file) {
+ addFileMenus(menu, *file, item->originID());
+ }
+ }
+ }
+
+ addCommonMenus(menu);
+
+ menu.exec(m_tree->viewport()->mapToGlobal(pos));
+}
+
+QMainWindow* getMainWindow(QWidget* w)
+{
+ QWidget* p = w;
+
+ while (p) {
+ if (auto* mw=dynamic_cast<QMainWindow*>(p)) {
+ return mw;
+ }
+
+ p = p->parentWidget();
+ }
+
+ return nullptr;
+}
+
+bool FileTree::showShellMenu(QPoint pos)
+{
+ auto* mw = getMainWindow(m_tree);
+
+ // menus by origin
+ std::map<int, env::ShellMenu> menus;
+ int totalFiles = 0;
+ bool warnOnEmpty = true;
+
+ for (auto&& index : m_tree->selectionModel()->selectedRows()) {
+ auto* item = m_model->itemFromIndex(proxiedIndex(index));
+ if (!item) {
+ continue;
+ }
+
+ if (item->isDirectory()) {
+ warnOnEmpty = false;
+
+ log::warn(
+ "directories do not have shell menus; '{}' selected",
+ item->filename());
+
+ continue;
+ }
+
+ if (item->isFromArchive()) {
+ warnOnEmpty = false;
+
+ log::warn(
+ "files from archives do not have shell menus; '{}' selected",
+ item->filename());
+
+ continue;
+ }
+
+ auto itor = menus.find(item->originID());
+ if (itor == menus.end()) {
+ itor = menus.emplace(item->originID(), mw).first;
+ }
+
+ itor->second.addFile(item->realPath());
+ ++totalFiles;
+
+ if (item->isConflicted()) {
+ const auto file = m_core.directoryStructure()->searchFile(
+ item->dataRelativeFilePath().toStdWString(), nullptr);
+
+ if (!file) {
+ log::error(
+ "file '{}' not found, data path={}, real path={}",
+ item->filename(), item->dataRelativeFilePath(), item->realPath());
+
+ continue;
+ }
+
+ const auto alts = file->getAlternatives();
+ if (alts.empty()) {
+ log::warn(
+ "file '{}' has no alternative origins but is marked as conflicted",
+ item->dataRelativeFilePath());
+ }
+
+ for (auto&& alt : alts) {
+ auto itor = menus.find(alt.first);
+ if (itor == menus.end()) {
+ itor = menus.emplace(alt.first, mw).first;
+ }
+
+ const auto fullPath = file->getFullPath(alt.first);
+ if (fullPath.empty()) {
+ log::error(
+ "file {} not found in origin {}",
+ item->dataRelativeFilePath(), alt.first);
+
+ continue;
+ }
+
+ itor->second.addFile(QString::fromStdWString(fullPath));
+ }
+ }
+ }
+
+ if (menus.empty()) {
+ // don't warn if something that doesn't have a shell menu was selected, a
+ // warning has already been logged above
+ if (warnOnEmpty) {
+ log::warn("no menus to show");
+ }
+
+ return false;
+ }
+ else if (menus.size() == 1) {
+ auto& menu = menus.begin()->second;
+ menu.exec(m_tree->viewport()->mapToGlobal(pos));
+ } else {
+ env::ShellMenuCollection mc(mw);
+ bool hasDiscrepancies = false;
+
+ for (auto&& m : menus) {
+ const auto* origin = m_core.directoryStructure()->findOriginByID(m.first);
+ if (!origin) {
+ log::error("origin {} not found for merged menus", m.first);
+ continue;
+ }
+
+ QString caption = QString::fromStdWString(origin->getName());
+ if (m.second.fileCount() < totalFiles) {
+ const auto d = m.second.fileCount();
+ caption += " " + tr("(only has %1 file(s))").arg(d);
+ hasDiscrepancies = true;
+ }
+
+ mc.add(caption, std::move(m.second));
+ }
+
+ if (hasDiscrepancies) {
+ mc.addDetails(tr("%1 file(s) selected").arg(totalFiles));
+ }
+
+ mc.exec(m_tree->viewport()->mapToGlobal(pos));
+ }
+
+ return true;
+}
+
+void FileTree::addDirectoryMenus(QMenu&, FileTreeItem&)
+{
+ // noop
+}
+
+void FileTree::addFileMenus(QMenu& menu, const FileEntry& file, int originID)
+{
+ using namespace spawn;
+
+ addOpenMenus(menu, file);
+
+ menu.addSeparator();
+ menu.setToolTipsVisible(true);
+
+ const QFileInfo target(QString::fromStdWString(file.getFullPath()));
+
+ MenuItem(tr("&Add as Executable"))
+ .callback([&]{ addAsExecutable(); })
+ .hint(tr("Add this file to the executables list"))
+ .disabledHint(tr("This file is not executable"))
+ .enabled(getFileExecutionType(target) == FileExecutionTypes::Executable)
+ .addTo(menu);
+
+ MenuItem(tr("E&xplore"))
+ .callback([&]{ exploreOrigin(); })
+ .hint(tr("Opens the file in Explorer"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive())
+ .addTo(menu);
+
+ MenuItem(tr("Open &Mod Info"))
+ .callback([&]{ openModInfo(); })
+ .hint(tr("Opens the Mod Info Window"))
+ .disabledHint(tr("This file is not in a managed mod"))
+ .enabled(originID != 0)
+ .addTo(menu);
+
+ if (isHidden(file)) {
+ MenuItem(tr("&Un-Hide"))
+ .callback([&]{ unhide(); })
+ .hint(tr("Un-hides the file"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive())
+ .addTo(menu);
+ } else {
+ MenuItem(tr("&Hide"))
+ .callback([&]{ hide(); })
+ .hint(tr("Hides the file"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive())
+ .addTo(menu);
+ }
+}
+
+void FileTree::addOpenMenus(QMenu& menu, const MOShared::FileEntry& file)
+{
+ using namespace spawn;
+
+ MenuItem openMenu, openHookedMenu;
+
+ const QFileInfo target(QString::fromStdWString(file.getFullPath()));
+
+ if (getFileExecutionType(target) == FileExecutionTypes::Executable) {
+ openMenu
+ .caption(tr("&Execute"))
+ .callback([&]{ open(); })
+ .hint(tr("Launches this program"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive());
+
+ openHookedMenu
+ .caption(tr("Execute with &VFS"))
+ .callback([&]{ openHooked(); })
+ .hint(tr("Launches this program hooked to the VFS"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive());
+ } else {
+ openMenu
+ .caption(tr("&Open"))
+ .callback([&]{ open(); })
+ .hint(tr("Opens this file with its default handler"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive());
+
+ openHookedMenu
+ .caption(tr("Open with &VFS"))
+ .callback([&]{ openHooked(); })
+ .hint(tr("Opens this file with its default handler hooked to the VFS"))
+ .disabledHint(tr("This file is in an archive"))
+ .enabled(!file.isFromArchive());
+ }
+
+ MenuItem previewMenu(tr("&Preview"));
+ previewMenu
+ .callback([&]{ preview(); })
+ .hint(tr("Previews this file within Mod Organizer"))
+ .disabledHint(tr(
+ "This file is in an archive or has no preview handler "
+ "associated with it"))
+ .enabled(canPreviewFile(m_plugins, file));
+
+ if (m_core.settings().interface().doubleClicksOpenPreviews()) {
+ previewMenu.addTo(menu);
+ openMenu.addTo(menu);
+ openHookedMenu.addTo(menu);
+ } else {
+ openMenu.addTo(menu);
+ previewMenu.addTo(menu);
+ openHookedMenu.addTo(menu);
+ }
+
+ // bold the first enabled option, only first three are considered
+ for (int i=0; i<3; ++i) {
+ if (i >= menu.actions().size()) {
+ break;
+ }
+
+ auto* a = menu.actions()[i];
+
+ if (menu.actions()[i]->isEnabled()) {
+ auto f = a->font();
+ f.setBold(true);
+ a->setFont(f);
+ break;
+ }
+ }
+}
+
+void FileTree::addCommonMenus(QMenu& menu)
+{
+ menu.addSeparator();
+
+ MenuItem(tr("&Save Tree to Text File..."))
+ .callback([&]{ dumpToFile(); })
+ .hint(tr("Writes the list of files to a text file"))
+ .addTo(menu);
+
+ MenuItem(tr("&Refresh"))
+ .callback([&]{ refresh(); })
+ .hint(tr("Refreshes the list"))
+ .addTo(menu);
+
+ MenuItem(tr("Ex&pand All"))
+ .callback([&]{ m_tree->expandAll(); })
+ .addTo(menu);
+
+ MenuItem(tr("&Collapse All"))
+ .callback([&]{ m_tree->collapseAll(); })
+ .addTo(menu);
+}
+
+QModelIndex FileTree::proxiedIndex(const QModelIndex& index)
+{
+ auto* model = m_tree->model();
+
+ if (auto* proxy=dynamic_cast<QSortFilterProxyModel*>(model)) {
+ return proxy->mapToSource(index);
+ } else {
+ return index;
+ }
+}
diff --git a/src/filetree.h b/src/filetree.h
new file mode 100644
index 00000000..c1458179
--- /dev/null
+++ b/src/filetree.h
@@ -0,0 +1,74 @@
+#ifndef MODORGANIZER_FILETREE_INCLUDED
+#define MODORGANIZER_FILETREE_INCLUDED
+
+#include "modinfo.h"
+#include "modinfodialogfwd.h"
+
+namespace MOShared { class FileEntry; }
+
+class OrganizerCore;
+class PluginContainer;
+class FileTreeModel;
+class FileTreeItem;
+
+class FileTree : public QObject
+{
+ Q_OBJECT;
+
+public:
+ FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree);
+
+ FileTreeModel* model();
+ void refresh();
+ void clear();
+
+ bool fullyLoaded() const;
+ void ensureFullyLoaded();
+
+ void open(FileTreeItem* item=nullptr);
+ void openHooked(FileTreeItem* item=nullptr);
+ void preview(FileTreeItem* item=nullptr);
+ void activate(FileTreeItem* item=nullptr);
+
+ void addAsExecutable(FileTreeItem* item=nullptr);
+ void exploreOrigin(FileTreeItem* item=nullptr);
+ void openModInfo(FileTreeItem* item=nullptr);
+
+ void hide(FileTreeItem* item=nullptr);
+ void unhide(FileTreeItem* item=nullptr);
+
+ void dumpToFile() const;
+
+signals:
+ void executablesChanged();
+ void originModified(int originID);
+ void displayModInformation(ModInfo::Ptr m, unsigned int i, ModInfoTabIDs tab);
+
+private:
+ OrganizerCore& m_core;
+ PluginContainer& m_plugins;
+ QTreeView* m_tree;
+ FileTreeModel* m_model;
+
+ FileTreeItem* singleSelection();
+
+ void onExpandedChanged(const QModelIndex& index, bool expanded);
+ void onItemActivated(const QModelIndex& index);
+ void onContextMenu(const QPoint &pos);
+ bool showShellMenu(QPoint pos);
+
+ void addDirectoryMenus(QMenu& menu, FileTreeItem& item);
+ void addFileMenus(QMenu& menu, const MOShared::FileEntry& file, int originID);
+ void addOpenMenus(QMenu& menu, const MOShared::FileEntry& file);
+ void addCommonMenus(QMenu& menu);
+
+ void toggleVisibility(bool b, FileTreeItem* item=nullptr);
+
+ QModelIndex proxiedIndex(const QModelIndex& index);
+
+ void dumpToFile(
+ QFile& out, const QString& parentPath,
+ const MOShared::DirectoryEntry& entry) const;
+};
+
+#endif // MODORGANIZER_FILETREE_INCLUDED
diff --git a/src/filetreeitem.cpp b/src/filetreeitem.cpp
new file mode 100644
index 00000000..e5425573
--- /dev/null
+++ b/src/filetreeitem.cpp
@@ -0,0 +1,392 @@
+#include "filetreeitem.h"
+#include "filetreemodel.h"
+#include "modinfo.h"
+#include "util.h"
+#include "modinfodialogfwd.h"
+#include <log.h>
+#include <utility.h>
+
+using namespace MOBase;
+using namespace MOShared;
+namespace fs = std::filesystem;
+
+constexpr bool AlwaysSortDirectoriesFirst = true;
+
+const QString& directoryFileType()
+{
+ static QString name;
+
+ if (name.isEmpty()) {
+ const DWORD flags = SHGFI_TYPENAME;
+ SHFILEINFOW sfi = {};
+
+ // "." for the current directory, which should always exist
+ const auto r = SHGetFileInfoW(L".", 0, &sfi, sizeof(sfi), flags);
+
+ if (!r) {
+ const auto e = GetLastError();
+
+ log::error(
+ "SHGetFileInfoW failed for folder file type, {}",
+ formatSystemMessage(e));
+
+ name = "File folder";
+ } else {
+ name = QString::fromWCharArray(sfi.szTypeName);
+ }
+ }
+
+ return name;
+}
+
+
+FileTreeItem::FileTreeItem(
+ FileTreeModel* model, FileTreeItem* parent,
+ std::wstring dataRelativeParentPath, bool isDirectory, std::wstring file) :
+ m_model(model), m_parent(parent), m_indexGuess(NoIndexGuess),
+ m_virtualParentPath(QString::fromStdWString(dataRelativeParentPath)),
+ m_wsFile(file),
+ m_wsLcFile(ToLowerCopy(file)),
+ m_key(m_wsLcFile),
+ m_file(QString::fromStdWString(file)),
+ m_isDirectory(isDirectory),
+ m_originID(-1),
+ m_flags(NoFlags),
+ m_loaded(false),
+ m_expanded(false),
+ m_sortingStale(true)
+{
+}
+
+FileTreeItem::Ptr FileTreeItem::createFile(
+ FileTreeModel* model, FileTreeItem* parent,
+ std::wstring dataRelativeParentPath, std::wstring file)
+{
+ return std::unique_ptr<FileTreeItem>(new FileTreeItem(
+ model, parent, std::move(dataRelativeParentPath), false, std::move(file)));
+}
+
+FileTreeItem::Ptr FileTreeItem::createDirectory(
+ FileTreeModel* model, FileTreeItem* parent,
+ std::wstring dataRelativeParentPath, std::wstring file)
+{
+ return std::unique_ptr<FileTreeItem>(new FileTreeItem(
+ model, parent, std::move(dataRelativeParentPath), true, std::move(file)));
+}
+
+void FileTreeItem::setOrigin(
+ int originID, const std::wstring& realPath, Flags flags,
+ const std::wstring& mod)
+{
+ m_originID = originID;
+ m_wsRealPath = realPath;
+ m_realPath = QString::fromStdWString(realPath);
+ m_flags = flags;
+ m_mod = QString::fromStdWString(mod);
+
+ m_fileSize.reset();
+ m_lastModified.reset();
+ m_fileType.reset();
+ m_compressedFileSize.reset();
+}
+
+void FileTreeItem::insert(FileTreeItem::Ptr child, std::size_t at)
+{
+ if (at > m_children.size()) {
+ log::error(
+ "{}: can't insert child {} at {}, out of range",
+ debugName(), child->debugName(), at);
+
+ return;
+ }
+
+ child->m_indexGuess = at;
+ m_children.insert(m_children.begin() + at, std::move(child));
+}
+
+void FileTreeItem::remove(std::size_t i)
+{
+ if (i >= m_children.size()) {
+ log::error("{}: can't remove child at {}", debugName(), i);
+ return;
+ }
+
+ m_children.erase(m_children.begin() + i);
+}
+
+void FileTreeItem::remove(std::size_t from, std::size_t n)
+{
+ if ((from + n) > m_children.size()) {
+ log::error("{}: can't remove children from {} n={}", debugName(), from, n);
+ return;
+ }
+
+ auto begin = m_children.begin() + from;
+ auto end = begin + n;
+
+ m_children.erase(begin, end);
+}
+
+
+template <class T>
+int threeWayCompare(T&& a, T&& b)
+{
+ if (a < b) {
+ return -1;
+ }
+
+ if (a > b) {
+ return 1;
+ }
+
+ return 0;
+}
+
+class FileTreeItem::Sorter
+{
+public:
+ static int compare(int column, const FileTreeItem* a, const FileTreeItem* b)
+ {
+ switch (column)
+ {
+ case FileTreeModel::FileName:
+ return naturalCompare(a->m_file, b->m_file);
+
+ case FileTreeModel::ModName:
+ return naturalCompare(a->m_mod, b->m_mod);
+
+ case FileTreeModel::FileType:
+ return naturalCompare(
+ a->fileType().value_or(QString()),
+ b->fileType().value_or(QString()));
+
+ case FileTreeModel::FileSize:
+ return threeWayCompare(
+ a->fileSize().value_or(0),
+ b->fileSize().value_or(0));
+
+ case FileTreeModel::LastModified:
+ return threeWayCompare(
+ a->lastModified().value_or(QDateTime()),
+ b->lastModified().value_or(QDateTime()));
+
+ default:
+ return 0;
+ }
+ }
+};
+
+void FileTreeItem::sort()
+{
+ if (!m_children.empty()) {
+ m_model->sortItem(*this, true);
+ }
+}
+
+void FileTreeItem::sort(int column, Qt::SortOrder order, bool force)
+{
+ if (!force && !m_expanded) {
+ m_sortingStale = true;
+ return;
+ }
+
+ if (m_sortingStale) {
+ //log::debug("sorting is stale for {}, sorting now", debugName());
+ m_sortingStale = false;
+ }
+
+ std::sort(m_children.begin(), m_children.end(), [&](auto&& a, auto&& b) {
+ int r = 0;
+
+ if (a->isDirectory() && !b->isDirectory()) {
+ if constexpr (AlwaysSortDirectoriesFirst) {
+ return true;
+ } else {
+ r = -1;
+ }
+ } else if (!a->isDirectory() && b->isDirectory()) {
+ if constexpr (AlwaysSortDirectoriesFirst) {
+ return false;
+ } else {
+ r = 1;
+ }
+ } else {
+ r = FileTreeItem::Sorter::compare(column, a.get(), b.get());
+ }
+
+ if (order == Qt::AscendingOrder) {
+ return (r < 0);
+ } else {
+ return (r > 0);
+ }
+ });
+
+ for (auto& child : m_children) {
+ child->sort(column, order, force);
+ }
+}
+
+QString FileTreeItem::virtualPath() const
+{
+ QString s = "Data\\";
+
+ if (!m_virtualParentPath.isEmpty()) {
+ s += m_virtualParentPath + "\\";
+ }
+
+ s += m_file;
+
+ return s;
+}
+
+QString FileTreeItem::dataRelativeFilePath() const
+{
+ auto path = dataRelativeParentPath();
+ if (!path.isEmpty()) {
+ path += "\\";
+ }
+
+ return path += m_file;
+}
+
+QFont FileTreeItem::font() const
+{
+ QFont f;
+
+ if (isFromArchive()) {
+ f.setItalic(true);
+ } else if (isHidden()) {
+ f.setStrikeOut(true);
+ }
+
+ return f;
+}
+
+std::optional<uint64_t> FileTreeItem::fileSize() const
+{
+ if (m_fileSize.empty() && !m_isDirectory) {
+ std::error_code ec;
+ const auto size = fs::file_size(fs::path(m_wsRealPath), ec);
+
+ if (ec) {
+ log::error("can't get file size for '{}', {}", m_realPath, ec.message());
+ m_fileSize.fail();
+ } else {
+ m_fileSize.set(size);
+ }
+ }
+
+ return m_fileSize.value;
+}
+
+std::optional<QDateTime> FileTreeItem::lastModified() const
+{
+ if (m_lastModified.empty()) {
+ if (m_realPath.isEmpty()) {
+ // this is a virtual directory
+ m_lastModified.set({});
+ } else if (isFromArchive()) {
+ // can't get last modified date for files in archives
+ m_lastModified.set({});
+ } else {
+ // looks like a regular file on the filesystem
+ const QFileInfo fi(m_realPath);
+ const auto d = fi.lastModified();
+
+ if (!d.isValid()) {
+ log::error("can't get last modified date for '{}'", m_realPath);
+ m_lastModified.fail();
+ } else {
+ m_lastModified.set(d);
+ }
+ }
+ }
+
+ return m_lastModified.value;
+}
+
+std::optional<QString> FileTreeItem::fileType() const
+{
+ if (m_fileType.empty()) {
+ getFileType();
+ }
+
+ return m_fileType.value;
+}
+
+void FileTreeItem::getFileType() const
+{
+ if (isDirectory()) {
+ m_fileType.set(directoryFileType());
+ return;
+ }
+
+ DWORD flags = SHGFI_TYPENAME;
+
+ if (isFromArchive()) {
+ // files from archives are not on the filesystem; this flag forces
+ // SHGetFileInfoW() to only work with the filename
+ flags |= SHGFI_USEFILEATTRIBUTES;
+ }
+
+ SHFILEINFOW sfi = {};
+ const auto r = SHGetFileInfoW(
+ m_wsRealPath.c_str(), 0, &sfi, sizeof(sfi), flags);
+
+ if (!r) {
+ const auto e = GetLastError();
+
+ log::error(
+ "SHGetFileInfoW failed for '{}', {}",
+ m_realPath, formatSystemMessage(e));
+
+ m_fileType.fail();
+ } else {
+ m_fileType.set(QString::fromWCharArray(sfi.szTypeName));
+ }
+}
+
+QFileIconProvider::IconType FileTreeItem::icon() const
+{
+ if (m_isDirectory) {
+ return QFileIconProvider::Folder;
+ } else {
+ return QFileIconProvider::File;
+ }
+}
+
+bool FileTreeItem::isHidden() const
+{
+ return m_file.endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive);
+}
+
+void FileTreeItem::unload()
+{
+ if (!m_loaded) {
+ return;
+ }
+
+ m_loaded = false;
+ m_children.clear();
+}
+
+bool FileTreeItem::areChildrenVisible() const
+{
+ if (m_expanded) {
+ if (m_parent) {
+ return m_parent->areChildrenVisible();
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+QString FileTreeItem::debugName() const
+{
+ return QString("%1(ld=%2,cs=%3)")
+ .arg(virtualPath())
+ .arg(m_loaded)
+ .arg(m_children.size());
+}
diff --git a/src/filetreeitem.h b/src/filetreeitem.h
new file mode 100644
index 00000000..390d5499
--- /dev/null
+++ b/src/filetreeitem.h
@@ -0,0 +1,320 @@
+#ifndef MODORGANIZER_FILETREEITEM_INCLUDED
+#define MODORGANIZER_FILETREEITEM_INCLUDED
+
+#include "directoryentry.h"
+#include <QFileIconProvider>
+
+class FileTreeModel;
+
+class FileTreeItem
+{
+ class Sorter;
+
+public:
+ using Ptr = std::unique_ptr<FileTreeItem>;
+ using Children = std::vector<Ptr>;
+
+ enum Flag
+ {
+ NoFlags = 0x00,
+ FromArchive = 0x01,
+ Conflicted = 0x02
+ };
+
+
+ Q_DECLARE_FLAGS(Flags, Flag);
+
+ static Ptr createFile(
+ FileTreeModel* model, FileTreeItem* parent,
+ std::wstring dataRelativeParentPath, std::wstring file);
+
+ static Ptr createDirectory(
+ FileTreeModel* model, FileTreeItem* parent,
+ std::wstring dataRelativeParentPath, std::wstring file);
+
+ FileTreeItem(const FileTreeItem&) = delete;
+ FileTreeItem& operator=(const FileTreeItem&) = delete;
+ FileTreeItem(FileTreeItem&&) = default;
+ FileTreeItem& operator=(FileTreeItem&&) = default;
+
+ void setOrigin(
+ int originID, const std::wstring& realPath,
+ Flags flags, const std::wstring& mod);
+
+ void add(Ptr child)
+ {
+ child->m_indexGuess = m_children.size();
+ m_children.push_back(std::move(child));
+ }
+
+ void insert(Ptr child, std::size_t at);
+
+ template <class Itor>
+ void insert(Itor begin, Itor end, std::size_t at)
+ {
+ std::size_t nextRowGuess = m_children.size();
+ for (auto itor=begin; itor!=end; ++itor) {
+ (*itor)->m_indexGuess = nextRowGuess++;
+ }
+
+ m_children.insert(m_children.begin() + at, begin, end);
+ }
+
+ void remove(std::size_t i);
+ void remove(std::size_t from, std::size_t n);
+
+ void clear()
+ {
+ m_children.clear();
+ m_loaded = false;
+ }
+
+ const Children& children() const
+ {
+ return m_children;
+ }
+
+ int childIndex(const FileTreeItem& item) const
+ {
+ if (item.m_indexGuess < m_children.size()) {
+ if (m_children[item.m_indexGuess].get() == &item) {
+ return static_cast<int>(item.m_indexGuess);
+ }
+ }
+
+ for (std::size_t i=0; i<m_children.size(); ++i) {
+ if (m_children[i].get() == &item) {
+ item.m_indexGuess = i;
+ return static_cast<int>(i);
+ }
+ }
+
+ return -1;
+ }
+
+ void sort(int column, Qt::SortOrder order, bool force);
+
+ FileTreeItem* parent()
+ {
+ return m_parent;
+ }
+
+ int originID() const
+ {
+ return m_originID;
+ }
+
+ const QString& virtualParentPath() const
+ {
+ return m_virtualParentPath;
+ }
+
+ QString virtualPath() const;
+
+ const QString& filename() const
+ {
+ return m_file;
+ }
+
+ const std::wstring& filenameWs() const
+ {
+ return m_wsFile;
+ }
+
+ const std::wstring& filenameWsLowerCase() const
+ {
+ return m_wsLcFile;
+ }
+
+ const MOShared::DirectoryEntry::FileKey& key() const
+ {
+ return m_key;
+ }
+
+ const QString& mod() const
+ {
+ return m_mod;
+ }
+
+ QFont font() const;
+
+ std::optional<uint64_t> fileSize() const;
+ std::optional<QDateTime> lastModified() const;
+ std::optional<QString> fileType() const;
+
+ std::optional<uint64_t> compressedFileSize() const
+ {
+ return m_compressedFileSize.value;
+ }
+
+ void setFileSize(uint64_t size)
+ {
+ m_fileSize.override(size);
+ }
+
+ void setCompressedFileSize(uint64_t compressedSize)
+ {
+ m_compressedFileSize.override(compressedSize);
+ }
+
+ const QString& realPath() const
+ {
+ return m_realPath;
+ }
+
+ const QString& dataRelativeParentPath() const
+ {
+ return m_virtualParentPath;
+ }
+
+ QString dataRelativeFilePath() const;
+
+ QFileIconProvider::IconType icon() const;
+
+ bool isDirectory() const
+ {
+ return m_isDirectory;
+ }
+
+ bool isFromArchive() const
+ {
+ return (m_flags & FromArchive);
+ }
+
+ bool isConflicted() const
+ {
+ return (m_flags & Conflicted);
+ }
+
+ bool isHidden() const;
+
+ bool hasChildren() const
+ {
+ if (!isDirectory()) {
+ return false;
+ }
+
+ if (isLoaded() && m_children.empty()) {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ void setLoaded(bool b)
+ {
+ m_loaded = b;
+ }
+
+ bool isLoaded() const
+ {
+ return m_loaded;
+ }
+
+ void unload();
+
+ void setExpanded(bool b)
+ {
+ if (m_expanded == b) {
+ return;
+ }
+
+ m_expanded = b;
+
+ if (m_expanded && m_sortingStale) {
+ sort();
+ }
+ }
+
+ bool isStrictlyExpanded() const
+ {
+ return m_expanded;
+ }
+
+ bool areChildrenVisible() const;
+
+ QString debugName() const;
+
+private:
+ template <class T>
+ struct Cached
+ {
+ std::optional<T> value;
+ bool failed = false;
+ bool overridden = false;
+
+ bool empty() const
+ {
+ return !failed && !value;
+ }
+
+ void set(T t)
+ {
+ value = std::move(t);
+ failed = false;
+ overridden = false;
+ }
+
+ void override(T t)
+ {
+ value = std::move(t);
+ failed = false;
+ overridden = true;
+ }
+
+ void fail()
+ {
+ value = {};
+ failed = true;
+ overridden = false;
+ }
+
+ void reset()
+ {
+ if (!overridden) {
+ value = {};
+ failed = false;
+ }
+ }
+ };
+
+ static constexpr std::size_t NoIndexGuess =
+ std::numeric_limits<std::size_t>::max();
+
+ FileTreeModel* m_model;
+ FileTreeItem* m_parent;
+ mutable std::size_t m_indexGuess;
+
+ const QString m_virtualParentPath;
+ const std::wstring m_wsFile, m_wsLcFile;
+ const MOShared::DirectoryEntry::FileKey m_key;
+ const QString m_file;
+ const bool m_isDirectory;
+
+ int m_originID;
+ QString m_realPath;
+ std::wstring m_wsRealPath;
+ Flags m_flags;
+ QString m_mod;
+
+ mutable Cached<uint64_t> m_fileSize;
+ mutable Cached<QDateTime> m_lastModified;
+ mutable Cached<QString> m_fileType;
+ mutable Cached<uint64_t> m_compressedFileSize;
+
+ bool m_loaded;
+ bool m_expanded;
+ bool m_sortingStale;
+ Children m_children;
+
+
+ FileTreeItem(
+ FileTreeModel* model, FileTreeItem* parent,
+ std::wstring dataRelativeParentPath, bool isDirectory, std::wstring file);
+
+ void getFileType() const;
+ void sort();
+};
+
+#endif // MODORGANIZER_FILETREEITEM_INCLUDED
diff --git a/src/filetreemodel.cpp b/src/filetreemodel.cpp
new file mode 100644
index 00000000..2130bf89
--- /dev/null
+++ b/src/filetreemodel.cpp
@@ -0,0 +1,1092 @@
+#include "filetreemodel.h"
+#include "organizercore.h"
+#include <log.h>
+
+using namespace MOBase;
+using namespace MOShared;
+
+// in mainwindow.cpp
+QString UnmanagedModName();
+
+
+#define trace(f)
+
+
+// tracks a contiguous range in the model to avoid calling begin*Rows(), etc.
+// for every single item that's added/removed
+//
+class FileTreeModel::Range
+{
+public:
+ // note that file ranges can start from an index higher than 0 if there are
+ // directories
+ //
+ Range(FileTreeModel* model, FileTreeItem& parentItem, int start=0)
+ : m_model(model), m_parentItem(parentItem), m_first(-1), m_current(start)
+ {
+ }
+
+ // includes the current index in the range
+ //
+ void includeCurrent()
+ {
+ // just remember the start of the range, m_current will be used in add()
+ // or remove() to figure out the actual range
+ if (m_first == -1) {
+ m_first = m_current;
+ }
+ }
+
+ // moves to the next row
+ //
+ void next()
+ {
+ ++m_current;
+ }
+
+ // returns the current row
+ //
+ int current() const
+ {
+ return m_current;
+ }
+
+ // adds the given items to this range
+ //
+ void add(FileTreeItem::Children toAdd)
+ {
+ if (m_first == -1) {
+ // nothing to add
+ Q_ASSERT(toAdd.empty());
+ return;
+ }
+
+ const auto last = m_current - 1;
+ const auto parentIndex = m_model->indexFromItem(m_parentItem);
+
+ // make sure the number of items is the same as the size of this range
+ Q_ASSERT(static_cast<int>(toAdd.size()) == (last - m_first + 1));
+
+ trace(log::debug("Range::add() {} to {}", m_first, last));
+
+ m_model->beginInsertRows(parentIndex, m_first, last);
+
+ m_parentItem.insert(
+ std::make_move_iterator(toAdd.begin()),
+ std::make_move_iterator(toAdd.end()),
+ static_cast<std::size_t>(m_first));
+
+ m_model->endInsertRows();
+
+ // reset
+ m_first = -1;
+ }
+
+ // removes the item in this range, returns an iterator to first item passed
+ // this range once removed, which can be end()
+ //
+ FileTreeItem::Children::const_iterator remove()
+ {
+ if (m_first >= 0) {
+ const auto last = m_current - 1;
+ const auto parentIndex = m_model->indexFromItem(m_parentItem);
+
+ trace(log::debug("Range::remove() {} to {}", m_first, last));
+
+ m_model->beginRemoveRows(parentIndex, m_first, last);
+
+ m_parentItem.remove(
+ static_cast<std::size_t>(m_first),
+ static_cast<std::size_t>(last - m_first + 1));
+
+ m_model->endRemoveRows();
+
+ m_model->removePendingIcons(parentIndex, m_first, last);
+
+ // adjust current row to account for those that were just removed
+ m_current -= (m_current - m_first);
+
+ // reset
+ m_first = -1;
+ }
+
+ if (m_current >= m_parentItem.children().size()) {
+ return m_parentItem.children().end();
+ }
+
+ return m_parentItem.children().begin() + m_current + 1;
+ }
+
+private:
+ FileTreeModel* m_model;
+ FileTreeItem& m_parentItem;
+ int m_first;
+ int m_current;
+};
+
+
+FileTreeItem* getItem(const QModelIndex& index)
+{
+ return static_cast<FileTreeItem*>(index.internalPointer());
+}
+
+void* makeInternalPointer(FileTreeItem* item)
+{
+ return item;
+}
+
+
+FileTreeModel::FileTreeModel(OrganizerCore& core, QObject* parent) :
+ QAbstractItemModel(parent), m_core(core), m_enabled(true),
+ m_root(FileTreeItem::createDirectory(this, nullptr, L"", L"")),
+ m_flags(NoFlags), m_fullyLoaded(false)
+{
+ m_root->setExpanded(true);
+
+ connect(&m_iconPendingTimer, &QTimer::timeout, [&]{ updatePendingIcons(); });
+}
+
+void FileTreeModel::refresh()
+{
+ TimeThis tt("FileTreeModel::refresh()");
+
+ m_fullyLoaded = false;
+ update(*m_root, *m_core.directoryStructure(), L"");
+}
+
+void FileTreeModel::clear()
+{
+ m_fullyLoaded = false;
+
+ beginResetModel();
+ m_root->clear();
+ endResetModel();
+}
+
+void FileTreeModel::recursiveFetchMore(const QModelIndex& m)
+{
+ if (canFetchMore(m)) {
+ fetchMore(m);
+ }
+
+ for (int i=0; i<rowCount(m); ++i) {
+ recursiveFetchMore(index(i, 0, m));
+ }
+}
+
+void FileTreeModel::ensureFullyLoaded()
+{
+ if (!m_fullyLoaded) {
+ TimeThis tt("FileTreeModel:: fully loading for search");
+ recursiveFetchMore(QModelIndex());
+ m_fullyLoaded = true;
+ }
+}
+
+bool FileTreeModel::enabled() const
+{
+ return m_enabled;
+}
+
+void FileTreeModel::setEnabled(bool b)
+{
+ m_enabled = b;
+}
+
+const FileTreeModel::SortInfo& FileTreeModel::sortInfo() const
+{
+ return m_sort;
+}
+
+bool FileTreeModel::showArchives() const
+{
+ return (m_flags.testFlag(Archives) && m_core.getArchiveParsing());
+}
+
+QModelIndex FileTreeModel::index(
+ int row, int col, const QModelIndex& parentIndex) const
+{
+ if (auto* parentItem=itemFromIndex(parentIndex)) {
+ if (row < 0 || row >= parentItem->children().size()) {
+ log::error("row {} out of range for {}", row, parentItem->debugName());
+ return {};
+ }
+
+ return createIndex(row, col, makeInternalPointer(parentItem));
+ }
+
+ log::error("FileTreeModel::index(): parentIndex has no internal pointer");
+ return {};
+}
+
+QModelIndex FileTreeModel::parent(const QModelIndex& index) const
+{
+ if (!index.isValid()) {
+ return {};
+ }
+
+ auto* parentItem = getItem(index);
+ if (!parentItem) {
+ log::error("FileTreeModel::parent(): no internal pointer");
+ return {};
+ }
+
+ return indexFromItem(*parentItem);
+}
+
+int FileTreeModel::rowCount(const QModelIndex& parent) const
+{
+ if (auto* item=itemFromIndex(parent)) {
+ return static_cast<int>(item->children().size());
+ }
+
+ return 0;
+}
+
+int FileTreeModel::columnCount(const QModelIndex&) const
+{
+ return ColumnCount;
+}
+
+bool FileTreeModel::hasChildren(const QModelIndex& parent) const
+{
+ if (auto* item=itemFromIndex(parent)) {
+ return item->hasChildren();
+ }
+
+ return false;
+}
+
+bool FileTreeModel::canFetchMore(const QModelIndex& parent) const
+{
+ if (!m_enabled) {
+ return false;
+ }
+
+ if (auto* item=itemFromIndex(parent)) {
+ return !item->isLoaded();
+ }
+
+ return false;
+}
+
+void FileTreeModel::fetchMore(const QModelIndex& parent)
+{
+ FileTreeItem* item = itemFromIndex(parent);
+ if (!item) {
+ return;
+ }
+
+ const auto path = item->dataRelativeFilePath();
+
+ auto* parentEntry = m_core.directoryStructure()
+ ->findSubDirectoryRecursive(path.toStdWString());
+
+ if (!parentEntry) {
+ log::error("FileTreeModel::fetchMore(): directory '{}' not found", path);
+ return;
+ }
+
+ const auto parentPath = item->dataRelativeParentPath();
+
+ update(*item, *parentEntry, parentPath.toStdWString());
+}
+
+QVariant FileTreeModel::data(const QModelIndex& index, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ {
+ if (auto* item=itemFromIndex(index)) {
+ return displayData(item, index.column());
+ }
+
+ break;
+ }
+
+ case Qt::FontRole:
+ {
+ if (auto* item=itemFromIndex(index)) {
+ return item->font();
+ }
+
+ break;
+ }
+
+ case Qt::ToolTipRole:
+ {
+ if (auto* item=itemFromIndex(index)) {
+ return makeTooltip(*item);
+ }
+
+ return {};
+ }
+
+ case Qt::ForegroundRole:
+ {
+ if (index.column() == 1) {
+ if (auto* item=itemFromIndex(index)) {
+ if (item->isConflicted()) {
+ return QBrush(Qt::red);
+ }
+ }
+ }
+
+ break;
+ }
+
+ case Qt::DecorationRole:
+ {
+ if (index.column() == 0) {
+ if (auto* item=itemFromIndex(index)) {
+ return makeIcon(*item, index);
+ }
+ }
+
+ break;
+ }
+ }
+
+ return {};
+}
+
+QVariant FileTreeModel::headerData(int i, Qt::Orientation ori, int role) const
+{
+ static const std::array<QString, ColumnCount> names = {
+ tr("Name"), tr("Mod"), tr("Type"), tr("Size"), tr("Date modified")
+ };
+
+ if (role == Qt::DisplayRole) {
+ if (i >= 0 && i < static_cast<int>(names.size())) {
+ return names[static_cast<std::size_t>(i)];
+ }
+ }
+
+ return {};
+}
+
+Qt::ItemFlags FileTreeModel::flags(const QModelIndex& index) const
+{
+ auto f = QAbstractItemModel::flags(index);
+
+ if (auto* item=itemFromIndex(index)) {
+ if (!item->hasChildren()) {
+ f |= Qt::ItemNeverHasChildren;
+ }
+ }
+
+ return f;
+}
+
+void FileTreeModel::sortItem(FileTreeItem& item, bool force)
+{
+ emit layoutAboutToBeChanged();
+
+ const auto oldList = persistentIndexList();
+ std::vector<std::pair<FileTreeItem*, int>> oldItems;
+
+ const auto itemCount = oldList.size();
+ oldItems.reserve(static_cast<std::size_t>(itemCount));
+
+ for (int i=0; i<itemCount; ++i) {
+ const QModelIndex& index = oldList[i];
+ oldItems.push_back({itemFromIndex(index), index.column()});
+ }
+
+ item.sort(m_sort.column, m_sort.order, force);
+
+ QModelIndexList newList;
+ newList.reserve(itemCount);
+
+ for (int i=0; i<itemCount; ++i) {
+ const auto& pair = oldItems[static_cast<std::size_t>(i)];
+ newList.append(indexFromItem(*pair.first, pair.second));
+ }
+
+ changePersistentIndexList(oldList, newList);
+
+ emit layoutChanged({}, QAbstractItemModel::VerticalSortHint);
+}
+
+void FileTreeModel::sort(int column, Qt::SortOrder order)
+{
+ m_sort.column = column;
+ m_sort.order = order;
+
+ sortItem(*m_root, false);
+}
+
+FileTreeItem* FileTreeModel::itemFromIndex(const QModelIndex& index) const
+{
+ if (!index.isValid()) {
+ return m_root.get();
+ }
+
+ auto* parentItem = getItem(index);
+ if (!parentItem) {
+ log::error("FileTreeModel::itemFromIndex(): no internal pointer");
+ return nullptr;
+ }
+
+ if (index.row() < 0 || index.row() >= parentItem->children().size()) {
+ log::error(
+ "FileeTreeModel::itemFromIndex(): row {} is out of range for {}",
+ index.row(), parentItem->debugName());
+
+ return nullptr;
+ }
+
+ return parentItem->children()[index.row()].get();
+}
+
+QModelIndex FileTreeModel::indexFromItem(FileTreeItem& item, int col) const
+{
+ auto* parent = item.parent();
+ if (!parent) {
+ return {};
+ }
+
+ const int index = parent->childIndex(item);
+ if (index == -1) {
+ log::error(
+ "FileTreeMode::indexFromItem(): item {} not found in parent",
+ item.debugName());
+
+ return {};
+ }
+
+ return createIndex(index, col, makeInternalPointer(parent));
+}
+
+void FileTreeModel::update(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath)
+{
+ trace(log::debug("updating {}", parentItem.debugName()));
+
+ auto path = parentPath;
+ if (!parentEntry.isTopLevel()) {
+ if (!path.empty()) {
+ path += L"\\";
+ }
+
+ path += parentEntry.getName();
+ }
+
+ parentItem.setLoaded(true);
+
+ bool added = false;
+
+ if (updateDirectories(parentItem, path, parentEntry)) {
+ added = true;
+ }
+
+ if (updateFiles(parentItem, path, parentEntry)) {
+ added = true;
+ }
+
+ if (added) {
+ parentItem.sort(m_sort.column, m_sort.order, true);
+ }
+}
+
+bool FileTreeModel::updateDirectories(
+ FileTreeItem& parentItem, const std::wstring& parentPath,
+ const MOShared::DirectoryEntry& parentEntry)
+{
+ // removeDisappearingDirectories() will add directories that are in the
+ // tree and still on the filesystem to this set; addNewDirectories() will
+ // use this to figure out if a directory is new or not
+ std::unordered_set<std::wstring_view> seen;
+
+ removeDisappearingDirectories(parentItem, parentEntry, parentPath, seen);
+ return addNewDirectories(parentItem, parentEntry, parentPath, seen);
+}
+
+void FileTreeModel::removeDisappearingDirectories(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath, std::unordered_set<std::wstring_view>& seen)
+{
+ auto& children = parentItem.children();
+ auto itor = children.begin();
+
+ // keeps track of the contiguous directories that need to be removed to
+ // avoid calling beginRemoveRows(), etc. for each item
+ Range range(this, parentItem);
+
+ // for each item in this tree item
+ while (itor != children.end()) {
+ const auto& item = *itor;
+
+ if (!item->isDirectory()) {
+ // directories are always first, no point continuing once a file has
+ // been seen
+ break;
+ }
+
+ auto d = parentEntry.findSubDirectory(item->filenameWsLowerCase(), true);
+
+ if (d) {
+ trace(log::debug("dir {} still there", item->filename()));
+
+ // directory is still there
+ seen.emplace(d->getName());
+
+ bool currentRemoved = false;
+
+ if (item->areChildrenVisible()) {
+ // the item is currently expanded, update it
+ update(*item, *d, parentPath);
+ }
+
+ if (shouldShowFolder(*d, item.get())) {
+ // folder should be left in the list
+ if (!item->areChildrenVisible() && item->isLoaded()) {
+ if (!d->isEmpty()) {
+ // the item is loaded (previously expanded but now collapsed) and
+ // has children, mark it as unloaded so it updates when next
+ // expanded
+ item->setLoaded(false);
+ }
+ }
+ } else {
+ // item wouldn't have any children, prune it
+ trace(log::debug("dir {} is empty and pruned", item->filename()));
+
+ range.includeCurrent();
+ currentRemoved = true;
+ ++itor;
+ }
+
+ if (!currentRemoved) {
+ // if there were directories before this row that need to be removed,
+ // do it now
+ itor = range.remove();
+ }
+ } else {
+ // directory is gone from the parent entry
+ trace(log::debug("dir {} is gone", item->filename()));
+
+ range.includeCurrent();
+ ++itor;
+ }
+
+ range.next();
+ }
+
+ // remove the last directory range, if any
+ range.remove();
+}
+
+bool FileTreeModel::addNewDirectories(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath,
+ const std::unordered_set<std::wstring_view>& seen)
+{
+ // keeps track of the contiguous directories that need to be added to
+ // avoid calling beginAddRows(), etc. for each item
+ Range range(this, parentItem);
+ std::vector<FileTreeItem::Ptr> toAdd;
+ bool added = false;
+
+ // for each directory on the filesystem
+ for (auto&& d : parentEntry.getSubDirectories()) {
+ if (seen.contains(d->getName())) {
+ // already seen in the parent item
+
+ // if there were directories before this row that need to be added,
+ // do it now
+ //
+ // todo: if the directory was actually removed in
+ // removeDisappearingDirectories(), the range doesn't need to be added
+ // now and could be extended further
+ range.add(std::move(toAdd));
+ toAdd.clear();
+ } else {
+ if (!shouldShowFolder(*d, nullptr)) {
+ // this is a new directory, but it doesn't contain anything interesting
+ trace(log::debug("new dir {}, empty and pruned", QString::fromStdWString(d->getName())));
+
+ // act as if this directory doesn't exist at all
+ continue;
+ }
+
+ // this is a new directory
+ trace(log::debug("new dir {}", QString::fromStdWString(d->getName())));
+
+ toAdd.push_back(createDirectoryItem(parentItem, parentPath, *d));
+ added = true;
+
+ range.includeCurrent();
+ }
+
+ range.next();
+ }
+
+ // add the last directory range, if any
+ range.add(std::move(toAdd));
+
+ return added;
+}
+
+bool FileTreeModel::updateFiles(
+ FileTreeItem& parentItem, const std::wstring& parentPath,
+ const MOShared::DirectoryEntry& parentEntry)
+{
+ // removeDisappearingFiles() will add files that are in the tree and still on
+ // the filesystem to this set; addNewFiless() will use this to figure out if
+ // a file is new or not
+ std::unordered_set<FileEntry::Index> seen;
+
+ int firstFileRow = 0;
+
+ removeDisappearingFiles(parentItem, parentEntry, firstFileRow, seen);
+ return addNewFiles(parentItem, parentEntry, parentPath, firstFileRow, seen);
+}
+
+void FileTreeModel::removeDisappearingFiles(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ int& firstFileRow, std::unordered_set<FileEntry::Index>& seen)
+{
+ auto& children = parentItem.children();
+ auto itor = children.begin();
+
+ firstFileRow = -1;
+
+ // keeps track of the contiguous directories that need to be removed to
+ // avoid calling beginRemoveRows(), etc. for each item
+ Range range(this, parentItem);
+
+ // for each item in this tree item
+ while (itor != children.end()) {
+ const auto& item = *itor;
+
+ if (!item->isDirectory()) {
+ if (firstFileRow == -1) {
+ firstFileRow = range.current();
+ }
+
+ auto f = parentEntry.findFile(item->key());
+
+ if (f && shouldShowFile(*f)) {
+ trace(log::debug("file {} still there", item->filename()));
+
+ // file is still there
+ seen.emplace(f->getIndex());
+
+ if (f->getOrigin() != item->originID()) {
+ // origin has changed
+ updateFileItem(*item, *f);
+ }
+
+ // if there were files before this row that need to be removed,
+ // do it now
+ itor = range.remove();
+ } else {
+ // file is gone from the parent entry
+ trace(log::debug("file {} is gone", item->filename()));
+
+ range.includeCurrent();
+ ++itor;
+ }
+ } else {
+ ++itor;
+ }
+
+ range.next();
+ }
+
+ // remove the last file range, if any
+ range.remove();
+
+ if (firstFileRow == -1) {
+ firstFileRow = static_cast<int>(children.size());
+ }
+}
+
+bool FileTreeModel::addNewFiles(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath, const int firstFileRow,
+ const std::unordered_set<FileEntry::Index>& seen)
+{
+ // keeps track of the contiguous files that need to be added to
+ // avoid calling beginAddRows(), etc. for each item
+ std::vector<FileTreeItem::Ptr> toAdd;
+ Range range(this, parentItem, firstFileRow);
+ bool added = false;
+
+ // for each directory on the filesystem
+ parentEntry.forEachFileIndex([&](auto&& fileIndex) {
+ if (seen.contains(fileIndex)) {
+ // already seen in the parent item
+
+ // if there were directories before this row that need to be added,
+ // do it now
+ range.add(std::move(toAdd));
+ toAdd.clear();
+ } else {
+ const auto file = parentEntry.getFileByIndex(fileIndex);
+
+ if (!file) {
+ log::error(
+ "FileTreeModel::addNewFiles(): file index {} in path {} not found",
+ fileIndex, parentPath);
+
+ return true;
+ }
+
+ if (shouldShowFile(*file)) {
+ // this is a new file
+ trace(log::debug("new file {}", QString::fromStdWString(file->getName())));
+
+ toAdd.push_back(createFileItem(parentItem, parentPath, *file));
+ added = true;
+
+ range.includeCurrent();
+ } else {
+ // this is a new file, but it shouldn't be shown
+ trace(log::debug("new file {}, not shown", QString::fromStdWString(file->getName())));
+ return true;
+ }
+ }
+
+ range.next();
+
+ return true;
+ });
+
+ // add the last file range, if any
+ range.add(std::move(toAdd));
+
+ return added;
+}
+
+FileTreeItem::Ptr FileTreeModel::createDirectoryItem(
+ FileTreeItem& parentItem, const std::wstring& parentPath,
+ const DirectoryEntry& d)
+{
+ auto item = FileTreeItem::createDirectory(
+ this, &parentItem, parentPath, d.getName());
+
+ if (d.isEmpty()) {
+ // if this directory is empty, mark the item as loaded so the expand
+ // arrow doesn't show
+ item->setLoaded(true);
+ }
+
+ return item;
+}
+
+FileTreeItem::Ptr FileTreeModel::createFileItem(
+ FileTreeItem& parentItem, const std::wstring& parentPath,
+ const FileEntry& file)
+{
+ auto item = FileTreeItem::createFile(
+ this, &parentItem, parentPath, file.getName());
+
+ updateFileItem(*item, file);
+
+ item->setLoaded(true);
+
+ return item;
+}
+
+void FileTreeModel::updateFileItem(
+ FileTreeItem& item, const MOShared::FileEntry& file)
+{
+ bool isArchive = false;
+ int originID = file.getOrigin(isArchive);
+
+ FileTreeItem::Flags flags = FileTreeItem::NoFlags;
+
+ if (isArchive) {
+ flags |= FileTreeItem::FromArchive;
+ }
+
+ if (!file.getAlternatives().empty()) {
+ flags |= FileTreeItem::Conflicted;
+ }
+
+ item.setOrigin(
+ originID, file.getFullPath(), flags, makeModName(file, originID));
+
+ if (file.getFileSize() != FileEntry::NoFileSize) {
+ item.setFileSize(file.getFileSize());
+ }
+
+ if (file.getCompressedFileSize() != FileEntry::NoFileSize) {
+ item.setCompressedFileSize(file.getCompressedFileSize());
+ }
+}
+
+bool FileTreeModel::shouldShowFile(const FileEntry& file) const
+{
+ if (showConflictsOnly() && (file.getAlternatives().size() == 0)) {
+ // only conflicts should be shown, but this file is not conflicted
+ return false;
+ }
+
+ if (!showArchives() && file.isFromArchive()) {
+ // files from archives shouldn't be shown, but this file is from an archive
+ return false;
+ }
+
+ return true;
+}
+
+bool FileTreeModel::shouldShowFolder(
+ const DirectoryEntry& dir, const FileTreeItem* item) const
+{
+ bool shouldPrune = m_flags.testFlag(PruneDirectories);
+
+ if (m_core.settings().archiveParsing()) {
+ if (!m_flags.testFlag(Archives)) {
+ // archive parsing is enabled but the tree shouldn't show archives; this
+ // is a bit of a special case for folders because they have to be hidden
+ // regardless of the PruneDirectories flag if they only exist in archives
+ //
+ // note that this test is inaccurate: if a loose folder exists but is
+ // empty, and the same folder exists in an archive but is _not_ empty,
+ // then it's considered to exist _only_ in an archive and will be pruned
+ //
+ // if directories are ever made first-class so they can retain their
+ // origins, this test can be made more accurate
+ shouldPrune = true;
+ }
+ }
+
+ if (!shouldPrune) {
+ // always show folders regardless of their content
+ return true;
+ }
+
+ if (item) {
+ if (item->isLoaded() && item->children().empty()) {
+ // item is loaded and has no children; prune it
+ return false;
+ }
+ }
+
+ bool foundFile = false;
+
+ // check all files in this directory, return early if a file should be shown
+ dir.forEachFile([&](auto&& f) {
+ if (shouldShowFile(f)) {
+ foundFile = true;
+
+ // stop
+ return false;
+ }
+
+ // continue
+ return true;
+ });
+
+ if (foundFile) {
+ return true;
+ }
+
+ // recurse into subdirectories
+ for (auto subdir : dir.getSubDirectories()) {
+ if (shouldShowFolder(*subdir, nullptr)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+QVariant FileTreeModel::displayData(const FileTreeItem* item, int column) const
+{
+ switch (column)
+ {
+ case FileName:
+ {
+ return item->filename();
+ }
+
+ case ModName:
+ {
+ return item->mod();
+ }
+
+ case FileType:
+ {
+ return item->fileType().value_or(QString());
+ }
+
+ case FileSize:
+ {
+ if (item->isDirectory()) {
+ return {};
+ } else {
+ QString fs;
+
+ if (auto n=item->fileSize()) {
+ fs = localizedByteSize(*n);
+ }
+
+ if (auto n=item->compressedFileSize()) {
+ return QString("%1 (%2)").arg(fs).arg(localizedByteSize(*n));
+ } else {
+ return fs;
+ }
+ }
+ }
+
+ case LastModified:
+ {
+ if (auto d=item->lastModified()) {
+ if (d->isValid()) {
+ return d->toString(Qt::SystemLocaleDate);
+ }
+ }
+
+ return {};
+ }
+
+ default:
+ {
+ return {};
+ }
+ }
+}
+
+std::wstring FileTreeModel::makeModName(
+ const MOShared::FileEntry& file, int originID) const
+{
+ static const std::wstring Unmanaged = UnmanagedModName().toStdWString();
+
+ const auto origin = m_core.directoryStructure()->getOriginByID(originID);
+
+ if (origin.getID() == 0) {
+ return Unmanaged;
+ }
+
+ std::wstring name = origin.getName();
+
+ const auto& archive = file.getArchive();
+ if (!archive.first.empty()) {
+ name += L" (" + archive.first + L")";
+ }
+
+ return name;
+}
+
+QString FileTreeModel::makeTooltip(const FileTreeItem& item) const
+{
+ auto nowrap = [&](auto&& s) {
+ return "<p style=\"white-space: pre; margin: 0; padding: 0;\">" + s + "</p>";
+ };
+
+ auto line = [&](auto&& caption, auto&& value) {
+ if (value.isEmpty()) {
+ return nowrap("<b>" + caption + ":</b>\n");
+ } else {
+ return nowrap("<b>" + caption + ":</b> " + value.toHtmlEscaped()) + "\n";
+ }
+ };
+
+
+ if (item.isDirectory()) {
+ return
+ line(tr("Directory"), item.filename()) +
+ line(tr("Virtual path"), item.virtualPath());
+ }
+
+
+ static const QString ListStart =
+ "<ul style=\""
+ "margin-left: 20px; "
+ "margin-top: 0; "
+ "margin-bottom: 0; "
+ "padding: 0; "
+ "-qt-list-indent: 0;"
+ "\">";
+
+ static const QString ListEnd = "</ul>";
+
+
+ QString s =
+ line(tr("Virtual path"), item.virtualPath()) +
+ line(tr("Real path"), item.realPath()) +
+ line(tr("From"), item.mod());
+
+
+ const auto file = m_core.directoryStructure()->searchFile(
+ item.dataRelativeFilePath().toStdWString(), nullptr);
+
+ if (file) {
+ const auto alternatives = file->getAlternatives();
+ QStringList list;
+
+ for (auto&& alt : file->getAlternatives()) {
+ const auto& origin = m_core.directoryStructure()->getOriginByID(alt.first);
+ list.push_back(QString::fromStdWString(origin.getName()));
+ }
+
+ if (list.size() == 1) {
+ s += line(tr("Also in"), list[0]);
+ } else if (list.size() >= 2) {
+ s += line(tr("Also in"), QString()) + ListStart;
+
+ for (auto&& alt : list) {
+ s += "<li>" + alt +"</li>";
+ }
+
+ s += ListEnd;
+ }
+ }
+
+ return s;
+}
+
+QVariant FileTreeModel::makeIcon(
+ const FileTreeItem& item, const QModelIndex& index) const
+{
+ if (item.isDirectory()) {
+ return m_iconFetcher.genericDirectoryIcon();
+ }
+
+ auto v = m_iconFetcher.icon(item.realPath());
+ if (!v.isNull()) {
+ return v;
+ }
+
+ m_iconPending.push_back(index);
+ m_iconPendingTimer.start(std::chrono::milliseconds(1));
+
+ return m_iconFetcher.genericFileIcon();
+}
+
+void FileTreeModel::updatePendingIcons()
+{
+ std::vector<QModelIndex> v(std::move(m_iconPending));
+ m_iconPending.clear();
+
+ for (auto&& index : v) {
+ emit dataChanged(index, index, {Qt::DecorationRole});
+ }
+
+ if (m_iconPending.empty()) {
+ m_iconPendingTimer.stop();
+ }
+}
+
+void FileTreeModel::removePendingIcons(
+ const QModelIndex& parent, int first, int last)
+{
+ auto itor = m_iconPending.begin();
+
+ while (itor != m_iconPending.end()) {
+ if (itor->parent() == parent) {
+ if (itor->row() >= first && itor->row() <= last) {
+ itor = m_iconPending.erase(itor);
+ continue;
+ }
+ }
+
+ ++itor;
+ }
+}
diff --git a/src/filetreemodel.h b/src/filetreemodel.h
new file mode 100644
index 00000000..093407b0
--- /dev/null
+++ b/src/filetreemodel.h
@@ -0,0 +1,167 @@
+#ifndef MODORGANIZER_FILETREEMODEL_INCLUDED
+#define MODORGANIZER_FILETREEMODEL_INCLUDED
+
+#include "filetreeitem.h"
+#include "iconfetcher.h"
+#include "directoryentry.h"
+#include <unordered_set>
+
+class OrganizerCore;
+
+class FileTreeModel : public QAbstractItemModel
+{
+ Q_OBJECT;
+
+public:
+ enum Flag
+ {
+ NoFlags = 0x00,
+ ConflictsOnly = 0x01,
+ Archives = 0x02,
+ PruneDirectories = 0x04
+ };
+
+ enum Columns
+ {
+ FileName = 0,
+ ModName,
+ FileType,
+ FileSize,
+ LastModified,
+
+ ColumnCount
+ };
+
+ Q_DECLARE_FLAGS(Flags, Flag);
+
+ struct SortInfo
+ {
+ int column = 0;
+ Qt::SortOrder order = Qt::AscendingOrder;
+ };
+
+
+ FileTreeModel(OrganizerCore& core, QObject* parent=nullptr);
+
+ void setFlags(Flags f)
+ {
+ m_flags = f;
+ }
+
+ void refresh();
+ void clear();
+
+ bool fullyLoaded() const
+ {
+ return m_fullyLoaded;
+ }
+
+ void ensureFullyLoaded();
+
+ bool enabled() const;
+ void setEnabled(bool b);
+
+ const SortInfo& sortInfo() const;
+
+ QModelIndex index(int row, int col, const QModelIndex& parent={}) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ int rowCount(const QModelIndex& parent={}) const override;
+ int columnCount(const QModelIndex& parent={}) const override;
+ bool hasChildren(const QModelIndex& parent={}) const override;
+ bool canFetchMore(const QModelIndex& parent) const override;
+ void fetchMore(const QModelIndex& parent) override;
+ QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const override;
+ QVariant headerData(int i, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
+ Qt::ItemFlags flags(const QModelIndex& index) const override;
+ void sort(int column, Qt::SortOrder order=Qt::AscendingOrder) override;
+
+ FileTreeItem* itemFromIndex(const QModelIndex& index) const;
+ void sortItem(FileTreeItem& item, bool force);
+
+private:
+ class Range;
+
+ using DirectoryIterator = std::vector<MOShared::DirectoryEntry*>::const_iterator;
+
+ OrganizerCore& m_core;
+ bool m_enabled;
+ mutable FileTreeItem::Ptr m_root;
+ Flags m_flags;
+ mutable IconFetcher m_iconFetcher;
+ mutable std::vector<QModelIndex> m_iconPending;
+ mutable QTimer m_iconPendingTimer;
+ SortInfo m_sort;
+ bool m_fullyLoaded;
+
+ bool showConflictsOnly() const
+ {
+ return (m_flags & ConflictsOnly);
+ }
+
+ bool showArchives() const;
+
+
+ void update(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath);
+
+
+ bool updateDirectories(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const MOShared::DirectoryEntry& parentEntry);
+
+ void removeDisappearingDirectories(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath, std::unordered_set<std::wstring_view>& seen);
+
+ bool addNewDirectories(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath,
+ const std::unordered_set<std::wstring_view>& seen);
+
+
+ bool updateFiles(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const MOShared::DirectoryEntry& parentEntry);
+
+ void removeDisappearingFiles(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ int& firstFileRow, std::unordered_set<MOShared::FileEntry::Index>& seen);
+
+ bool addNewFiles(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath, int firstFileRow,
+ const std::unordered_set<MOShared::FileEntry::Index>& seen);
+
+
+ FileTreeItem::Ptr createDirectoryItem(
+ FileTreeItem& parentItem, const std::wstring& parentPath,
+ const MOShared::DirectoryEntry& d);
+
+ FileTreeItem::Ptr createFileItem(
+ FileTreeItem& parentItem, const std::wstring& parentPath,
+ const MOShared::FileEntry& file);
+
+ void updateFileItem(FileTreeItem& item, const MOShared::FileEntry& file);
+
+
+ QVariant displayData(const FileTreeItem* item, int column) const;
+ std::wstring makeModName(const MOShared::FileEntry& file, int originID) const;
+
+ void ensureLoaded(FileTreeItem* item) const;
+ void updatePendingIcons();
+ void removePendingIcons(const QModelIndex& parent, int first, int last);
+
+ bool shouldShowFile(const MOShared::FileEntry& file) const;
+ bool shouldShowFolder(const MOShared::DirectoryEntry& dir, const FileTreeItem* item) const;
+ QString makeTooltip(const FileTreeItem& item) const;
+ QVariant makeIcon(const FileTreeItem& item, const QModelIndex& index) const;
+
+ QModelIndex indexFromItem(FileTreeItem& item, int col=0) const;
+ void recursiveFetchMore(const QModelIndex& m);
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeModel::Flags);
+Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeItem::Flags);
+
+#endif // MODORGANIZER_FILETREEMODEL_INCLUDED
diff --git a/src/iconfetcher.cpp b/src/iconfetcher.cpp
new file mode 100644
index 00000000..2b19f993
--- /dev/null
+++ b/src/iconfetcher.cpp
@@ -0,0 +1,159 @@
+#include "iconfetcher.h"
+#include "util.h"
+
+void IconFetcher::Waiter::wait()
+{
+ std::unique_lock lock(m_wakeUpMutex);
+ m_wakeUp.wait(lock, [&]{ return m_queueAvailable; });
+ m_queueAvailable = false;
+}
+
+void IconFetcher::Waiter::wakeUp()
+{
+ {
+ std::scoped_lock lock(m_wakeUpMutex);
+ m_queueAvailable = true;
+ }
+
+ m_wakeUp.notify_one();
+}
+
+
+IconFetcher::IconFetcher()
+ : m_iconSize(GetSystemMetrics(SM_CXSMICON)), m_stop(false)
+{
+ m_quickCache.file = getPixmapIcon(QFileIconProvider::File);
+ m_quickCache.directory = getPixmapIcon(QFileIconProvider::Folder);
+
+ m_thread = std::thread([&]{ threadFun(); });
+}
+
+IconFetcher::~IconFetcher()
+{
+ stop();
+ m_thread.join();
+}
+
+void IconFetcher::stop()
+{
+ m_stop = true;
+ m_waiter.wakeUp();
+}
+
+QVariant IconFetcher::icon(const QString& path) const
+{
+ if (hasOwnIcon(path)) {
+ return fileIcon(path);
+ } else {
+ const auto dot = path.lastIndexOf(".");
+
+ if (dot == -1) {
+ // no extension
+ return m_quickCache.file;
+ }
+
+ return extensionIcon(path.midRef(dot));
+ }
+}
+
+QPixmap IconFetcher::genericFileIcon() const
+{
+ return m_quickCache.file;
+}
+
+QPixmap IconFetcher::genericDirectoryIcon() const
+{
+ return m_quickCache.directory;
+}
+
+bool IconFetcher::hasOwnIcon(const QString& path) const
+{
+ static const QString exe = ".exe";
+ static const QString lnk = ".lnk";
+ static const QString ico = ".ico";
+
+ return
+ path.endsWith(exe, Qt::CaseInsensitive) ||
+ path.endsWith(lnk, Qt::CaseInsensitive) ||
+ path.endsWith(ico, Qt::CaseInsensitive);
+}
+
+void IconFetcher::threadFun()
+{
+ MOShared::SetThisThreadName("IconFetcher");
+
+ while (!m_stop) {
+ m_waiter.wait();
+ if (m_stop) {
+ break;
+ }
+
+ checkCache(m_extensionCache);
+ checkCache(m_fileCache);
+ }
+}
+
+void IconFetcher::checkCache(Cache& cache)
+{
+ std::set<QString> queue;
+
+ {
+ std::scoped_lock lock(cache.queueMutex);
+ queue = std::move(cache.queue);
+ cache.queue.clear();
+ }
+
+ if (queue.empty()) {
+ return;
+ }
+
+ std::map<QString, QPixmap> map;
+ for (auto&& ext : queue) {
+ map.emplace(std::move(ext), getPixmapIcon(ext));
+ }
+
+ {
+ std::scoped_lock lock(cache.mapMutex);
+ for (auto&& p : map) {
+ cache.map.insert(std::move(p));
+ }
+ }
+}
+
+void IconFetcher::queue(Cache& cache, QString path) const
+{
+ {
+ std::scoped_lock lock(cache.queueMutex);
+ cache.queue.insert(std::move(path));
+ }
+
+ m_waiter.wakeUp();
+}
+
+QVariant IconFetcher::fileIcon(const QString& path) const
+{
+ {
+ std::scoped_lock lock(m_fileCache.mapMutex);
+ auto itor = m_fileCache.map.find(path);
+ if (itor != m_fileCache.map.end()) {
+ return itor->second;
+ }
+ }
+
+ queue(m_fileCache, path);
+ return {};
+}
+
+QVariant IconFetcher::extensionIcon(const QStringRef& ext) const
+{
+ {
+ std::scoped_lock lock(m_extensionCache.mapMutex);
+ auto itor = m_extensionCache.map.find(ext);
+ if (itor != m_extensionCache.map.end()) {
+ return itor->second;
+ }
+ }
+
+ queue(m_extensionCache, ext.toString());
+ return {};
+}
diff --git a/src/iconfetcher.h b/src/iconfetcher.h
new file mode 100644
index 00000000..030bfb79
--- /dev/null
+++ b/src/iconfetcher.h
@@ -0,0 +1,76 @@
+#ifndef MODORGANIZER_ICONFETCHER_INCLUDED
+#define MODORGANIZER_ICONFETCHER_INCLUDED
+
+#include <QFileIconProvider>
+#include <mutex>
+
+class IconFetcher
+{
+public:
+ IconFetcher();
+ ~IconFetcher();
+
+ void stop();
+
+ QVariant icon(const QString& path) const;
+ QPixmap genericFileIcon() const;
+ QPixmap genericDirectoryIcon() const;
+
+private:
+ struct QuickCache
+ {
+ QPixmap file;
+ QPixmap directory;
+ };
+
+ struct Cache
+ {
+ std::map<QString, QPixmap, std::less<>> map;
+ std::mutex mapMutex;
+
+ std::set<QString> queue;
+ std::mutex queueMutex;
+ };
+
+ class Waiter
+ {
+ public:
+ void wait();
+ void wakeUp();
+
+ private:
+ mutable std::mutex m_wakeUpMutex;
+ std::condition_variable m_wakeUp;
+ bool m_queueAvailable = false;
+ };
+
+
+ const int m_iconSize;
+ QFileIconProvider m_provider;
+ std::thread m_thread;
+ std::atomic<bool> m_stop;
+
+ mutable QuickCache m_quickCache;
+ mutable Cache m_extensionCache;
+ mutable Cache m_fileCache;
+ mutable Waiter m_waiter;
+
+
+ bool hasOwnIcon(const QString& path) const;
+
+ template <class T>
+ QPixmap getPixmapIcon(T&& t) const
+ {
+ return m_provider.icon(t).pixmap({m_iconSize, m_iconSize});
+ }
+
+ void threadFun();
+
+ void checkCache(Cache& cache);
+ void queue(Cache& cache, QString path) const;
+
+ QVariant fileIcon(const QString& path) const;
+ QVariant extensionIcon(const QStringRef& ext) const;
+};
+
+#endif // MODORGANIZER_ICONFETCHER_INCLUDED
diff --git a/src/loglist.cpp b/src/loglist.cpp
index 5d6710c7..28aae0ff 100644
--- a/src/loglist.cpp
+++ b/src/loglist.cpp
@@ -27,7 +27,6 @@ const std::size_t MaxLines = 1000;
LogModel::LogModel()
{
- connect(this, &LogModel::entryAdded, [&](auto&& e){ onEntryAdded(e); });
}
void LogModel::create()
@@ -42,7 +41,8 @@ LogModel& LogModel::instance()
void LogModel::add(MOBase::log::Entry e)
{
- emit entryAdded(std::move(e));
+ QMetaObject::invokeMethod(
+ this, [this, e]{ onEntryAdded(std::move(e)); }, Qt::QueuedConnection);
}
void LogModel::clear()
@@ -173,13 +173,16 @@ LogList::LogList(QWidget* parent)
this, &QWidget::customContextMenuRequested,
[&](auto&& pos){ onContextMenu(pos); });
- connect(
- model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
- this, SLOT(scrollToBottom()));
+ connect(model(), &LogModel::rowsInserted, this, [&]{ onNewEntry(); });
+ connect(model(), &LogModel::dataChanged, this, [&]{ onNewEntry(); });
- connect(
- model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
- this, SLOT(scrollToBottom()));
+ m_timer.setSingleShot(true);
+ connect(&m_timer, &QTimer::timeout, [&]{ scrollToBottom(); });
+}
+
+void LogList::onNewEntry()
+{
+ m_timer.start(std::chrono::milliseconds(10));
}
void LogList::setCore(OrganizerCore& core)
diff --git a/src/loglist.h b/src/loglist.h
index 36671be4..56b95d65 100644
--- a/src/loglist.h
+++ b/src/loglist.h
@@ -48,9 +48,6 @@ protected:
QVariant headerData(
int section, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
-signals:
- void entryAdded(MOBase::log::Entry e);
-
private:
std::deque<MOBase::log::Entry> m_entries;
@@ -75,7 +72,10 @@ public:
private:
OrganizerCore* m_core;
+ QTimer m_timer;
+
void onContextMenu(const QPoint& pos);
+ void onNewEntry();
};
#endif // LOGBUFFER_H
diff --git a/src/main.cpp b/src/main.cpp
index 29d2d02c..2f4c80a1 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -635,8 +635,6 @@ int runApplication(MOApplication &application, SingleInstance &instance,
}
}
- Q_ASSERT(!edition.isEmpty());
-
game->setGameVariant(edition);
log::info(
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 774534d9..b34d7e8a 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -77,6 +77,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "eventfilter.h"
#include "statusbar.h"
#include "filterlist.h"
+#include "datatab.h"
#include <utility.h>
#include <dataarchives.h>
#include <bsainvalidation.h>
@@ -86,6 +87,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "localsavegames.h"
#include "listdialog.h"
#include "envshortcut.h"
+#include "browserdialog.h"
#include <QAbstractItemDelegate>
#include <QAbstractProxyModel>
@@ -299,7 +301,19 @@ MainWindow::MainWindow(Settings &settings
const bool pluginListAdjusted =
settings.geometry().restoreState(ui->espList->header());
- settings.geometry().restoreState(ui->dataTree->header());
+ m_DataTab.reset(new DataTab(m_OrganizerCore, m_PluginContainer, this, ui));
+ m_DataTab->restoreState(settings);
+
+ connect(m_DataTab.get(), &DataTab::executablesChanged, [&]{ refreshExecutablesList(); });
+
+ connect(
+ m_DataTab.get(), &DataTab::originModified,
+ [&](int id){ originModified(id); });
+
+ connect(
+ m_DataTab.get(), &DataTab::displayModInformation,
+ [&](auto&& m, auto&& i, auto&& tab){ displayModInformation(m, i, tab); });
+
settings.geometry().restoreState(ui->downloadView->header());
ui->splitter->setStretchFactor(0, 3);
@@ -349,9 +363,6 @@ MainWindow::MainWindow(Settings &settings
connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), m_PluginListSortProxy, SLOT(updateFilter(QString)));
connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString)));
- connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*)));
- connect(ui->dataTree, SIGNAL(itemActivated(QTreeWidgetItem*, int)), this, SLOT(activateDataTreeItem(QTreeWidgetItem*, int)));
-
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed()));
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int)));
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString)));
@@ -396,8 +407,6 @@ MainWindow::MainWindow(Settings &settings
connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled);
connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close);
- connect(&m_IntegratedBrowser, SIGNAL(requestDownload(QUrl,QNetworkReply*)), &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*)));
-
m_CheckBSATimer.setSingleShot(true);
connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList()));
@@ -445,17 +454,17 @@ MainWindow::MainWindow(Settings &settings
if (m_OrganizerCore.getArchiveParsing())
{
- ui->showArchiveDataCheckBox->setCheckState(Qt::Checked);
- ui->showArchiveDataCheckBox->setEnabled(true);
- m_showArchiveData = true;
+ ui->dataTabShowFromArchives->setCheckState(Qt::Checked);
+ ui->dataTabShowFromArchives->setEnabled(true);
}
else
{
- ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked);
- ui->showArchiveDataCheckBox->setEnabled(false);
- m_showArchiveData = false;
+ ui->dataTabShowFromArchives->setCheckState(Qt::Unchecked);
+ ui->dataTabShowFromArchives->setEnabled(false);
}
+ QApplication::instance()->installEventFilter(this);
+
refreshExecutablesList();
updatePinnedExecutables();
resetActionIcons();
@@ -622,7 +631,12 @@ MainWindow::~MainWindow()
m_PluginContainer.setUserInterface(nullptr, nullptr);
m_OrganizerCore.setUserInterface(nullptr);
- m_IntegratedBrowser.close();
+
+ if (m_IntegratedBrowser) {
+ m_IntegratedBrowser->close();
+ m_IntegratedBrowser.reset();
+ }
+
delete ui;
} catch (std::exception &e) {
QMessageBox::critical(nullptr, tr("Crash on exit"),
@@ -1181,7 +1195,7 @@ void MainWindow::downloadFilterChanged(const QString &filter)
void MainWindow::expandModList(const QModelIndex &index)
{
QAbstractItemModel *model = ui->modList->model();
-#pragma message("why is this so complicated? mapping the index doesn't work, probably a bug in QtGroupingProxy?")
+
for (int i = 0; i < model->rowCount(); ++i) {
QModelIndex targetIdx = model->index(i, 0);
if (model->data(targetIdx).toString() == index.data().toString()) {
@@ -1383,7 +1397,12 @@ bool MainWindow::canExit()
void MainWindow::cleanup()
{
QWebEngineProfile::defaultProfile()->clearAllVisitedLinks();
- m_IntegratedBrowser.close();
+
+ if (m_IntegratedBrowser) {
+ m_IntegratedBrowser->close();
+ m_IntegratedBrowser = {};
+ }
+
m_SaveMetaTimer.stop();
m_MetaSave.waitForFinished();
}
@@ -1461,7 +1480,11 @@ 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) {
+ QMainWindow::event(event);
+ return true;
}
+
return false;
}
@@ -1487,8 +1510,17 @@ void MainWindow::modPagePluginInvoke()
IPluginModPage *plugin = qobject_cast<IPluginModPage*>(triggeredAction->data().value<QObject*>());
if (plugin != nullptr) {
if (plugin->useIntegratedBrowser()) {
- m_IntegratedBrowser.setWindowTitle(plugin->displayName());
- m_IntegratedBrowser.openUrl(plugin->pageURL());
+
+ if (!m_IntegratedBrowser) {
+ m_IntegratedBrowser.reset(new BrowserDialog);
+
+ connect(
+ m_IntegratedBrowser.get(), SIGNAL(requestDownload(QUrl,QNetworkReply*)),
+ &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*)));
+ }
+
+ m_IntegratedBrowser->setWindowTitle(plugin->displayName());
+ m_IntegratedBrowser->openUrl(plugin->pageURL());
} else {
QDesktopServices::openUrl(QUrl(plugin->pageURL()));
}
@@ -1657,161 +1689,6 @@ void MainWindow::on_profileBox_currentIndexChanged(int index)
}
}
-void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon)
-{
- bool isDirectory = true;
- //QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- //QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
-
- std::wostringstream temp;
- temp << directorySoFar << "\\" << directoryEntry.getName();
- {
- std::vector<DirectoryEntry*>::const_iterator current, end;
- directoryEntry.getSubDirectories(current, end);
- for (; current != end; ++current) {
- QString pathName = ToQString((*current)->getName());
- QStringList columns(pathName);
- columns.append("");
- if (!(*current)->isEmpty()) {
- QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns);
- directoryChild->setData(0, Qt::DecorationRole, *folderIcon);
- directoryChild->setData(0, Qt::UserRole + 3, isDirectory);
-
- if (conflictsOnly || !m_showArchiveData) {
- updateTo(directoryChild, temp.str(), **current, conflictsOnly, fileIcon, folderIcon);
- if (directoryChild->childCount() != 0) {
- subTree->addChild(directoryChild);
- }
- else {
- delete directoryChild;
- }
- }
- else {
- QTreeWidgetItem *onDemandLoad = new QTreeWidgetItem(QStringList());
- onDemandLoad->setData(0, Qt::UserRole + 0, "__loaded_on_demand__");
- onDemandLoad->setData(0, Qt::UserRole + 1, ToQString(temp.str()));
- onDemandLoad->setData(0, Qt::UserRole + 2, conflictsOnly);
- directoryChild->addChild(onDemandLoad);
- subTree->addChild(directoryChild);
- }
- }
- else {
- QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns);
- directoryChild->setData(0, Qt::DecorationRole, *folderIcon);
- directoryChild->setData(0, Qt::UserRole + 3, isDirectory);
- subTree->addChild(directoryChild);
- }
- }
- }
-
-
- isDirectory = false;
- {
- for (const FileEntry::Ptr current : directoryEntry.getFiles()) {
- if (conflictsOnly && (current->getAlternatives().size() == 0)) {
- continue;
- }
-
- bool isArchive = false;
- int originID = current->getOrigin(isArchive);
- if (!m_showArchiveData && isArchive) {
- continue;
- }
-
- QString fileName = ToQString(current->getName());
- QStringList columns(fileName);
- FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
-
- QString source;
- const unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
-
- if (modIndex == UINT_MAX) {
- source = UnmanagedModName();
- } else {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- source = modInfo->name();
- }
-
- std::pair<std::wstring, int> archive = current->getArchive();
- if (archive.first.length() != 0) {
- source.append(" (").append(ToQString(archive.first)).append(")");
- }
- columns.append(source);
- QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns);
- if (isArchive) {
- QFont font = fileChild->font(0);
- font.setItalic(true);
- fileChild->setFont(0, font);
- fileChild->setFont(1, font);
- } else if (fileName.endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive)) {
- QFont font = fileChild->font(0);
- font.setStrikeOut(true);
- fileChild->setFont(0, font);
- fileChild->setFont(1, font);
- }
- fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath()));
- fileChild->setData(0, Qt::DecorationRole, *fileIcon);
- fileChild->setData(0, Qt::UserRole + 3, isDirectory);
- fileChild->setData(0, Qt::UserRole + 1, isArchive);
- fileChild->setData(1, Qt::UserRole, source);
- fileChild->setData(1, Qt::UserRole + 1, originID);
-
- std::vector<std::pair<int, std::pair<std::wstring, int>>> alternatives = current->getAlternatives();
-
- if (!alternatives.empty()) {
- std::wostringstream altString;
- altString << ToWString(tr("Also in: <br>"));
- for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator altIter = alternatives.begin();
- altIter != alternatives.end(); ++altIter) {
- if (altIter != alternatives.begin()) {
- altString << " , ";
- }
- altString << "<span style=\"white-space: nowrap;\"><i>" << m_OrganizerCore.directoryStructure()->getOriginByID(altIter->first).getName() << "</font></span>";
- }
- fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str())));
- fileChild->setForeground(1, QBrush(Qt::red));
- } else {
- fileChild->setToolTip(1, tr("No conflict"));
- }
- subTree->addChild(fileChild);
- }
- }
-
-
- //subTree->sortChildren(0, Qt::AscendingOrder);
-}
-
-void MainWindow::delayedRemove()
-{
- for (QTreeWidgetItem *item : m_RemoveWidget) {
- item->removeChild(item->child(0));
- }
- m_RemoveWidget.clear();
-}
-
-void MainWindow::expandDataTreeItem(QTreeWidgetItem *item)
-{
- if ((item->childCount() == 1) && (item->child(0)->data(0, Qt::UserRole).toString() == "__loaded_on_demand__")) {
- // read the data we need from the sub-item, then dispose of it
- QTreeWidgetItem *onDemandDataItem = item->child(0);
- const QString path = onDemandDataItem->data(0, Qt::UserRole + 1).toString();
- std::wstring wspath = path.toStdWString();
- bool conflictsOnly = onDemandDataItem->data(0, Qt::UserRole + 2).toBool();
-
- std::wstring virtualPath = (wspath + L"\\").substr(6) + ToWString(item->text(0));
- DirectoryEntry *dir = m_OrganizerCore.directoryStructure()->findSubDirectoryRecursive(virtualPath);
- if (dir != nullptr) {
- QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- updateTo(item, wspath, *dir, conflictsOnly, &fileIcon, &folderIcon);
- } else {
- log::warn("failed to update view of {}", path);
- }
- m_RemoveWidget.push_back(item);
- QTimer::singleShot(5, this, SLOT(delayedRemove()));
- }
-}
-
bool MainWindow::refreshProfiles(bool selectProfile)
{
QComboBox* profileBox = findChild<QComboBox*>("profileBox");
@@ -1893,58 +1770,6 @@ void MainWindow::refreshExecutablesList()
ui->executablesListBox->setEnabled(true);
}
-
-void MainWindow::refreshDataTree()
-{
- QCheckBox *conflictsBox = findChild<QCheckBox*>("conflictsCheckBox");
- QTreeWidget *tree = findChild<QTreeWidget*>("dataTree");
- QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- tree->clear();
- QStringList columns("data");
- columns.append("");
- QTreeWidgetItem *subTree = new QTreeWidgetItem(columns);
- subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder));
- updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon);
- tree->insertTopLevelItem(0, subTree);
- subTree->setExpanded(true);
-}
-
-void MainWindow::refreshDataTreeKeepExpandedNodes()
-{
- QCheckBox *conflictsBox = findChild<QCheckBox*>("conflictsCheckBox");
- QTreeWidget *tree = findChild<QTreeWidget*>("dataTree");
- QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
- QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- QStringList expandedNodes;
- QTreeWidgetItemIterator it1(tree, QTreeWidgetItemIterator::NotHidden | QTreeWidgetItemIterator::HasChildren);
- while (*it1) {
- QTreeWidgetItem *current = (*it1);
- if (current->isExpanded() && !(current->text(0)=="data")) {
- expandedNodes.append(current->text(0)+"/"+current->parent()->text(0));
- }
- ++it1;
- }
-
- tree->clear();
- QStringList columns("data");
- columns.append("");
- QTreeWidgetItem *subTree = new QTreeWidgetItem(columns);
- subTree->setData(0, Qt::DecorationRole, (new QFileIconProvider())->icon(QFileIconProvider::Folder));
- updateTo(subTree, L"", *m_OrganizerCore.directoryStructure(), conflictsBox->isChecked(), &fileIcon, &folderIcon);
- tree->insertTopLevelItem(0, subTree);
- subTree->setExpanded(true);
- QTreeWidgetItemIterator it2(tree, QTreeWidgetItemIterator::HasChildren);
- while (*it2) {
- QTreeWidgetItem *current = (*it2);
- if (!(current->text(0)=="data") && expandedNodes.contains(current->text(0)+"/"+current->parent()->text(0))) {
- current->setExpanded(true);
- }
- ++it2;
- }
-}
-
-
void MainWindow::refreshSavesIfOpen()
{
if (ui->tabWidget->currentIndex() == 3) {
@@ -2317,7 +2142,6 @@ void MainWindow::storeSettings()
s.geometry().saveVisibility(ui->categoriesGroup);
s.geometry().saveState(ui->espList->header());
- s.geometry().saveState(ui->dataTree->header());
s.geometry().saveState(ui->downloadView->header());
s.geometry().saveState(ui->modList->header());
@@ -2325,6 +2149,7 @@ void MainWindow::storeSettings()
s.widgets().saveIndex(ui->executablesListBox);
m_Filters->saveState(s);
+ m_DataTab->saveState(s);
}
QWidget* MainWindow::qtWidget()
@@ -2332,11 +2157,6 @@ QWidget* MainWindow::qtWidget()
return this;
}
-void MainWindow::on_btnRefreshData_clicked()
-{
- m_OrganizerCore.refreshDirectoryStructure();
-}
-
void MainWindow::on_btnRefreshDownloads_clicked()
{
m_OrganizerCore.downloadManager()->refreshList();
@@ -2349,7 +2169,7 @@ void MainWindow::on_tabWidget_currentChanged(int index)
} else if (index == 1) {
m_OrganizerCore.refreshBSAList();
} else if (index == 2) {
- refreshDataTreeKeepExpandedNodes();
+ m_DataTab->activated();
} else if (index == 3) {
refreshSaveList();
}
@@ -2558,12 +2378,10 @@ void MainWindow::directory_refreshed()
// now
updateProblemsButton();
-
//Some better check for the current tab is needed.
if (ui->tabWidget->currentIndex() == 2) {
- refreshDataTreeKeepExpandedNodes();
+ m_DataTab->updateTree();
}
-
}
void MainWindow::esplist_changed()
@@ -5208,15 +5026,13 @@ void MainWindow::on_actionSettings_triggered()
m_OrganizerCore.setArchiveParsing(state);
if (!state)
{
- ui->showArchiveDataCheckBox->setCheckState(Qt::Unchecked);
- ui->showArchiveDataCheckBox->setEnabled(false);
- m_showArchiveData = false;
+ ui->dataTabShowFromArchives->setCheckState(Qt::Unchecked);
+ ui->dataTabShowFromArchives->setEnabled(false);
}
else
{
- ui->showArchiveDataCheckBox->setCheckState(Qt::Checked);
- ui->showArchiveDataCheckBox->setEnabled(true);
- m_showArchiveData = true;
+ ui->dataTabShowFromArchives->setCheckState(Qt::Checked);
+ ui->dataTabShowFromArchives->setEnabled(true);
}
m_OrganizerCore.refreshModList();
m_OrganizerCore.refreshDirectoryStructure();
@@ -5315,93 +5131,6 @@ void MainWindow::languageChange(const QString &newLanguage)
ui->openFolderMenu->setMenu(openFolderMenu());
}
-void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry)
-{
- for (FileEntry::Ptr current : directoryEntry.getFiles()) {
- bool isArchive = false;
- int origin = current->getOrigin(isArchive);
- if (isArchive) {
- // TODO: don't list files from archives. maybe make this an option?
- continue;
- }
- QString fullName = directory + "\\" + ToQString(current->getName());
- file.write(fullName.toUtf8());
-
- file.write("\t(");
- file.write(ToQString(m_OrganizerCore.directoryStructure()->getOriginByID(origin).getName()).toUtf8());
- file.write(")\r\n");
- }
-
- // recurse into subdirectories
- std::vector<DirectoryEntry*>::const_iterator current, end;
- directoryEntry.getSubDirectories(current, end);
- for (; current != end; ++current) {
- writeDataToFile(file, directory + "\\" + ToQString((*current)->getName()), **current);
- }
-}
-
-void MainWindow::writeDataToFile()
-{
- QString fileName = QFileDialog::getSaveFileName(this);
- if (!fileName.isEmpty()) {
- QFile file(fileName);
- if (!file.open(QIODevice::WriteOnly)) {
- reportError(tr("failed to write to file %1").arg(fileName));
- }
-
- writeDataToFile(file, "data", *m_OrganizerCore.directoryStructure());
- file.close();
-
- MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this);
- }
-}
-
-void MainWindow::addAsExecutable()
-{
- if (m_ContextItem == nullptr) {
- return;
- }
-
- const QFileInfo target(m_ContextItem->data(0, Qt::UserRole).toString());
- const auto fec = spawn::getFileExecutionContext(this, target);
-
- switch (fec.type)
- {
- case spawn::FileExecutionTypes::Executable:
- {
- const QString name = QInputDialog::getText(
- this, tr("Enter Name"),
- tr("Enter a name for the executable"),
- QLineEdit::Normal,
- target.completeBaseName());
-
- if (!name.isEmpty()) {
- //Note: If this already exists, you'll lose custom settings
- m_OrganizerCore.executablesList()->setExecutable(Executable()
- .title(name)
- .binaryInfo(fec.binary)
- .arguments(fec.arguments)
- .workingDirectory(target.absolutePath()));
-
- refreshExecutablesList();
- }
-
- break;
- }
-
- case spawn::FileExecutionTypes::Other: // fall-through
- default:
- {
- QMessageBox::information(
- this, tr("Not an executable"),
- tr("This is not a recognized executable."));
-
- break;
- }
- }
-}
-
-
void MainWindow::originModified(int originID)
{
FilesOrigin &origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
@@ -5411,56 +5140,6 @@ void MainWindow::originModified(int originID)
}
-void MainWindow::hideFile()
-{
- QString oldName = m_ContextItem->data(0, Qt::UserRole).toString();
- QString newName = oldName + ModInfo::s_HiddenExt;
-
- if (QFileInfo(newName).exists()) {
- if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- if (!QFile(newName).remove()) {
- QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName));
- return;
- }
- } else {
- return;
- }
- }
-
- if (QFile::rename(oldName, newName)) {
- originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt());
- refreshDataTreeKeepExpandedNodes();
- } else {
- reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName)));
- }
-}
-
-
-void MainWindow::unhideFile()
-{
- QString oldName = m_ContextItem->data(0, Qt::UserRole).toString();
- QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length());
- if (QFileInfo(newName).exists()) {
- if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- if (!QFile(newName).remove()) {
- QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName));
- return;
- }
- } else {
- return;
- }
- }
- if (QFile::rename(oldName, newName)) {
- originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt());
- refreshDataTreeKeepExpandedNodes();
- } else {
- reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName)));
- }
-}
-
-
void MainWindow::enableSelectedPlugins_clicked()
{
m_OrganizerCore.pluginList()->enableSelected(ui->espList->selectionModel());
@@ -5511,149 +5190,6 @@ void MainWindow::disableSelectedMods_clicked()
}
}
-
-void MainWindow::activateDataTreeItem(QTreeWidgetItem *item, int column)
-{
- const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
-
- if (isArchive || isDirectory) {
- return;
- }
-
- const QString path = item->data(0, Qt::UserRole).toString();
- if (path.isEmpty()) {
- return;
- }
-
- const QFileInfo targetInfo(path);
-
- const auto tryPreview = m_OrganizerCore.settings().interface().doubleClicksOpenPreviews();
-
- if (tryPreview && m_PluginContainer.previewGenerator().previewSupported(targetInfo.suffix())) {
- previewDataFile(item);
- } else {
- openDataFile(item);
- }
-}
-
-void MainWindow::openDataFile()
-{
- if (m_ContextItem == nullptr) {
- return;
- }
-
- openDataFile(m_ContextItem);
-}
-
-void MainWindow::openDataFile(QTreeWidgetItem* item)
-{
- const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
-
- if (isArchive || isDirectory) {
- return;
- }
-
- const QString path = item->data(0, Qt::UserRole).toString();
- const QFileInfo targetInfo(path);
-
- m_OrganizerCore.processRunner()
- .setFromFile(this, targetInfo)
- .setHooked(false)
- .setWaitForCompletion(ProcessRunner::Refresh)
- .run();
-}
-
-void MainWindow::runDataFileHooked()
-{
- if (m_ContextItem == nullptr) {
- return;
- }
-
- runDataFileHooked(m_ContextItem);
-}
-
-void MainWindow::runDataFileHooked(QTreeWidgetItem* item)
-{
- const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
-
- if (isArchive || isDirectory) {
- return;
- }
-
- const QString path = item->data(0, Qt::UserRole).toString();
- const QFileInfo targetInfo(path);
-
- m_OrganizerCore.processRunner()
- .setFromFile(this, targetInfo)
- .setHooked(true)
- .setWaitForCompletion(ProcessRunner::Refresh)
- .run();
-}
-
-void MainWindow::previewDataFile()
-{
- if (m_ContextItem == nullptr) {
- return;
- }
-
- previewDataFile(m_ContextItem);
-}
-
-void MainWindow::previewDataFile(QTreeWidgetItem* item)
-{
- QString fileName = QDir::fromNativeSeparators(item->data(0, Qt::UserRole).toString());
- m_OrganizerCore.previewFileWithAlternatives(this, fileName);
-}
-
-void MainWindow::openDataOriginExplorer_clicked()
-{
- if (m_ContextItem == nullptr) {
- return;
- }
-
- const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool();
-
- if (isArchive || isDirectory) {
- return;
- }
-
- const auto fullPath = m_ContextItem->data(0, Qt::UserRole).toString();
-
- log::debug("opening in explorer: {}", fullPath);
- shell::Explore(fullPath);
-}
-
-void MainWindow::openDataModInfo_clicked()
-{
- if (m_ContextItem == nullptr) {
- return;
- }
-
- const auto originID = m_ContextItem->data(1, Qt::UserRole + 1).toInt();
- if (originID == 0) {
- // unmanaged
- return;
- }
-
- const auto& origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
- const auto& name = QString::fromStdWString(origin.getName());
-
- unsigned int index = ModInfo::getIndex(name);
- if (index == UINT_MAX) {
- log::error("can't open mod info, mod '{}' not found", name);
- return;
- }
-
- ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
- if (modInfo) {
- displayModInformation(modInfo, index, ModInfoTabIDs::None);
- }
-}
-
void MainWindow::updateAvailable()
{
ui->actionUpdate->setEnabled(true);
@@ -5676,106 +5212,6 @@ void MainWindow::motdReceived(const QString &motd)
}
}
-void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
-{
- m_ContextItem = ui->dataTree->itemAt(pos.x(), pos.y());
-
- QMenu menu;
- if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0)
- && (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) {
- QString fileName = m_ContextItem->text(0);
- const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool();
-
- QAction* open = nullptr;
- QAction* runHooked = nullptr;
- QAction* preview = nullptr;
-
- if (canRunFile(isArchive, fileName)) {
- open = new QAction(tr("&Execute"), ui->dataTree);
- runHooked = new QAction(tr("Execute with &VFS"), ui->dataTree);
- } else if (canOpenFile(isArchive, fileName)) {
- open = new QAction(tr("&Open"), ui->dataTree);
- runHooked = new QAction(tr("Open with &VFS"), ui->dataTree);
- }
-
- if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) {
- preview = new QAction(tr("Preview"), ui->dataTree);
- }
-
- if (open) {
- connect(open, &QAction::triggered, [&]{ openDataFile(); });
- }
-
- if (runHooked) {
- connect(runHooked, &QAction::triggered, [&]{ runDataFileHooked(); });
- }
-
- if (preview) {
- connect(preview, &QAction::triggered, [&]{ previewDataFile(); });
- }
-
- if (open && preview) {
- if (m_OrganizerCore.settings().interface().doubleClicksOpenPreviews()) {
- menu.addAction(preview);
- menu.addAction(open);
- } else {
- menu.addAction(open);
- menu.addAction(preview);
- }
- } else {
- if (open) {
- menu.addAction(open);
- }
-
- if (preview) {
- menu.addAction(preview);
- }
- }
-
- if (runHooked) {
- menu.addAction(runHooked);
- }
-
- menu.addAction(tr("&Add as Executable"), this, SLOT(addAsExecutable()));
-
- if (!isArchive && !isDirectory) {
- menu.addAction("Open Origin in Explorer", this, SLOT(openDataOriginExplorer_clicked()));
- }
-
- menu.addAction("Open Mod Info", this, SLOT(openDataModInfo_clicked()));
-
- menu.addSeparator();
-
- // offer to hide/unhide file, but not for files from archives
- if (!isArchive) {
- if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive)) {
- menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile()));
- } else {
- menu.addAction(tr("Hide"), this, SLOT(hideFile()));
- }
- }
-
- if (open || preview || runHooked) {
- // bold the first option
- auto* top = menu.actions()[0];
- auto f = top->font();
- f.setBold(true);
- top->setFont(f);
- }
- }
-
- menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile()));
- menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked()));
-
- menu.exec(ui->dataTree->viewport()->mapToGlobal(pos));
-}
-
-void MainWindow::on_conflictsCheckBox_toggled(bool)
-{
- refreshDataTreeKeepExpandedNodes();
-}
-
void MainWindow::on_actionUpdate_triggered()
{
m_OrganizerCore.startMOUpdate();
@@ -7031,17 +6467,3 @@ void MainWindow::sendSelectedModsToSeparator_clicked()
}
}
}
-
-void MainWindow::on_showArchiveDataCheckBox_toggled(const bool checked)
-{
- if (m_OrganizerCore.getArchiveParsing() && checked)
- {
- m_showArchiveData = checked;
- }
- else
- {
- m_showArchiveData = false;
- }
- refreshDataTree();
-}
-
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 149a0fd8..1016c061 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -21,7 +21,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define MAINWINDOW_H
#include "bsafolder.h"
-#include "browserdialog.h"
#include "delayedfilewriter.h"
#include "errorcodes.h"
#include "imoinfo.h"
@@ -34,12 +33,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "iplugingame.h" //namespace MOBase { class IPluginGame; }
#include <log.h>
-//Note the commented headers here can be replaced with forward references,
-//when I get round to cleaning up main.cpp
class Executable;
class CategoryFactory;
class OrganizerCore;
class FilterList;
+class DataTab;
+class BrowserDialog;
class PluginListSortProxy;
namespace BSA { class Archive; }
@@ -122,8 +121,6 @@ public:
bool addProfile();
void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives);
- void refreshDataTree();
- void refreshDataTreeKeepExpandedNodes();
void refreshSaveList();
void setModListSorting(int index);
@@ -137,10 +134,6 @@ public:
void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info);
- void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
-
- QString getOriginDisplayName(int originID);
-
void installTranslator(const QString &name);
virtual void disconnectPlugins();
@@ -219,20 +212,12 @@ private:
QMenu* createPopupMenu() override;
void activateSelectedProfile();
- void startSteam();
-
- void updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const MOShared::DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon);
bool refreshProfiles(bool selectProfile = true);
void refreshExecutablesList();
void installMod(QString fileName = "");
- QList<MOBase::IOrganizer::FileInfo> findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const;
-
bool modifyExecutablesDialog(int selection);
void displayModInformation(int row, ModInfoTabIDs tab=ModInfoTabIDs::None);
- void testExtractBSA(int modIndex);
-
- void writeDataToFile(QFile &file, const QString &directory, const MOShared::DirectoryEntry &directoryEntry);
/**
* Sets category selections from menu; for multiple mods, this will only apply
@@ -267,8 +252,6 @@ private:
void displaySaveGameInfo(QListWidgetItem *newItem);
- HANDLE nextChildProcess();
-
bool errorReported(QString &logFile);
void updateESPLock(bool locked);
@@ -285,8 +268,6 @@ private:
QMenu *openFolderMenu();
- std::set<QString> enabledArchives();
-
void scheduleUpdateButton();
QDir currentSavesDir() const;
@@ -297,7 +278,6 @@ private:
void dropLocalFile(const QUrl &url, const QString &outputDir, bool move);
void sendSelectedModsToPriority(int newPriority);
- void sendSelectedPluginsToPriority(int newPriority);
void toggleMO2EndorseState();
void toggleUpdateAction();
@@ -322,6 +302,7 @@ private:
MOBase::TutorialControl m_Tutorial;
std::unique_ptr<FilterList> m_Filters;
+ std::unique_ptr<DataTab> m_DataTab;
int m_OldProfileIndex;
@@ -360,18 +341,14 @@ private:
QString m_CurrentLanguage;
std::vector<QTranslator*> m_Translators;
- BrowserDialog m_IntegratedBrowser;
+ std::unique_ptr<BrowserDialog> m_IntegratedBrowser;
QFileSystemWatcher m_SavesWatcher;
- std::vector<QTreeWidgetItem*> m_RemoveWidget;
-
QByteArray m_ArchiveListHash;
bool m_DidUpdateMasterList;
- bool m_showArchiveData{ true };
-
MOBase::DelayedFileWriter m_ArchiveListWriter;
QAction* m_LinkToolbar;
@@ -439,18 +416,6 @@ private slots:
void deleteSavegame_clicked();
void fixMods_clicked(SaveGameInfo::MissingAssets const &missingAssets);
// data-tree context menu
- void writeDataToFile();
- void openDataFile();
- void openDataFile(QTreeWidgetItem* item);
- void runDataFileHooked();
- void runDataFileHooked(QTreeWidgetItem* item);
- void addAsExecutable();
- void previewDataFile();
- void previewDataFile(QTreeWidgetItem* item);
- void hideFile();
- void unhideFile();
- void openDataOriginExplorer_clicked();
- void openDataModInfo_clicked();
// pluginlist context menu
void enableSelectedPlugins_clicked();
@@ -593,10 +558,7 @@ private slots:
void unignoreUpdate();
void refreshSavesIfOpen();
- void expandDataTreeItem(QTreeWidgetItem *item);
- void activateDataTreeItem(QTreeWidgetItem *item, int column);
void about();
- void delayedRemove();
void modListSortIndicatorChanged(int column, Qt::SortOrder order);
void modListSectionResized(int logicalIndex, int oldSize, int newSize);
@@ -635,11 +597,7 @@ private slots: // ui slots
void on_centralWidget_customContextMenuRequested(const QPoint &pos);
void on_bsaList_customContextMenuRequested(const QPoint &pos);
void on_clearFiltersButton_clicked();
- void on_btnRefreshData_clicked();
void on_btnRefreshDownloads_clicked();
- void on_conflictsCheckBox_toggled(bool checked);
- void on_showArchiveDataCheckBox_toggled(bool checked);
- void on_dataTree_customContextMenuRequested(const QPoint &pos);
void on_executablesListBox_currentIndexChanged(int index);
void on_modList_customContextMenuRequested(const QPoint &pos);
void on_modList_doubleClicked(const QModelIndex &index);
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index d0daafc0..b54d54f8 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
- <width>926</width>
+ <width>1009</width>
<height>710</height>
</rect>
</property>
@@ -97,12 +97,6 @@
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
- <property name="headerHidden">
- <bool>true</bool>
- </property>
- <attribute name="headerVisible">
- <bool>true</bool>
- </attribute>
<attribute name="headerCascadingSectionResizes">
<bool>false</bool>
</attribute>
@@ -781,22 +775,16 @@ p, li { white-space: pre-wrap; }
<string>Plugins</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QPushButton" name="bossButton">
+ <property name="toolTip">
+ <string>Sort the plugins using LOOT.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Sort the plugins using LOOT.</string>
+ </property>
<property name="text">
<string>Sort</string>
</property>
@@ -807,22 +795,28 @@ p, li { white-space: pre-wrap; }
</widget>
</item>
<item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
+ <widget class="MOBase::LineEditClear" name="espFilterEdit">
+ <property name="toolTip">
+ <string>Filter the list of plugins.</string>
</property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
+ <property name="whatsThis">
+ <string>Filter the list of plugins.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
</property>
- </spacer>
+ </widget>
</item>
<item>
<widget class="QPushButton" name="restoreButton">
<property name="toolTip">
- <string>Restore Backup...</string>
+ <string>Restore a backup.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Restore a backup.</string>
</property>
<property name="text">
<string notr="true"/>
@@ -842,7 +836,10 @@ p, li { white-space: pre-wrap; }
<item>
<widget class="QPushButton" name="saveButton">
<property name="toolTip">
- <string>Create Backup</string>
+ <string>Create a backup.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Create a backup.</string>
</property>
<property name="text">
<string notr="true"/>
@@ -869,7 +866,7 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="whatsThis">
- <string>This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter.</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
@@ -896,14 +893,10 @@ p, li { white-space: pre-wrap; }
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="toolTip">
- <string>List of available esp/esm files</string>
+ <string>List of available esp/esm files.</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;
-&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;This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.&lt;br /&gt;There is a great tool named &amp;quot;BOSS&amp;quot; to automatically sort these files.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>List of available esp/esm files.</string>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
@@ -949,20 +942,6 @@ p, li { white-space: pre-wrap; }
</attribute>
</widget>
</item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="MOBase::LineEditClear" name="espFilterEdit">
- <property name="text">
- <string/>
- </property>
- <property name="placeholderText">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
</layout>
</widget>
<widget class="QWidget" name="bsaTab">
@@ -973,18 +952,6 @@ p, li { white-space: pre-wrap; }
<string>Archives</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0">
<item>
@@ -1043,96 +1010,87 @@ p, li { white-space: pre-wrap; }
<string>Data</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
<item>
- <widget class="QPushButton" name="btnRefreshData">
- <property name="toolTip">
- <string>refresh data-directory overview</string>
- </property>
- <property name="whatsThis">
- <string>Refresh the overview. This may take a moment.</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
- <widget class="QTreeWidget" name="dataTree">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
+ <widget class="QPushButton" name="dataTabRefresh">
+ <property name="toolTip">
+ <string>Refresh the data structure.</string>
</property>
<property name="whatsThis">
- <string>This is an overview of your data directory as visible to the game (and tools). </string>
+ <string>Refresh the data structure.</string>
</property>
- <property name="alternatingRowColors">
- <bool>true</bool>
+ <property name="text">
+ <string>Refresh</string>
</property>
- <property name="uniformRowHeights">
- <bool>true</bool>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
</property>
- <attribute name="headerDefaultSectionSize">
- <number>400</number>
- </attribute>
- <column>
- <property name="text">
- <string>File</string>
- </property>
- </column>
- <column>
- <property name="text">
- <string>Mod</string>
- </property>
- </column>
</widget>
</item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
- <widget class="QCheckBox" name="conflictsCheckBox">
+ <widget class="QLineEdit" name="dataTabFilter">
<property name="toolTip">
- <string>Filters the above list so that only conflicts are displayed.</string>
+ <string>Filter the Data tree.</string>
</property>
<property name="whatsThis">
- <string>Filters the above list so that only conflicts are displayed.</string>
+ <string>Filter the Data tree.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="dataTabShowOnlyConflicts">
+ <property name="toolTip">
+ <string>Filter the list so that only conflicts are displayed.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Filter the list so that only conflicts are displayed.</string>
</property>
<property name="text">
- <string>Show only conflicts</string>
+ <string>Conflicts only</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="showArchiveDataCheckBox">
+ <widget class="QCheckBox" name="dataTabShowFromArchives">
<property name="toolTip">
- <string>Filters the above list so that files from archives are not shown</string>
+ <string>Filter the list so that files from archives are shown.</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="whatsThis">
- <string>Filters the above list so that files from archives are not shown</string>
+ <string>Filter the list so that files from archives are shown.</string>
</property>
<property name="text">
- <string>Show files from Archives</string>
+ <string>Archives</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QTreeView" name="dataTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="whatsThis">
+ <string>This is an overview of your data directory as visible to the game (and tools). </string>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
</property>
</widget>
</item>
@@ -1145,18 +1103,6 @@ p, li { white-space: pre-wrap; }
<string>Saves</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
<item>
<widget class="QListWidget" name="savegameList">
<property name="contextMenuPolicy">
@@ -1192,31 +1138,52 @@ p, li { white-space: pre-wrap; }
<string>Downloads</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
- <property name="leftMargin">
- <number>2</number>
- </property>
- <property name="topMargin">
- <number>2</number>
- </property>
- <property name="rightMargin">
- <number>2</number>
- </property>
- <property name="bottomMargin">
- <number>2</number>
- </property>
<item>
- <widget class="QPushButton" name="btnRefreshDownloads">
- <property name="toolTip">
- <string>Refresh downloads view</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0">
+ <item>
+ <widget class="QPushButton" name="btnRefreshDownloads">
+ <property name="toolTip">
+ <string>Refresh the downloads.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh the downloads.</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="downloadFilterEdit">
+ <property name="toolTip">
+ <string>Filter the list of downloads.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Filter the list of downloads.</string>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showHiddenBox">
+ <property name="toolTip">
+ <string>Show downloads marked as hidden.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Show downloads marked as hidden.</string>
+ </property>
+ <property name="text">
+ <string>Hidden files</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
</item>
<item>
<layout class="QVBoxLayout" name="downloadLayout">
@@ -1240,9 +1207,6 @@ p, li { white-space: pre-wrap; }
<property name="whatsThis">
<string>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</string>
</property>
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
<property name="dragEnabled">
<bool>true</bool>
</property>
@@ -1271,37 +1235,6 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,2">
- <item>
- <widget class="QCheckBox" name="showHiddenBox">
- <property name="text">
- <string>Show Hidden</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_3">
- <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="MOBase::LineEditClear" name="downloadFilterEdit">
- <property name="placeholderText">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
</layout>
</widget>
</widget>
@@ -1354,8 +1287,8 @@ p, li { white-space: pre-wrap; }
<rect>
<x>0</x>
<y>0</y>
- <width>926</width>
- <height>21</height>
+ <width>1009</width>
+ <height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index a071d58b..2fb12809 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -80,7 +80,11 @@ public:
MOApplication::MOApplication(int &argc, char **argv)
: QApplication(argc, argv)
{
- connect(&m_StyleWatcher, SIGNAL(fileChanged(QString)), SLOT(updateStyle(QString)));
+ connect(&m_StyleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){
+ log::debug("style file '{}' changed, reloading", file);
+ updateStyle(file);
+ });
+
m_DefaultStyle = style()->objectName();
setStyle(new ProxyStyle(style()));
}
diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp
index 302175aa..fb093529 100644
--- a/src/modinfodialog.cpp
+++ b/src/modinfodialog.cpp
@@ -47,11 +47,24 @@ int naturalCompare(const QString& a, const QString& b)
return c;
}();
+
+ // todo: remove this once the fix is released
+ // see https://bugreports.qt.io/projects/QTBUG/issues/QTBUG-81673
+ // and https://codereview.qt-project.org/c/qt/qtbase/+/287966/5/src/corelib/text/qcollator_win.cpp
+ if (!a.size()) {
+ return b.size() ? -1 : 0;
+ }
+ if (!b.size()) {
+ return +1;
+ }
+
+
return c.compare(a, b);
}
bool canPreviewFile(
- PluginContainer& pluginContainer, bool isArchive, const QString& filename)
+ const PluginContainer& pluginContainer,
+ bool isArchive, const QString& filename)
{
if (isArchive) {
return false;
diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h
index 5d949ce9..c758573c 100644
--- a/src/modinfodialogfwd.h
+++ b/src/modinfodialogfwd.h
@@ -2,6 +2,7 @@
#define MODINFODIALOGFWD_H
#include "filerenamer.h"
+#include <QStyledItemDelegate>
class ModInfo;
using ModInfoPtr = QSharedPointer<ModInfo>;
@@ -22,7 +23,7 @@ enum class ModInfoTabIDs
class PluginContainer;
-bool canPreviewFile(PluginContainer& pluginContainer, bool isArchive, const QString& filename);
+bool canPreviewFile(const PluginContainer& pluginContainer, bool isArchive, const QString& filename);
bool canRunFile(bool isArchive, const QString& filename);
bool canOpenFile(bool isArchive, const QString& filename);
bool canExploreFile(bool isArchive, const QString& filename);
diff --git a/src/modlist.cpp b/src/modlist.cpp
index c0f18821..31eb8387 100644
--- a/src/modlist.cpp
+++ b/src/modlist.cpp
@@ -676,11 +676,6 @@ QVariant ModList::headerData(int section, Qt::Orientation orientation,
return getColumnToolTip(section);
} else if (role == Qt::TextAlignmentRole) {
return QVariant(Qt::AlignCenter);
- } else if (role == Qt::SizeHintRole) {
- QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section));
- temp.rwidth() += 20;
- temp.rheight() += 12;
- return temp;
}
}
return QAbstractItemModel::headerData(section, orientation, role);
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index 336be37d..c4f9e081 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -500,16 +500,6 @@ std::wstring OrganizerCore::crashDumpsPath() {
).toStdWString();
}
-bool OrganizerCore::getArchiveParsing() const
-{
- return m_ArchiveParsing;
-}
-
-void OrganizerCore::setArchiveParsing(const bool archiveParsing)
-{
- m_ArchiveParsing = archiveParsing;
-}
-
void OrganizerCore::setCurrentProfile(const QString &profileName)
{
if ((m_CurrentProfile != nullptr)
diff --git a/src/organizercore.h b/src/organizercore.h
index 6c9edb9f..4ee6ddc5 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -218,8 +218,16 @@ public:
bool onFinishedRun(const std::function<void (const QString &, unsigned int)> &func);
void refreshModList(bool saveChanges = true);
QStringList modsSortedByProfilePriority() const;
- bool getArchiveParsing() const;
- void setArchiveParsing(bool archiveParsing);
+
+ bool getArchiveParsing() const
+ {
+ return m_ArchiveParsing;
+ }
+
+ void setArchiveParsing(bool archiveParsing)
+ {
+ m_ArchiveParsing = archiveParsing;
+ }
public: // IPluginDiagnose interface
diff --git a/src/pch.h b/src/pch.h
index b7c5d695..030ee634 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -22,6 +22,7 @@
#include <string.h>
#include <string>
#include <tuple>
+#include <unordered_set>
#include <utility>
#include <vector>
#include <wchar.h>
@@ -37,6 +38,7 @@
#include <Shlwapi.h>
#include <tchar.h>
#include <wincred.h>
+#include <windowsx.h>
// boost
#include <boost/algorithm/string.hpp>
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp
index 3f2f4018..266fe35c 100644
--- a/src/pluginlist.cpp
+++ b/src/pluginlist.cpp
@@ -1315,11 +1315,6 @@ QVariant PluginList::headerData(int section, Qt::Orientation orientation,
return getColumnName(section);
} else if (role == Qt::ToolTipRole) {
return getColumnToolTip(section);
- } else if (role == Qt::SizeHintRole) {
- QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section));
- temp.rwidth() += 25;
- temp.rheight() += 12;
- return temp;
}
}
return QAbstractItemModel::headerData(section, orientation, role);
diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp
index 00bf319e..6e44cc91 100644
--- a/src/shared/directoryentry.cpp
+++ b/src/shared/directoryentry.cpp
@@ -19,7 +19,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "directoryentry.h"
#include "windows_error.h"
-#include "leaktrace.h"
#include "error_report.h"
#include <log.h>
#include <bsatk.h>
@@ -32,54 +31,94 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <algorithm>
#include <map>
#include <atomic>
-#include <QObject>
+namespace MOShared
+{
-namespace MOShared {
+using namespace MOBase;
+static const int MAXPATH_UNICODE = 32767;
-namespace log = MOBase::log;
+static std::wstring tail(const std::wstring &source, const size_t count)
+{
+ if (count >= source.length()) {
+ return source;
+ }
-static const int MAXPATH_UNICODE = 32767;
+ return source.substr(source.length() - count);
+}
-class OriginConnection {
+static bool SupportOptimizedFind()
+{
+ // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2, win 7 and newer
-public:
+ OSVERSIONINFOEX versionInfo;
+ versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
+ versionInfo.dwMajorVersion = 6;
+ versionInfo.dwMinorVersion = 1;
- typedef int Index;
- static const int INVALID_INDEX = INT_MIN;
+ ULONGLONG mask = ::VerSetConditionMask(
+ ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL),
+ VER_MINORVERSION, VER_GREATER_EQUAL);
+
+ return (::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, mask) == TRUE);
+}
+
+static bool DirCompareByName(const DirectoryEntry *lhs, const DirectoryEntry *rhs)
+{
+ return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
+}
+
+class OriginConnection
+{
public:
+ typedef int Index;
+ static const int INVALID_INDEX = INT_MIN;
OriginConnection()
: m_NextID(0)
{
- LEAK_TRACE;
}
- ~OriginConnection()
+ FilesOrigin& createOrigin(
+ const std::wstring &originName, const std::wstring &directory, int priority,
+ boost::shared_ptr<FileRegister> fileRegister,
+ boost::shared_ptr<OriginConnection> originConnection)
{
- LEAK_UNTRACE;
- }
-
- FilesOrigin& createOrigin(const std::wstring &originName, const std::wstring &directory, int priority,
- boost::shared_ptr<FileRegister> fileRegister, boost::shared_ptr<OriginConnection> originConnection) {
int newID = createID();
+
m_Origins[newID] = FilesOrigin(newID, originName, directory, priority, fileRegister, originConnection);
m_OriginsNameMap[originName] = newID;
m_OriginsPriorityMap[priority] = newID;
+
return m_Origins[newID];
}
- bool exists(const std::wstring &name) {
+ bool exists(const std::wstring &name)
+ {
return m_OriginsNameMap.find(name) != m_OriginsNameMap.end();
}
- FilesOrigin &getByID(Index ID) {
+ FilesOrigin &getByID(Index ID)
+ {
return m_Origins[ID];
}
- FilesOrigin &getByName(const std::wstring &name) {
+ const FilesOrigin* findByID(Index ID) const
+ {
+ auto itor = m_Origins.find(ID);
+
+ if (itor == m_Origins.end()) {
+ return nullptr;
+ } else {
+ return &itor->second;
+ }
+ }
+
+ FilesOrigin &getByName(const std::wstring &name)
+ {
std::map<std::wstring, int>::iterator iter = m_OriginsNameMap.find(name);
+
if (iter != m_OriginsNameMap.end()) {
return m_Origins[iter->second];
} else {
@@ -92,6 +131,7 @@ public:
void changePriorityLookup(int oldPriority, int newPriority)
{
auto iter = m_OriginsPriorityMap.find(oldPriority);
+
if (iter != m_OriginsPriorityMap.end()) {
Index idx = iter->second;
m_OriginsPriorityMap.erase(iter);
@@ -102,6 +142,7 @@ public:
void changeNameLookup(const std::wstring &oldName, const std::wstring &newName)
{
auto iter = m_OriginsNameMap.find(oldName);
+
if (iter != m_OriginsNameMap.end()) {
Index idx = iter->second;
m_OriginsNameMap.erase(iter);
@@ -112,184 +153,93 @@ public:
}
private:
-
- Index createID() {
- return m_NextID++;
- }
-
-private:
-
Index m_NextID;
-
std::map<Index, FilesOrigin> m_Origins;
std::map<std::wstring, Index> m_OriginsNameMap;
std::map<int, Index> m_OriginsPriorityMap;
-};
-
-
-//
-// FilesOrigin
-//
-
-
-void FilesOrigin::enable(bool enabled, time_t notAfter)
-{
- if (!enabled) {
- std::set<FileEntry::Index> copy = m_Files;
- m_FileRegister.lock()->removeOriginMulti(copy, m_ID, notAfter);
- m_Files.clear();
- }
- m_Disabled = !enabled;
-}
-
-
-void FilesOrigin::removeFile(FileEntry::Index index)
-{
- auto iter = m_Files.find(index);
- if (iter != m_Files.end()) {
- m_Files.erase(iter);
- }
-}
-
-
-
-static std::wstring tail(const std::wstring &source, const size_t count)
-{
- if (count >= source.length()) {
- return source;
- }
-
- return source.substr(source.length() - count);
-}
-
-
-FilesOrigin::FilesOrigin()
- : m_ID(0), m_Disabled(false), m_Name(), m_Path(), m_Priority(0)
-{
- LEAK_TRACE;
-}
-
-FilesOrigin::FilesOrigin(const FilesOrigin &reference)
- : m_ID(reference.m_ID)
- , m_Disabled(reference.m_Disabled)
- , m_Name(reference.m_Name)
- , m_Path(reference.m_Path)
- , m_Priority(reference.m_Priority)
- , m_FileRegister(reference.m_FileRegister)
- , m_OriginConnection(reference.m_OriginConnection)
-{
- LEAK_TRACE;
-}
-
-
-FilesOrigin::FilesOrigin(int ID, const std::wstring &name, const std::wstring &path, int priority, boost::shared_ptr<MOShared::FileRegister> fileRegister, boost::shared_ptr<MOShared::OriginConnection> originConnection)
- : m_ID(ID), m_Disabled(false), m_Name(name), m_Path(path), m_Priority(priority),
- m_FileRegister(fileRegister), m_OriginConnection(originConnection)
-{
- LEAK_TRACE;
-}
-
-FilesOrigin::~FilesOrigin()
-{
- LEAK_UNTRACE;
-}
-
-
-void FilesOrigin::setPriority(int priority)
-{
- m_OriginConnection.lock()->changePriorityLookup(m_Priority, priority);
-
- m_Priority = priority;
-}
-
-
-void FilesOrigin::setName(const std::wstring &name)
-{
- m_OriginConnection.lock()->changeNameLookup(m_Name, name);
- // change path too
- if (tail(m_Path, m_Name.length()) == m_Name) {
- m_Path = m_Path.substr(0, m_Path.length() - m_Name.length()).append(name);
+ Index createID()
+ {
+ return m_NextID++;
}
- m_Name = name;
-}
-
-std::vector<FileEntry::Ptr> FilesOrigin::getFiles() const
-{
- std::vector<FileEntry::Ptr> result;
- for (FileEntry::Index fileIdx : m_Files)
- if (FileEntry::Ptr p = m_FileRegister.lock()->getFile(fileIdx))
- result.push_back(p);
+};
- return result;
-}
-FileEntry::Ptr FilesOrigin::findFile(FileEntry::Index index) const
+FileEntry::FileEntry() :
+ m_Index(UINT_MAX), m_Name(), m_Origin(-1), m_Parent(nullptr),
+ m_FileSize(NoFileSize), m_CompressedFileSize(NoFileSize),
+ m_LastAccessed(time(nullptr))
{
- return m_FileRegister.lock()->getFile(index);
}
-bool FilesOrigin::containsArchive(std::wstring archiveName)
+FileEntry::FileEntry(Index index, const std::wstring &name, DirectoryEntry *parent) :
+ m_Index(index), m_Name(name), m_Origin(-1), m_Archive(L"", -1), m_Parent(parent),
+ m_FileSize(NoFileSize), m_CompressedFileSize(NoFileSize),
+ m_LastAccessed(time(nullptr))
{
- for (FileEntry::Index fileIdx : m_Files)
- if (FileEntry::Ptr p = m_FileRegister.lock()->getFile(fileIdx))
- if (p->isFromArchive(archiveName)) return true;
- return false;
}
-//
-// FileEntry
-//
-
-void FileEntry::addOrigin(int origin, FILETIME fileTime, const std::wstring &archive, int order)
+void FileEntry::addOrigin(
+ int origin, FILETIME fileTime, const std::wstring &archive, int order)
{
m_LastAccessed = time(nullptr);
if (m_Parent != nullptr) {
m_Parent->propagateOrigin(origin);
}
- // If this file has no previous origin, this mod is now the origin with no alternatives
if (m_Origin == -1) {
+ // If this file has no previous origin, this mod is now the origin with no
+ // alternatives
m_Origin = origin;
m_FileTime = fileTime;
m_Archive = std::pair<std::wstring, int>(archive, order);
}
+ else if (
+ (m_Parent != nullptr) && (
+ (m_Parent->getOriginByID(origin).getPriority() > m_Parent->getOriginByID(m_Origin).getPriority()) ||
+ (archive.size() == 0 && m_Archive.first.size() > 0 ))
+ ) {
+ // If this mod has a higher priority than the origin mod OR
+ // this mod has a loose file and the origin mod has an archived file,
+ // this mod is now the origin and the previous origin is the first alternative
- // If this mod has a higher priority than the origin mod OR
- // this mod has a loose file and the origin mod has an archived file,
- // this mod is now the origin and the previous origin is the first alternative
- else if ((m_Parent != nullptr)
- && ((m_Parent->getOriginByID(origin).getPriority() > m_Parent->getOriginByID(m_Origin).getPriority())
- || (archive.size() == 0 && m_Archive.first.size() > 0 ))) {
- if (std::find_if(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::pair<std::wstring, int>> &i) -> bool { return i.first == m_Origin; }) == m_Alternatives.end()) {
- m_Alternatives.push_back(std::pair<int, std::pair<std::wstring, int>>(m_Origin, m_Archive));
+ auto itor = std::find_if(
+ m_Alternatives.begin(), m_Alternatives.end(),
+ [&](auto&& i) { return i.first == m_Origin; });
+
+ if (itor == m_Alternatives.end()) {
+ m_Alternatives.push_back({m_Origin, m_Archive});
}
+
m_Origin = origin;
m_FileTime = fileTime;
m_Archive = std::pair<std::wstring, int>(archive, order);
}
-
- // This mod is just an alternative
else {
+ // This mod is just an alternative
bool found = false;
+
if (m_Origin == origin) {
// already an origin
return;
}
- for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
+
+ for (auto iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
if (iter->first == origin) {
// already an origin
return;
}
- if ((m_Parent != nullptr)
- && (m_Parent->getOriginByID(iter->first).getPriority() < m_Parent->getOriginByID(origin).getPriority())) {
- m_Alternatives.insert(iter, std::pair<int, std::pair<std::wstring, int>>(origin, std::pair<std::wstring, int>(archive, order)));
+
+ if ((m_Parent != nullptr) &&
+ (m_Parent->getOriginByID(iter->first).getPriority() < m_Parent->getOriginByID(origin).getPriority())) {
+ m_Alternatives.insert(iter, {origin, {archive, order}});
found = true;
break;
}
}
+
if (!found) {
- m_Alternatives.push_back(std::pair<int, std::pair<std::wstring, int>>(origin, std::pair<std::wstring, int>(archive, order)));
+ m_Alternatives.push_back({origin, {archive, order}});
}
}
}
@@ -299,10 +249,9 @@ bool FileEntry::removeOrigin(int origin)
if (m_Origin == origin) {
if (!m_Alternatives.empty()) {
// find alternative with the highest priority
- std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator currentIter = m_Alternatives.begin();
- for (std::vector<std::pair<int, std::pair<std::wstring, int>>>::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
+ auto currentIter = m_Alternatives.begin();
+ for (auto iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) {
if (iter->first != origin) {
-
//Both files are not from archives.
if (!iter->second.first.size() && !currentIter->second.first.size()) {
if ((m_Parent->getOriginByID(iter->first).getPriority() > m_Parent->getOriginByID(currentIter->first).getPriority())) {
@@ -325,63 +274,44 @@ bool FileEntry::removeOrigin(int origin)
}
}
}
+
int currentID = currentIter->first;
m_Archive = currentIter->second;
m_Alternatives.erase(currentIter);
m_Origin = currentID;
-
- // now we need to update the file time...
- //std::wstring filePath = getFullPath();
- //HANDLE file = ::CreateFile(filePath.c_str(), GENERIC_READ | GENERIC_WRITE,
- // 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
- //if (!::GetFileTime(file, nullptr, nullptr, &m_FileTime)) {
- // maybe this file is in a bsa, but there is no easy way to find out which. User should refresh
- // the view to find out
- //m_Archive = std::pair<std::wstring, int>(L"bsa?", -1);
- //} else {
- //m_Archive = std::pair<std::wstring, int>(L"", -1);
- //}
-
- //::CloseHandle(file);
-
} else {
m_Origin = -1;
m_Archive = std::pair<std::wstring, int>(L"", -1);
return true;
}
} else {
- auto newEnd = std::remove_if(m_Alternatives.begin(), m_Alternatives.end(), [&](auto &i) -> bool { return i.first == origin; });
- if (newEnd != m_Alternatives.end())
+ auto newEnd = std::remove_if(
+ m_Alternatives.begin(), m_Alternatives.end(),
+ [&](auto &i) { return i.first == origin; });
+
+ if (newEnd != m_Alternatives.end()) {
m_Alternatives.erase(newEnd, m_Alternatives.end());
+ }
}
return false;
}
-FileEntry::FileEntry()
- : m_Index(UINT_MAX), m_Name(), m_Origin(-1), m_Parent(nullptr), m_LastAccessed(time(nullptr))
-{
- LEAK_TRACE;
-}
-
-FileEntry::FileEntry(Index index, const std::wstring &name, DirectoryEntry *parent)
- : m_Index(index), m_Name(name), m_Origin(-1), m_Archive(L"", -1), m_Parent(parent), m_LastAccessed(time(nullptr))
-{
- LEAK_TRACE;
-}
-
-FileEntry::~FileEntry()
-{
- LEAK_UNTRACE;
-}
-
void FileEntry::sortOrigins()
{
- m_Alternatives.push_back(std::pair<int, std::pair<std::wstring, int>>(m_Origin, m_Archive));
- std::sort(m_Alternatives.begin(), m_Alternatives.end(), [&](const std::pair<int, std::pair<std::wstring, int>> &LHS, const std::pair<int, std::pair<std::wstring, int>> &RHS) -> bool {
+ m_Alternatives.push_back({m_Origin, m_Archive});
+
+ std::sort(m_Alternatives.begin(), m_Alternatives.end(), [&](auto&& LHS, auto&& RHS) {
if (!LHS.second.first.size() && !RHS.second.first.size()) {
- int l = m_Parent->getOriginByID(LHS.first).getPriority(); if (l < 0) l = INT_MAX;
- int r = m_Parent->getOriginByID(RHS.first).getPriority(); if (r < 0) r = INT_MAX;
+ int l = m_Parent->getOriginByID(LHS.first).getPriority();
+ if (l < 0) {
+ l = INT_MAX;
+ }
+
+ int r = m_Parent->getOriginByID(RHS.first).getPriority();
+ if (r < 0) {
+ r = INT_MAX;
+ }
return l < r;
}
@@ -393,9 +323,13 @@ void FileEntry::sortOrigins()
return l < r;
}
- if (RHS.second.first.size()) return false;
+ if (RHS.second.first.size()) {
+ return false;
+ }
+
return true;
});
+
if (!m_Alternatives.empty()) {
m_Origin = m_Alternatives.back().first;
m_Archive = m_Alternatives.back().second;
@@ -403,6 +337,55 @@ void FileEntry::sortOrigins()
}
}
+bool FileEntry::isFromArchive(std::wstring archiveName) const
+{
+ if (archiveName.length() == 0) {
+ return m_Archive.first.length() != 0;
+ }
+
+ if (m_Archive.first.compare(archiveName) == 0) {
+ return true;
+ }
+
+ for (auto alternative : m_Alternatives) {
+ if (alternative.second.first.compare(archiveName) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+std::wstring FileEntry::getFullPath(int originID) const
+{
+ if (originID == -1) {
+ bool ignore = false;
+ originID = getOrigin(ignore);
+ }
+
+ // base directory for origin
+ const auto* o = m_Parent->findOriginByID(originID);
+ if (!o) {
+ return {};
+ }
+
+ std::wstring result = o->getPath();
+
+ // all intermediate directories
+ recurseParents(result, m_Parent);
+
+ return result + L"\\" + m_Name;
+}
+
+std::wstring FileEntry::getRelativePath() const
+{
+ std::wstring result;
+
+ // all intermediate directories
+ recurseParents(result, m_Parent);
+
+ return result + L"\\" + m_Name;
+}
bool FileEntry::recurseParents(std::wstring &path, const DirectoryEntry *parent) const
{
@@ -413,97 +396,288 @@ bool FileEntry::recurseParents(std::wstring &path, const DirectoryEntry *parent)
if (recurseParents(path, parent->getParent())) {
path.append(L"\\").append(parent->getName());
}
+
return true;
}
}
-std::wstring FileEntry::getFullPath() const
+
+FilesOrigin::FilesOrigin()
+ : m_ID(0), m_Disabled(false), m_Name(), m_Path(), m_Priority(0)
{
- std::wstring result;
- bool ignore = false;
- result = m_Parent->getOriginByID(getOrigin(ignore)).getPath(); //base directory for origin
- recurseParents(result, m_Parent); // all intermediate directories
- return result + L"\\" + m_Name;
}
-std::wstring FileEntry::getRelativePath() const
+FilesOrigin::FilesOrigin(const FilesOrigin &reference)
+ : m_ID(reference.m_ID)
+ , m_Disabled(reference.m_Disabled)
+ , m_Name(reference.m_Name)
+ , m_Path(reference.m_Path)
+ , m_Priority(reference.m_Priority)
+ , m_FileRegister(reference.m_FileRegister)
+ , m_OriginConnection(reference.m_OriginConnection)
{
- std::wstring result;
- recurseParents(result, m_Parent); // all intermediate directories
- return result + L"\\" + m_Name;
}
-bool FileEntry::isFromArchive(std::wstring archiveName)
+FilesOrigin::FilesOrigin(
+ int ID, const std::wstring &name, const std::wstring &path, int priority,
+ boost::shared_ptr<MOShared::FileRegister> fileRegister,
+ boost::shared_ptr<MOShared::OriginConnection> originConnection) :
+ m_ID(ID), m_Disabled(false), m_Name(name), m_Path(path),
+ m_Priority(priority), m_FileRegister(fileRegister),
+ m_OriginConnection(originConnection)
{
- if (archiveName.length() == 0) return m_Archive.first.length() != 0;
- if (m_Archive.first.compare(archiveName) == 0) return true;
- for (auto alternative : m_Alternatives) {
- if (alternative.second.first.compare(archiveName) == 0) return true;
+}
+
+void FilesOrigin::setPriority(int priority)
+{
+ m_OriginConnection.lock()->changePriorityLookup(m_Priority, priority);
+
+ m_Priority = priority;
+}
+
+void FilesOrigin::setName(const std::wstring &name)
+{
+ m_OriginConnection.lock()->changeNameLookup(m_Name, name);
+
+ // change path too
+ if (tail(m_Path, m_Name.length()) == m_Name) {
+ m_Path = m_Path.substr(0, m_Path.length() - m_Name.length()).append(name);
}
- return false;
+
+ m_Name = name;
}
+std::vector<FileEntry::Ptr> FilesOrigin::getFiles() const
+{
+ std::vector<FileEntry::Ptr> result;
-//
-// DirectoryEntry
-//
-DirectoryEntry::DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID)
- : m_OriginConnection(new OriginConnection),
- m_Name(name), m_Parent(parent), m_Populated(false), m_TopLevel(true)
+ for (FileEntry::Index fileIdx : m_Files) {
+ if (FileEntry::Ptr p = m_FileRegister.lock()->getFile(fileIdx)) {
+ result.push_back(p);
+ }
+ }
+
+ return result;
+}
+
+FileEntry::Ptr FilesOrigin::findFile(FileEntry::Index index) const
{
- m_FileRegister.reset(new FileRegister(m_OriginConnection));
- m_Origins.insert(originID);
- LEAK_TRACE;
+ return m_FileRegister.lock()->getFile(index);
}
-DirectoryEntry::DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID,
- boost::shared_ptr<FileRegister> fileRegister, boost::shared_ptr<OriginConnection> originConnection)
- : m_FileRegister(fileRegister), m_OriginConnection(originConnection),
- m_Name(name), m_Parent(parent), m_Populated(false), m_TopLevel(false)
+void FilesOrigin::enable(bool enabled, time_t notAfter)
{
- LEAK_TRACE;
- m_Origins.insert(originID);
+ if (!enabled) {
+ std::set<FileEntry::Index> copy = m_Files;
+ m_FileRegister.lock()->removeOriginMulti(copy, m_ID, notAfter);
+ m_Files.clear();
+ }
+
+ m_Disabled = !enabled;
}
+void FilesOrigin::removeFile(FileEntry::Index index)
+{
+ auto iter = m_Files.find(index);
+
+ if (iter != m_Files.end()) {
+ m_Files.erase(iter);
+ }
+}
-DirectoryEntry::~DirectoryEntry()
+bool FilesOrigin::containsArchive(std::wstring archiveName)
{
- LEAK_UNTRACE;
- clear();
+ for (FileEntry::Index fileIdx : m_Files) {
+ if (FileEntry::Ptr p = m_FileRegister.lock()->getFile(fileIdx)) {
+ if (p->isFromArchive(archiveName)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
-const std::wstring &DirectoryEntry::getName() const
+FileRegister::FileRegister(boost::shared_ptr<OriginConnection> originConnection)
+ : m_OriginConnection(originConnection)
{
- return m_Name;
}
+bool FileRegister::indexValid(FileEntry::Index index) const
+{
+ return (m_Files.find(index) != m_Files.end());
+}
-void DirectoryEntry::clear()
+FileEntry::Ptr FileRegister::createFile(const std::wstring &name, DirectoryEntry *parent)
{
- m_Files.clear();
- for (DirectoryEntry *entry : m_SubDirectories) {
- delete entry;
+ FileEntry::Index index = generateIndex();
+
+ auto r = m_Files.insert_or_assign(
+ index, FileEntry::Ptr(new FileEntry(index, name, parent)));
+
+ return r.first->second;
+}
+
+FileEntry::Ptr FileRegister::getFile(FileEntry::Index index) const
+{
+ auto iter = m_Files.find(index);
+
+ if (iter != m_Files.end()) {
+ return iter->second;
+ } else {
+ return FileEntry::Ptr();
}
- m_SubDirectories.clear();
}
+bool FileRegister::removeFile(FileEntry::Index index)
+{
+ auto iter = m_Files.find(index);
+
+ if (iter != m_Files.end()) {
+ unregisterFile(iter->second);
+ m_Files.erase(index);
+ return true;
+ } else {
+ log::error(QObject::tr("invalid file index for remove: {}").toStdString(), index);
+ return false;
+ }
+}
-FilesOrigin &DirectoryEntry::createOrigin(const std::wstring &originName, const std::wstring &directory, int priority)
+void FileRegister::removeOrigin(FileEntry::Index index, int originID)
{
- if (m_OriginConnection->exists(originName)) {
- FilesOrigin &origin = m_OriginConnection->getByName(originName);
- origin.enable(true);
- return origin;
+ auto iter = m_Files.find(index);
+
+ if (iter != m_Files.end()) {
+ if (iter->second->removeOrigin(originID)) {
+ unregisterFile(iter->second);
+ m_Files.erase(iter);
+ }
} else {
- return m_OriginConnection->createOrigin(originName, directory, priority, m_FileRegister, m_OriginConnection);
+ log::error(QObject::tr("invalid file index for remove (for origin): {}").toStdString(), index);
}
}
+void FileRegister::removeOriginMulti(
+ std::set<FileEntry::Index> indices, int originID, time_t notAfter)
+{
+ std::vector<FileEntry::Ptr> removedFiles;
+
+ for (auto iter = indices.begin(); iter != indices.end(); ) {
+ auto pos = m_Files.find(*iter);
+
+ if (pos != m_Files.end()
+ && (pos->second->lastAccessed() < notAfter)
+ && pos->second->removeOrigin(originID)) {
+ removedFiles.push_back(pos->second);
+ m_Files.erase(pos);
+ ++iter;
+ } else {
+ indices.erase(iter++);
+ }
+ }
+
+ // optimization: this is only called when disabling an origin and in this case
+ // we don't have to remove the file from the origin
+
+ // need to remove files from their parent directories. multiple ways to go
+ // about this:
+ // a) for each file, search its parents file-list (preferably by name) and
+ // remove what is found
+ // b) gather the parent directories, go through the file list for each once
+ // and remove all files that have been removed
+ //
+ // the latter should be faster when there are many files in few directories.
+ // since this is called only when disabling an origin that is probably
+ // frequently the case
+
+ std::set<DirectoryEntry*> parents;
+ for (const FileEntry::Ptr &file : removedFiles) {
+ if (file->getParent() != nullptr) {
+ parents.insert(file->getParent());
+ }
+ }
+
+ for (DirectoryEntry *parent : parents) {
+ parent->removeFiles(indices);
+ }
+}
+
+void FileRegister::sortOrigins()
+{
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ iter->second->sortOrigins();
+ }
+}
+
+FileEntry::Index FileRegister::generateIndex()
+{
+ static std::atomic<FileEntry::Index> sIndex(0);
+ return sIndex++;
+}
+
+void FileRegister::unregisterFile(FileEntry::Ptr file)
+{
+ bool ignore;
+
+ // unregister from origin
+ int originID = file->getOrigin(ignore);
+ m_OriginConnection->getByID(originID).removeFile(file->getIndex());
+ const auto& alternatives = file->getAlternatives();
+
+ for (auto iter = alternatives.begin(); iter != alternatives.end(); ++iter) {
+ m_OriginConnection->getByID(iter->first).removeFile(file->getIndex());
+ }
+
+ // unregister from directory
+ if (file->getParent() != nullptr) {
+ file->getParent()->removeFile(file->getIndex());
+ }
+}
+
+
+DirectoryEntry::DirectoryEntry(
+ const std::wstring &name, DirectoryEntry *parent, int originID) :
+ m_OriginConnection(new OriginConnection),
+ m_Name(name), m_Parent(parent), m_Populated(false), m_TopLevel(true)
+{
+ m_FileRegister.reset(new FileRegister(m_OriginConnection));
+ m_Origins.insert(originID);
+}
+
+DirectoryEntry::DirectoryEntry(
+ const std::wstring &name, DirectoryEntry *parent, int originID,
+ boost::shared_ptr<FileRegister> fileRegister,
+ boost::shared_ptr<OriginConnection> originConnection) :
+ m_FileRegister(fileRegister), m_OriginConnection(originConnection),
+ m_Name(name), m_Parent(parent), m_Populated(false), m_TopLevel(false)
+{
+ m_Origins.insert(originID);
+}
+
+DirectoryEntry::~DirectoryEntry()
+{
+ clear();
+}
+
+void DirectoryEntry::clear()
+{
+ m_Files.clear();
+ m_FilesLookup.clear();
+
+ for (DirectoryEntry *entry : m_SubDirectories) {
+ delete entry;
+ }
+
+ m_SubDirectories.clear();
+ m_SubDirectoriesLookup.clear();
+}
-void DirectoryEntry::addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority)
+void DirectoryEntry::addFromOrigin(
+ const std::wstring &originName, const std::wstring &directory, int priority)
{
FilesOrigin &origin = createOrigin(originName, directory, priority);
+
if (directory.length() != 0) {
boost::scoped_array<wchar_t> buffer(new wchar_t[MAXPATH_UNICODE + 1]);
memset(buffer.get(), L'\0', MAXPATH_UNICODE + 1);
@@ -511,11 +685,13 @@ void DirectoryEntry::addFromOrigin(const std::wstring &originName, const std::ws
buffer.get()[offset] = L'\0';
addFiles(origin, buffer.get(), offset);
}
+
m_Populated = true;
}
-
-void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority, int order)
+void DirectoryEntry::addFromBSA(
+ const std::wstring &originName, std::wstring &directory,
+ const std::wstring &fileName, int priority, int order)
{
FilesOrigin &origin = createOrigin(originName, directory, priority);
@@ -523,6 +699,7 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di
if (::GetFileAttributesExW(fileName.c_str(), GetFileExInfoStandard, &fileData) == 0) {
throw windows_error(QObject::tr("failed to determine file time").toStdString());
}
+
FILETIME now;
::GetSystemTimeAsFileTime(&now);
@@ -541,9 +718,15 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di
if (!containsArchive(fileName.substr(namePos)) || ::CompareFileTime(&fileData.ftLastWriteTime, &now) > 0) {
BSA::Archive archive;
BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str(), false);
+
if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
std::ostringstream stream;
- stream << QObject::tr("invalid bsa file: ").toStdString() << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError();
+
+ stream
+ << QObject::tr("invalid bsa file: ").toStdString()
+ << ToString(fileName, false)
+ << " error code " << res << " - " << ::GetLastError();
+
throw std::runtime_error(stream.str());
}
@@ -555,128 +738,232 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di
void DirectoryEntry::propagateOrigin(int origin)
{
m_Origins.insert(origin);
+
if (m_Parent != nullptr) {
m_Parent->propagateOrigin(origin);
}
}
+bool DirectoryEntry::originExists(const std::wstring &name) const
+{
+ return m_OriginConnection->exists(name);
+}
-static bool SupportOptimizedFind()
+FilesOrigin &DirectoryEntry::getOriginByID(int ID) const
{
- // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2, win 7 and newer
+ return m_OriginConnection->getByID(ID);
+}
- OSVERSIONINFOEX versionInfo;
- versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
- versionInfo.dwMajorVersion = 6;
- versionInfo.dwMinorVersion = 1;
- ULONGLONG mask = ::VerSetConditionMask(
- ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL),
- VER_MINORVERSION, VER_GREATER_EQUAL);
+FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const
+{
+ return m_OriginConnection->getByName(name);
+}
- bool res = ::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, mask) == TRUE;
- return res;
+const FilesOrigin* DirectoryEntry::findOriginByID(int ID) const
+{
+ return m_OriginConnection->findByID(ID);
}
+int DirectoryEntry::anyOrigin() const
+{
+ bool ignore;
-static bool DirCompareByName(const DirectoryEntry *lhs, const DirectoryEntry *rhs)
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
+ if ((entry.get() != nullptr) && !entry->isFromArchive()) {
+ return entry->getOrigin(ignore);
+ }
+ }
+
+ // if we got here, no file directly within this directory is a valid indicator for a mod, thus
+ // we continue looking in subdirectories
+ for (DirectoryEntry *entry : m_SubDirectories) {
+ int res = entry->anyOrigin();
+ if (res != -1){
+ return res;
+ }
+ }
+
+ return *(m_Origins.begin());
+}
+
+std::vector<FileEntry::Ptr> DirectoryEntry::getFiles() const
{
- return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
+ std::vector<FileEntry::Ptr> result;
+
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ result.push_back(m_FileRegister->getFile(iter->second));
+ }
+
+ return result;
}
+DirectoryEntry *DirectoryEntry::findSubDirectory(
+ const std::wstring &name, bool alreadyLowerCase) const
+{
+ SubDirectoriesLookup::const_iterator itor;
-void DirectoryEntry::addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset)
+ if (alreadyLowerCase) {
+ itor = m_SubDirectoriesLookup.find(name);
+ } else {
+ itor = m_SubDirectoriesLookup.find(ToLowerCopy(name));
+ }
+
+ if (itor == m_SubDirectoriesLookup.end()) {
+ return nullptr;
+ }
+
+ return itor->second;
+}
+
+DirectoryEntry *DirectoryEntry::findSubDirectoryRecursive(const std::wstring &path)
{
- WIN32_FIND_DATAW findData;
+ return getSubDirectoryRecursive(path, false, -1);
+}
- _snwprintf_s(buffer + bufferOffset, MAXPATH_UNICODE - bufferOffset, _TRUNCATE, L"\\*");
+const FileEntry::Ptr DirectoryEntry::findFile(
+ const std::wstring &name, bool alreadyLowerCase) const
+{
+ FilesLookup::const_iterator iter;
- HANDLE searchHandle = nullptr;
+ if (alreadyLowerCase) {
+ iter = m_FilesLookup.find(FileKey(name));
+ } else {
+ iter = m_FilesLookup.find(FileKey(ToLowerCopy(name)));
+ }
- if (SupportOptimizedFind()) {
- searchHandle = ::FindFirstFileExW(buffer, FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr,
- FIND_FIRST_EX_LARGE_FETCH);
+ if (iter != m_FilesLookup.end()) {
+ return m_FileRegister->getFile(iter->second);
} else {
- searchHandle = ::FindFirstFileExW(buffer, FindExInfoStandard, &findData, FindExSearchNameMatch, nullptr, 0);
+ return FileEntry::Ptr();
}
+}
- if (searchHandle != INVALID_HANDLE_VALUE) {
- BOOL result = true;
- while (result) {
- if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
- if ((wcscmp(findData.cFileName, L".") != 0) &&
- (wcscmp(findData.cFileName, L"..") != 0)) {
- int offset = _snwprintf(buffer + bufferOffset, MAXPATH_UNICODE, L"\\%ls", findData.cFileName);
- // recurse into subdirectories
- getSubDirectory(findData.cFileName, true, origin.getID())->addFiles(origin, buffer, bufferOffset + offset);
- }
- } else {
- insert(findData.cFileName, origin, findData.ftLastWriteTime, L"", -1);
- }
- result = ::FindNextFileW(searchHandle, &findData);
- }
+const FileEntry::Ptr DirectoryEntry::findFile(const FileKey& key) const
+{
+ auto iter = m_FilesLookup.find(key);
+
+ if (iter != m_FilesLookup.end()) {
+ return m_FileRegister->getFile(iter->second);
+ } else {
+ return FileEntry::Ptr();
}
- std::sort(m_SubDirectories.begin(), m_SubDirectories.end(), &DirCompareByName);
- ::FindClose(searchHandle);
}
+bool DirectoryEntry::hasFile(const std::wstring& name) const
+{
+ return m_Files.contains(ToLowerCopy(name));
+}
-void DirectoryEntry::addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName, int order)
+bool DirectoryEntry::containsArchive(std::wstring archiveName)
{
- // add files
- for (unsigned int fileIdx = 0; fileIdx < archiveFolder->getNumFiles(); ++fileIdx) {
- BSA::File::Ptr file = archiveFolder->getFile(fileIdx);
- insert(ToWString(file->getName(), true), origin, fileTime, archiveName, order);
+ for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
+ FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
+ if (entry->isFromArchive(archiveName)) {
+ return true;
+ }
}
- // recurse into subdirectories
- for (unsigned int folderIdx = 0; folderIdx < archiveFolder->getNumSubFolders(); ++folderIdx) {
- BSA::Folder::Ptr folder = archiveFolder->getSubFolder(folderIdx);
- DirectoryEntry *folderEntry = getSubDirectoryRecursive(ToWString(folder->getName(), true), true, origin.getID());
+ return false;
+}
- folderEntry->addFiles(origin, folder, fileTime, archiveName, order);
+const FileEntry::Ptr DirectoryEntry::searchFile(
+ const std::wstring &path, const DirectoryEntry **directory) const
+{
+ if (directory != nullptr) {
+ *directory = nullptr;
}
-}
+ if ((path.length() == 0) || (path == L"*")) {
+ // no file name -> the path ended on a (back-)slash
+ if (directory != nullptr) {
+ *directory = this;
+ }
+
+ return FileEntry::Ptr();
+ }
-bool DirectoryEntry::removeFile(const std::wstring &filePath, int *origin)
+ const size_t len = path.find_first_of(L"\\/");
+
+ if (len == std::string::npos) {
+ // no more path components
+ auto iter = m_Files.find(ToLowerCopy(path));
+
+ if (iter != m_Files.end()) {
+ return m_FileRegister->getFile(iter->second);
+ } else if (directory != nullptr) {
+ DirectoryEntry *temp = findSubDirectory(path);
+ if (temp != nullptr) {
+ *directory = temp;
+ }
+ }
+ } else {
+ // file is in a subdirectory, recurse into the matching subdirectory
+ std::wstring pathComponent = path.substr(0, len);
+ DirectoryEntry *temp = findSubDirectory(pathComponent);
+
+ if (temp != nullptr) {
+ if (len >= path.size()) {
+ log::error(QObject::tr("unexpected end of path").toStdString());
+ return FileEntry::Ptr();
+ }
+
+ return temp->searchFile(path.substr(len + 1), directory);
+ }
+ }
+
+ return FileEntry::Ptr();
+}
+
+void DirectoryEntry::insertFile(
+ const std::wstring &filePath, FilesOrigin &origin, FILETIME fileTime)
{
size_t pos = filePath.find_first_of(L"\\/");
+
if (pos == std::string::npos) {
- return this->remove(filePath, origin);
+ this->insert(filePath, origin, fileTime, std::wstring(), -1);
} else {
std::wstring dirName = filePath.substr(0, pos);
std::wstring rest = filePath.substr(pos + 1);
- DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false);
- if (entry != nullptr) {
- return entry->removeFile(rest, origin);
- } else {
- return false;
- }
+ getSubDirectoryRecursive(dirName, true, origin.getID())->insertFile(rest, origin, fileTime);
}
}
-void DirectoryEntry::removeDirRecursive()
+void DirectoryEntry::removeFile(FileEntry::Index index)
{
- while (!m_Files.empty()) {
- m_FileRegister->removeFile(m_Files.begin()->second);
+ removeFileFromList(index);
+}
+
+bool DirectoryEntry::removeFile(const std::wstring &filePath, int *origin)
+{
+ size_t pos = filePath.find_first_of(L"\\/");
+
+ if (pos == std::string::npos) {
+ return this->remove(filePath, origin);
}
- for (DirectoryEntry *entry : m_SubDirectories) {
- entry->removeDirRecursive();
- delete entry;
+ std::wstring dirName = filePath.substr(0, pos);
+ std::wstring rest = filePath.substr(pos + 1);
+ DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false);
+
+ if (entry != nullptr) {
+ return entry->removeFile(rest, origin);
+ } else {
+ return false;
}
- m_SubDirectories.clear();
}
void DirectoryEntry::removeDir(const std::wstring &path)
{
size_t pos = path.find_first_of(L"\\/");
+
if (pos == std::string::npos) {
for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) {
DirectoryEntry *entry = *iter;
+
if (CaseInsensitiveEqual(entry->getName(), path)) {
entry->removeDirRecursive();
- m_SubDirectories.erase(iter);
+ removeDirectoryFromList(iter);
delete entry;
break;
}
@@ -685,235 +972,188 @@ void DirectoryEntry::removeDir(const std::wstring &path)
std::wstring dirName = path.substr(0, pos);
std::wstring rest = path.substr(pos + 1);
DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false);
+
if (entry != nullptr) {
entry->removeDir(rest);
}
}
}
-bool DirectoryEntry::hasContentsFromOrigin(int originID) const
+bool DirectoryEntry::remove(const std::wstring &fileName, int *origin)
{
- return m_Origins.find(originID) != m_Origins.end();
-}
+ const auto lcFileName = ToLowerCopy(fileName);
-void DirectoryEntry::insertFile(const std::wstring &filePath, FilesOrigin &origin, FILETIME fileTime)
-{
- size_t pos = filePath.find_first_of(L"\\/");
- if (pos == std::string::npos) {
- this->insert(filePath, origin, fileTime, std::wstring(), -1);
- } else {
- std::wstring dirName = filePath.substr(0, pos);
- std::wstring rest = filePath.substr(pos + 1);
- getSubDirectoryRecursive(dirName, true, origin.getID())->insertFile(rest, origin, fileTime);
+ auto iter = m_Files.find(lcFileName);
+ bool b = false;
+
+ if (iter != m_Files.end()) {
+ if (origin != nullptr) {
+ FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
+ if (entry.get() != nullptr) {
+ bool ignore;
+ *origin = entry->getOrigin(ignore);
+ }
+ }
+
+ b = m_FileRegister->removeFile(iter->second);
}
+
+ return b;
}
+bool DirectoryEntry::hasContentsFromOrigin(int originID) const
+{
+ return m_Origins.find(originID) != m_Origins.end();
+}
-void DirectoryEntry::removeFile(FileEntry::Index index)
+FilesOrigin &DirectoryEntry::createOrigin(
+ const std::wstring &originName, const std::wstring &directory, int priority)
{
- if (!m_Files.empty()) {
- auto iter = std::find_if(m_Files.begin(), m_Files.end(),
- [&index](const std::pair<std::wstring, FileEntry::Index> &iter) -> bool {
- return iter.second == index; } );
- if (iter != m_Files.end()) {
- m_Files.erase(iter);
- } else {
- log::error(
- QObject::tr("file \"{}\" not in directory \"{}\"").toStdString(),
- m_FileRegister->getFile(index)->getName(), this->getName());
- }
+ if (m_OriginConnection->exists(originName)) {
+ FilesOrigin &origin = m_OriginConnection->getByName(originName);
+ origin.enable(true);
+ return origin;
} else {
- log::error(
- QObject::tr("file \"{}\" not in directory \"{}\", directory empty").toStdString(),
- m_FileRegister->getFile(index)->getName(), this->getName());
+ return m_OriginConnection->createOrigin(
+ originName, directory, priority, m_FileRegister, m_OriginConnection);
}
}
-
void DirectoryEntry::removeFiles(const std::set<FileEntry::Index> &indices)
{
- for (auto iter = m_Files.begin(); iter != m_Files.end();) {
- if (indices.find(iter->second) != indices.end()) {
- m_Files.erase(iter++);
- } else {
- ++iter;
- }
- }
+ removeFilesFromList(indices);
}
-bool DirectoryEntry::containsArchive(std::wstring archiveName)
+FileEntry::Ptr DirectoryEntry::insert(
+ const std::wstring &fileName, FilesOrigin &origin, FILETIME fileTime,
+ const std::wstring &archive, int order)
{
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
- if (entry->isFromArchive(archiveName)) return true;
- }
- return false;
-}
+ std::wstring fileNameLower = ToLowerCopy(fileName);
-int DirectoryEntry::anyOrigin() const
-{
- bool ignore;
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
- if ((entry.get() != nullptr) && !entry->isFromArchive()) {
- return entry->getOrigin(ignore);
- }
- }
+ auto iter = m_Files.find(fileNameLower);
+ FileEntry::Ptr file;
- // if we got here, no file directly within this directory is a valid indicator for a mod, thus
- // we continue looking in subdirectories
- for (DirectoryEntry *entry : m_SubDirectories) {
- int res = entry->anyOrigin();
- if (res != -1){
- return res;
- }
+ if (iter != m_Files.end()) {
+ file = m_FileRegister->getFile(iter->second);
+ } else {
+ file = m_FileRegister->createFile(fileName, this);
+ addFileToList(std::move(fileNameLower), file->getIndex());
+
+ // fileNameLower has moved from this point
}
- return *(m_Origins.begin());
-}
+ file->addOrigin(origin.getID(), fileTime, archive, order);
+ origin.addFile(file->getIndex());
-bool DirectoryEntry::originExists(const std::wstring &name) const
-{
- return m_OriginConnection->exists(name);
+ return file;
}
-
-FilesOrigin &DirectoryEntry::getOriginByID(int ID) const
+void DirectoryEntry::addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset)
{
- return m_OriginConnection->getByID(ID);
-}
+ WIN32_FIND_DATAW findData;
+ _snwprintf_s(buffer + bufferOffset, MAXPATH_UNICODE - bufferOffset, _TRUNCATE, L"\\*");
-FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const
-{
- return m_OriginConnection->getByName(name);
-}
+ HANDLE searchHandle = nullptr;
-/*
-int DirectoryEntry::getOrigin(const std::wstring &path, bool &archive)
-{
- const DirectoryEntry *directory = nullptr;
- const FileEntry::Ptr file = searchFile(path, &directory);
- if (file.get() != nullptr) {
- return file->getOrigin(archive);
+ if (SupportOptimizedFind()) {
+ searchHandle = ::FindFirstFileExW(
+ buffer, FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr,
+ FIND_FIRST_EX_LARGE_FETCH);
} else {
- if (directory != nullptr) {
- return directory->anyOrigin();
- } else {
- return -1;
- }
+ searchHandle = ::FindFirstFileExW(
+ buffer, FindExInfoStandard, &findData, FindExSearchNameMatch, nullptr, 0);
}
-}*/
-std::vector<FileEntry::Ptr> DirectoryEntry::getFiles() const
-{
- std::vector<FileEntry::Ptr> result;
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- result.push_back(m_FileRegister->getFile(iter->second));
- }
- return result;
-}
+ if (searchHandle != INVALID_HANDLE_VALUE) {
+ BOOL result = true;
+ while (result) {
+ if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+ if ((wcscmp(findData.cFileName, L".") != 0) &&
+ (wcscmp(findData.cFileName, L"..") != 0)) {
+ int offset = _snwprintf(buffer + bufferOffset, MAXPATH_UNICODE, L"\\%ls", findData.cFileName);
-const FileEntry::Ptr DirectoryEntry::searchFile(const std::wstring &path, const DirectoryEntry **directory) const
-{
- if (directory != nullptr) {
- *directory = nullptr;
- }
+ // recurse into subdirectories
+ DirectoryEntry* sd = getSubDirectory(findData.cFileName, true, origin.getID());
+ sd->addFiles(origin, buffer, bufferOffset + offset);
+ }
+ } else {
+ insert(findData.cFileName, origin, findData.ftLastWriteTime, L"", -1);
+ }
- if ((path.length() == 0) || (path == L"*")) {
- // no file name -> the path ended on a (back-)slash
- if (directory != nullptr) {
- *directory = this;
+ result = ::FindNextFileW(searchHandle, &findData);
}
- return FileEntry::Ptr();
}
- size_t len = path.find_first_of(L"\\/");
-
- if (len == std::string::npos) {
- // no more path components
- auto iter = m_Files.find(ToLower(path));
- if (iter != m_Files.end()) {
- return m_FileRegister->getFile(iter->second);
- } else if (directory != nullptr) {
- DirectoryEntry *temp = findSubDirectory(path);
- if (temp != nullptr) {
- *directory = temp;
- }
- }
- } else {
- // file is in in a subdirectory, recurse into the matching subdirectory
- std::wstring pathComponent = path.substr(0, len);
- DirectoryEntry *temp = findSubDirectory(pathComponent);
- if (temp != nullptr) {
- if (len >= path.size()) {
- log::error(QObject::tr("unexpected end of path").toStdString());
- return FileEntry::Ptr();
- }
- return temp->searchFile(path.substr(len + 1), directory);
- }
- }
- return FileEntry::Ptr();
+ std::sort(m_SubDirectories.begin(), m_SubDirectories.end(), &DirCompareByName);
+ ::FindClose(searchHandle);
}
-
-DirectoryEntry *DirectoryEntry::findSubDirectory(const std::wstring &name) const
+void DirectoryEntry::addFiles(
+ FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime,
+ const std::wstring &archiveName, int order)
{
- for (DirectoryEntry *entry : m_SubDirectories) {
- if (CaseInsensitiveEqual(entry->getName(), name)) {
- return entry;
- }
- }
- return nullptr;
-}
+ // add files
+ for (unsigned int fileIdx = 0; fileIdx < archiveFolder->getNumFiles(); ++fileIdx) {
+ BSA::File::Ptr file = archiveFolder->getFile(fileIdx);
+ auto f = insert(ToWString(file->getName(), true), origin, fileTime, archiveName, order);
-DirectoryEntry *DirectoryEntry::findSubDirectoryRecursive(const std::wstring &path)
-{
- return getSubDirectoryRecursive(path, false, -1);
-}
+ if (f) {
+ if (file->getUncompressedFileSize() > 0) {
+ f->setFileSize(file->getFileSize(), file->getUncompressedFileSize());
+ } else {
+ f->setFileSize(file->getFileSize(), FileEntry::NoFileSize);
+ }
+ }
+ }
+ // recurse into subdirectories
+ for (unsigned int folderIdx = 0; folderIdx < archiveFolder->getNumSubFolders(); ++folderIdx) {
+ BSA::Folder::Ptr folder = archiveFolder->getSubFolder(folderIdx);
+ DirectoryEntry *folderEntry = getSubDirectoryRecursive(ToWString(folder->getName(), true), true, origin.getID());
-const FileEntry::Ptr DirectoryEntry::findFile(const std::wstring &name) const
-{
- auto iter = m_Files.find(ToLower(name));
- if (iter != m_Files.end()) {
- return m_FileRegister->getFile(iter->second);
- } else {
- return FileEntry::Ptr();
+ folderEntry->addFiles(origin, folder, fileTime, archiveName, order);
}
}
-DirectoryEntry *DirectoryEntry::getSubDirectory(const std::wstring &name, bool create, int originID)
+DirectoryEntry *DirectoryEntry::getSubDirectory(
+ const std::wstring &name, bool create, int originID)
{
for (DirectoryEntry *entry : m_SubDirectories) {
if (CaseInsensitiveEqual(entry->getName(), name)) {
return entry;
}
}
+
if (create) {
- std::vector<DirectoryEntry*>::iterator iter = m_SubDirectories.insert(m_SubDirectories.end(),
- new DirectoryEntry(name, this, originID, m_FileRegister, m_OriginConnection));
- return *iter;
+ auto* entry = new DirectoryEntry(
+ name, this, originID, m_FileRegister, m_OriginConnection);
+
+ addDirectoryToList(entry);
+
+ return entry;
} else {
return nullptr;
}
}
-
-DirectoryEntry *DirectoryEntry::getSubDirectoryRecursive(const std::wstring &path, bool create, int originID)
+DirectoryEntry *DirectoryEntry::getSubDirectoryRecursive(
+ const std::wstring &path, bool create, int originID)
{
if (path.length() == 0) {
// path ended with a backslash?
return this;
}
- size_t pos = path.find_first_of(L"\\/");
+ const size_t pos = path.find_first_of(L"\\/");
+
if (pos == std::wstring::npos) {
return getSubDirectory(path, create);
} else {
DirectoryEntry *nextChild = getSubDirectory(path.substr(0, pos), create, originID);
+
if (nextChild == nullptr) {
return nullptr;
} else {
@@ -922,134 +1162,103 @@ DirectoryEntry *DirectoryEntry::getSubDirectoryRecursive(const std::wstring &pat
}
}
-
-
-FileRegister::FileRegister(boost::shared_ptr<OriginConnection> originConnection)
- : m_OriginConnection(originConnection)
+void DirectoryEntry::removeDirRecursive()
{
- LEAK_TRACE;
-}
+ while (!m_Files.empty()) {
+ m_FileRegister->removeFile(m_Files.begin()->second);
+ }
-FileRegister::~FileRegister()
-{
- LEAK_UNTRACE;
- m_Files.clear();
-}
+ m_FilesLookup.clear();
+ for (DirectoryEntry *entry : m_SubDirectories) {
+ entry->removeDirRecursive();
+ delete entry;
+ }
-FileEntry::Index FileRegister::generateIndex()
-{
- static std::atomic<FileEntry::Index> sIndex(0);
- return sIndex++;
+ m_SubDirectories.clear();
+ m_SubDirectoriesLookup.clear();
}
-bool FileRegister::indexValid(FileEntry::Index index) const
+void DirectoryEntry::addDirectoryToList(DirectoryEntry* e)
{
- return m_Files.find(index) != m_Files.end();
+ m_SubDirectories.push_back(e);
+ m_SubDirectoriesLookup.emplace(ToLowerCopy(e->getName()), e);
}
-FileEntry::Ptr FileRegister::createFile(const std::wstring &name, DirectoryEntry *parent)
+void DirectoryEntry::removeDirectoryFromList(SubDirectories::iterator itor)
{
- FileEntry::Index index = generateIndex();
- m_Files[index] = FileEntry::Ptr(new FileEntry(index, name, parent));
- return m_Files[index];
-}
+ const auto* entry = *itor;
+ {
+ auto itor2 = std::find_if(
+ m_SubDirectoriesLookup.begin(), m_SubDirectoriesLookup.end(),
+ [&](auto&& d) { return (d.second == entry); });
-FileEntry::Ptr FileRegister::getFile(FileEntry::Index index) const
-{
- auto iter = m_Files.find(index);
- if (iter != m_Files.end()) {
- return iter->second;
- } else {
- return FileEntry::Ptr();
- }
-}
-
-void FileRegister::unregisterFile(FileEntry::Ptr file)
-{
- bool ignore;
- // unregister from origin
- int originID = file->getOrigin(ignore);
- m_OriginConnection->getByID(originID).removeFile(file->getIndex());
- const std::vector<std::pair<int, std::pair<std::wstring, int>>> &alternatives = file->getAlternatives();
- for (auto iter = alternatives.begin(); iter != alternatives.end(); ++iter) {
- m_OriginConnection->getByID(iter->first).removeFile(file->getIndex());
+ if (itor2 == m_SubDirectoriesLookup.end()) {
+ log::error("entry {} not in sub directories map", entry->getName());
+ } else {
+ m_SubDirectoriesLookup.erase(itor2);
+ }
}
- // unregister from directory
- if (file->getParent() != nullptr) {
- file->getParent()->removeFile(file->getIndex());
- }
+ m_SubDirectories.erase(itor);
}
-
-bool FileRegister::removeFile(FileEntry::Index index)
+void DirectoryEntry::removeFileFromList(FileEntry::Index index)
{
- auto iter = m_Files.find(index);
- if (iter != m_Files.end()) {
- unregisterFile(iter->second);
- m_Files.erase(index);
- return true;
- } else {
- log::error(QObject::tr("invalid file index for remove: {}").toStdString(), index);
- return false;
- }
-}
+ auto removeFrom = [&](auto& list) {
+ auto iter = std::find_if(
+ list.begin(), list.end(),
+ [&index](auto&& pair) { return (pair.second == index); }
+ );
-void FileRegister::removeOrigin(FileEntry::Index index, int originID)
-{
- auto iter = m_Files.find(index);
- if (iter != m_Files.end()) {
- if (iter->second->removeOrigin(originID)) {
- unregisterFile(iter->second);
- m_Files.erase(iter);
+ if (iter == list.end()) {
+ auto f = m_FileRegister->getFile(index);
+
+ if (f) {
+ log::error(
+ "can't remove file '{}', not in directory entry '{}'",
+ f->getName(), getName());
+ } else {
+ log::error(
+ "can't remove file with index {}, not in directory entry '{}' and "
+ "not in register",
+ index, getName());
+ }
+ } else {
+ list.erase(iter);
}
- } else {
- log::error(QObject::tr("invalid file index for remove (for origin): {}").toStdString(), index);
- }
+ };
+
+ removeFrom(m_FilesLookup);
+ removeFrom(m_Files);
}
-void FileRegister::removeOriginMulti(std::set<FileEntry::Index> indices, int originID, time_t notAfter)
+void DirectoryEntry::removeFilesFromList(const std::set<FileEntry::Index>& indices)
{
- std::vector<FileEntry::Ptr> removedFiles;
- for (auto iter = indices.begin(); iter != indices.end();) {
- auto pos = m_Files.find(*iter);
- if (pos != m_Files.end()
- && (pos->second->lastAccessed() < notAfter)
- && pos->second->removeOrigin(originID)) {
- removedFiles.push_back(pos->second);
- m_Files.erase(pos);
- ++iter;
+ for (auto iter = m_Files.begin(); iter != m_Files.end();) {
+ if (indices.find(iter->second) != indices.end()) {
+ iter = m_Files.erase(iter);
} else {
- indices.erase(iter++);
+ ++iter;
}
}
- // optimization: this is only called when disabling an origin and in this case we don't have
- // to remove the file from the origin
-
- // need to remove files from their parent directories. multiple ways to go about this:
- // a) for each file, search its parents file-list (preferably by name) and remove what is found
- // b) gather the parent directories, go through the file list for each once and remove all files that have been removed
- // the latter should be faster when there are many files in few directories. since this is called
- // only when disabling an origin that is probably frequently the case
- std::set<DirectoryEntry*> parents;
- for (const FileEntry::Ptr &file : removedFiles) {
- if (file->getParent() != nullptr) {
- parents.insert(file->getParent());
+ for (auto iter = m_FilesLookup.begin(); iter != m_FilesLookup.end();) {
+ if (indices.find(iter->second) != indices.end()) {
+ iter = m_FilesLookup.erase(iter);
+ } else {
+ ++iter;
}
}
- for (DirectoryEntry *parent : parents) {
- parent->removeFiles(indices);
- }
}
-void FileRegister::sortOrigins()
+void DirectoryEntry::addFileToList(
+ std::wstring fileNameLower, FileEntry::Index index)
{
- for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
- iter->second->sortOrigins();
- }
+ m_FilesLookup.emplace(fileNameLower, index);
+ m_Files.emplace(std::move(fileNameLower), index);
+ // fileNameLower has been moved from this point
}
} // namespace MOShared
diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h
index 785c3ff6..e26011d7 100644
--- a/src/shared/directoryentry.h
+++ b/src/shared/directoryentry.h
@@ -35,128 +35,211 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#endif
#include "util.h"
+namespace MOShared { struct DirectoryEntryFileKey; }
-namespace MOShared {
+namespace std
+{
+ template <>
+ struct hash<MOShared::DirectoryEntryFileKey>
+ {
+ using argument_type = MOShared::DirectoryEntryFileKey;
+ using result_type = std::size_t;
+
+ inline result_type operator()(const argument_type& key) const;
+ };
+}
+namespace MOShared
+{
+
class DirectoryEntry;
class OriginConnection;
class FileRegister;
-class FileEntry {
-
+class FileEntry
+{
public:
+ static constexpr uint64_t NoFileSize =
+ std::numeric_limits<uint64_t>::max();
typedef unsigned int Index;
typedef boost::shared_ptr<FileEntry> Ptr;
- typedef std::vector<std::pair<int, std::pair<std::wstring, int>>> AlternativesVector;
-public:
+ // a vector of {originId, {archiveName, order}}
+ //
+ // if a file is in an archive, archiveName is the name of the bsa and order
+ // is the order of the associated plugin in the plugins list
+ //
+ // is a file is not in an archive, archiveName is empty and order is usually
+ // -1
+ typedef std::vector<std::pair<int, std::pair<std::wstring, int>>>
+ AlternativesVector;
FileEntry();
-
FileEntry(Index index, const std::wstring &name, DirectoryEntry *parent);
- ~FileEntry();
+ Index getIndex() const
+ {
+ return m_Index;
+ }
- Index getIndex() const { return m_Index; }
+ time_t lastAccessed() const
+ {
+ return m_LastAccessed;
+ }
- time_t lastAccessed() const { return m_LastAccessed; }
+ void addOrigin(
+ int origin, FILETIME fileTime, const std::wstring &archive, int order);
- void addOrigin(int origin, FILETIME fileTime, const std::wstring &archive, int order);
- // remove the specified origin from the list of origins that contain this file. if no origin is left,
- // the file is effectively deleted and true is returned. otherwise, false is returned
+ // remove the specified origin from the list of origins that contain this
+ // file. if no origin is left, the file is effectively deleted and true is
+ // returned. otherwise, false is returned
bool removeOrigin(int origin);
+
void sortOrigins();
- // gets the list of alternative origins (origins with lower priority than the primary one).
- // if sortOrigins has been called, it is sorted by priority (ascending)
- const AlternativesVector &getAlternatives() const { return m_Alternatives; }
+ // gets the list of alternative origins (origins with lower priority than
+ // the primary one). if sortOrigins has been called, it is sorted by priority
+ // (ascending)
+ const AlternativesVector &getAlternatives() const
+ {
+ return m_Alternatives;
+ }
+
+ const std::wstring &getName() const
+ {
+ return m_Name;
+ }
+
+ int getOrigin() const
+ {
+ return m_Origin;
+ }
+
+ int getOrigin(bool &archive) const
+ {
+ archive = (m_Archive.first.length() != 0);
+ return m_Origin;
+ }
+
+ const std::pair<std::wstring, int> &getArchive() const
+ {
+ return m_Archive;
+ }
+
+ bool isFromArchive(std::wstring archiveName = L"") const;
+
+ // if originID is -1, uses the main origin; if this file doesn't exist in the
+ // given origin, returns an empty string
+ //
+ std::wstring getFullPath(int originID=-1) const;
- const std::wstring &getName() const { return m_Name; }
- int getOrigin() const { return m_Origin; }
- int getOrigin(bool &archive) const { archive = (m_Archive.first.length() != 0); return m_Origin; }
- const std::pair<std::wstring, int> &getArchive() const { return m_Archive; }
- bool isFromArchive(std::wstring archiveName = L"");
- std::wstring getFullPath() const;
std::wstring getRelativePath() const;
- DirectoryEntry *getParent() { return m_Parent; }
- void setFileTime(FILETIME fileTime) const { m_FileTime = fileTime; }
- FILETIME getFileTime() const { return m_FileTime; }
+ DirectoryEntry *getParent()
+ {
+ return m_Parent;
+ }
-private:
+ void setFileTime(FILETIME fileTime) const
+ {
+ m_FileTime = fileTime;
+ }
- bool recurseParents(std::wstring &path, const DirectoryEntry *parent) const;
+ FILETIME getFileTime() const
+ {
+ return m_FileTime;
+ }
- void determineTime();
+ void setFileSize(uint64_t size, uint64_t compressedSize)
+ {
+ m_FileSize = size;
+ m_CompressedFileSize = compressedSize;
+ }
-private:
+ uint64_t getFileSize() const
+ {
+ return m_FileSize;
+ }
+
+ uint64_t getCompressedFileSize() const
+ {
+ return m_CompressedFileSize;
+ }
+private:
Index m_Index;
std::wstring m_Name;
- int m_Origin = -1;
+ int m_Origin;
std::pair<std::wstring, int> m_Archive;
AlternativesVector m_Alternatives;
DirectoryEntry *m_Parent;
mutable FILETIME m_FileTime;
+ uint64_t m_FileSize, m_CompressedFileSize;
time_t m_LastAccessed;
- friend bool operator<(const FileEntry &lhs, const FileEntry &rhs) {
- return _wcsicmp(lhs.m_Name.c_str(), rhs.m_Name.c_str()) < 0;
- }
- friend bool operator==(const FileEntry &lhs, const FileEntry &rhs) {
- return _wcsicmp(lhs.m_Name.c_str(), rhs.m_Name.c_str()) == 0;
- }
+ bool recurseParents(std::wstring &path, const DirectoryEntry *parent) const;
};
// represents a mod or the data directory, providing files to the tree
-class FilesOrigin {
+class FilesOrigin
+{
friend class OriginConnection;
-public:
+public:
FilesOrigin();
FilesOrigin(const FilesOrigin &reference);
- ~FilesOrigin();
- // sets priority for this origin, but it will overwrite the exisiting mapping for this priority,
- // the previous origin will no longer be referenced
+ // sets priority for this origin, but it will overwrite the existing mapping
+ // for this priority, the previous origin will no longer be referenced
void setPriority(int priority);
- int getPriority() const { return m_Priority; }
+ int getPriority() const
+ {
+ return m_Priority;
+ }
void setName(const std::wstring &name);
- const std::wstring &getName() const { return m_Name; }
+ const std::wstring &getName() const
+ {
+ return m_Name;
+ }
- int getID() const { return m_ID; }
- const std::wstring &getPath() const { return m_Path; }
+ int getID() const
+ {
+ return m_ID;
+ }
+
+ const std::wstring &getPath() const
+ {
+ return m_Path;
+ }
std::vector<FileEntry::Ptr> getFiles() const;
FileEntry::Ptr findFile(FileEntry::Index index) const;
void enable(bool enabled, time_t notAfter = LONG_MAX);
- bool isDisabled() const { return m_Disabled; }
+ bool isDisabled() const
+ {
+ return m_Disabled;
+ }
+
+ void addFile(FileEntry::Index index)
+ {
+ m_Files.insert(index);
+ }
- void addFile(FileEntry::Index index) { m_Files.insert(index); }
void removeFile(FileEntry::Index index);
bool containsArchive(std::wstring archiveName);
private:
-
- FilesOrigin(int ID, const std::wstring &name, const std::wstring &path, int priority,
- boost::shared_ptr<FileRegister> fileRegister, boost::shared_ptr<OriginConnection> originConnection);
-
-
-private:
-
int m_ID;
-
bool m_Disabled;
-
std::set<FileEntry::Index> m_Files;
std::wstring m_Name;
std::wstring m_Path;
@@ -164,23 +247,27 @@ private:
boost::weak_ptr<FileRegister> m_FileRegister;
boost::weak_ptr<OriginConnection> m_OriginConnection;
+ FilesOrigin(
+ int ID, const std::wstring &name, const std::wstring &path, int priority,
+ boost::shared_ptr<FileRegister> fileRegister,
+ boost::shared_ptr<OriginConnection> originConnection);
};
class FileRegister
{
-
public:
-
FileRegister(boost::shared_ptr<OriginConnection> originConnection);
- ~FileRegister();
bool indexValid(FileEntry::Index index) const;
FileEntry::Ptr createFile(const std::wstring &name, DirectoryEntry *parent);
FileEntry::Ptr getFile(FileEntry::Index index) const;
- size_t size() const { return m_Files.size(); }
+ size_t size() const
+ {
+ return m_Files.size();
+ }
bool removeFile(FileEntry::Index index);
void removeOrigin(FileEntry::Index index, int originID);
@@ -189,84 +276,189 @@ public:
void sortOrigins();
private:
+ std::map<FileEntry::Index, FileEntry::Ptr> m_Files;
+ boost::shared_ptr<OriginConnection> m_OriginConnection;
FileEntry::Index generateIndex();
-
void unregisterFile(FileEntry::Ptr file);
+};
-private:
- std::map<FileEntry::Index, FileEntry::Ptr> m_Files;
+struct DirectoryEntryFileKey
+{
+ DirectoryEntryFileKey(std::wstring v)
+ : value(std::move(v)), hash(getHash(value))
+ {
+ }
- boost::shared_ptr<OriginConnection> m_OriginConnection;
+ bool operator==(const DirectoryEntryFileKey& o) const
+ {
+ return (value == o.value);
+ }
+
+ static std::size_t getHash(const std::wstring& value)
+ {
+ return std::hash<std::wstring>()(value);
+ }
+ const std::wstring value;
+ const std::size_t hash;
};
class DirectoryEntry
{
public:
+ using FileKey = DirectoryEntryFileKey;
- DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID);
+ DirectoryEntry(
+ const std::wstring &name, DirectoryEntry *parent, int originID);
- DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID,
- boost::shared_ptr<FileRegister> fileRegister,
- boost::shared_ptr<OriginConnection> originConnection);
+ DirectoryEntry(
+ const std::wstring &name, DirectoryEntry *parent, int originID,
+ boost::shared_ptr<FileRegister> fileRegister,
+ boost::shared_ptr<OriginConnection> originConnection);
~DirectoryEntry();
void clear();
- bool isPopulated() const { return m_Populated; }
- bool isEmpty() const { return m_Files.empty() && m_SubDirectories.empty(); }
+ bool isPopulated() const
+ {
+ return m_Populated;
+ }
+
+ bool isTopLevel() const
+ {
+ return m_TopLevel;
+ }
+
+ bool isEmpty() const
+ {
+ return m_Files.empty() && m_SubDirectories.empty();
+ }
+
+ bool hasFiles() const
+ {
+ return !m_Files.empty();
+ }
+
+ const DirectoryEntry *getParent() const
+ {
+ return m_Parent;
+ }
- const DirectoryEntry *getParent() const { return m_Parent; }
+ // add files to this directory (and subdirectories) from the specified origin.
+ // That origin may exist or not
+ void addFromOrigin(
+ const std::wstring &originName,
+ const std::wstring &directory, int priority);
- // add files to this directory (and subdirectories) from the specified origin. That origin may exist or not
- void addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority);
- void addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority, int order);
+ void addFromBSA(
+ const std::wstring &originName, std::wstring &directory,
+ const std::wstring &fileName, int priority, int order);
void propagateOrigin(int origin);
- const std::wstring &getName() const;
+ const std::wstring &getName() const
+ {
+ return m_Name;
+ }
- boost::shared_ptr<FileRegister> getFileRegister() { return m_FileRegister; }
+ boost::shared_ptr<FileRegister> getFileRegister()
+ {
+ return m_FileRegister;
+ }
bool originExists(const std::wstring &name) const;
FilesOrigin &getOriginByID(int ID) const;
FilesOrigin &getOriginByName(const std::wstring &name) const;
+ const FilesOrigin* findOriginByID(int ID) const;
int anyOrigin() const;
- //int getOrigin(const std::wstring &path, bool &archive);
-
std::vector<FileEntry::Ptr> getFiles() const;
- void getSubDirectories(std::vector<DirectoryEntry*>::const_iterator &begin
- , std::vector<DirectoryEntry*>::const_iterator &end) const {
- begin = m_SubDirectories.begin(); end = m_SubDirectories.end();
+ void getSubDirectories(
+ std::vector<DirectoryEntry*>::const_iterator &begin,
+ std::vector<DirectoryEntry*>::const_iterator &end) const
+ {
+ begin = m_SubDirectories.begin();
+ end = m_SubDirectories.end();
+ }
+
+ const std::vector<DirectoryEntry*>& getSubDirectories() const
+ {
+ return m_SubDirectories;
+ }
+
+ template <class F>
+ void forEachDirectory(F&& f) const
+ {
+ for (auto&& d : m_SubDirectories) {
+ if (!f(*d)) {
+ break;
+ }
+ }
+ }
+
+ template <class F>
+ void forEachFile(F&& f) const
+ {
+ for (auto&& p : m_Files) {
+ if (auto file=m_FileRegister->getFile(p.second)) {
+ if (!f(*file)) {
+ break;
+ }
+ }
+ }
+ }
+
+ template <class F>
+ void forEachFileIndex(F&& f) const
+ {
+ for (auto&& p : m_Files) {
+ if (!f(p.second)) {
+ break;
+ }
+ }
+ }
+
+ FileEntry::Ptr getFileByIndex(FileEntry::Index index) const
+ {
+ return m_FileRegister->getFile(index);
}
- DirectoryEntry *findSubDirectory(const std::wstring &name) const;
+ DirectoryEntry *findSubDirectory(
+ const std::wstring &name, bool alreadyLowerCase=false) const;
+
DirectoryEntry *findSubDirectoryRecursive(const std::wstring &path);
/** retrieve a file in this directory by name.
* @param name name of the file
* @return fileentry object for the file or nullptr if no file matches
*/
- const FileEntry::Ptr findFile(const std::wstring &name) const;
+ const FileEntry::Ptr findFile(const std::wstring &name, bool alreadyLowerCase=false) const;
+ const FileEntry::Ptr findFile(const FileKey& key) const;
+ bool hasFile(const std::wstring& name) const;
bool containsArchive(std::wstring archiveName);
- /** search through this directory and all subdirectories for a file by the specified name (relative path).
- if directory is not nullptr, the referenced variable will be set to the path containing the file */
- const FileEntry::Ptr searchFile(const std::wstring &path, const DirectoryEntry **directory) const;
+ // search through this directory and all subdirectories for a file by the
+ // specified name (relative path).
+ //
+ // if directory is not nullptr, the referenced variable will be set to the
+ // path containing the file
+ //
+ const FileEntry::Ptr searchFile(
+ const std::wstring &path, const DirectoryEntry **directory=nullptr) const;
void insertFile(const std::wstring &filePath, FilesOrigin &origin, FILETIME fileTime);
void removeFile(FileEntry::Index index);
- // remove the specified file from the tree. This can be a path leading to a file in a subdirectory
+ // remove the specified file from the tree. This can be a path leading to a
+ // file in a subdirectory
bool removeFile(const std::wstring &filePath, int *origin = nullptr);
/**
@@ -275,76 +467,77 @@ public:
*/
void removeDir(const std::wstring &path);
- bool remove(const std::wstring &fileName, int *origin) {
- auto iter = m_Files.find(ToLower(fileName));
- if (iter != m_Files.end()) {
- if (origin != nullptr) {
- FileEntry::Ptr entry = m_FileRegister->getFile(iter->second);
- if (entry.get() != nullptr) {
- bool ignore;
- *origin = entry->getOrigin(ignore);
- }
- }
- return m_FileRegister->removeFile(iter->second);
- } else {
- return false;
- }
- }
+ bool remove(const std::wstring &fileName, int *origin);
bool hasContentsFromOrigin(int originID) const;
- FilesOrigin &createOrigin(const std::wstring &originName, const std::wstring &directory, int priority);
+ FilesOrigin &createOrigin(
+ const std::wstring &originName,
+ const std::wstring &directory, int priority);
void removeFiles(const std::set<FileEntry::Index> &indices);
private:
+ using FilesMap = std::map<std::wstring, FileEntry::Index>;
+ using FilesLookup = std::unordered_map<FileKey, FileEntry::Index>;
+ using SubDirectories = std::vector<DirectoryEntry*>;
+ using SubDirectoriesLookup = std::unordered_map<std::wstring, DirectoryEntry*>;
- DirectoryEntry(const DirectoryEntry &reference);
- DirectoryEntry &operator=(const DirectoryEntry &reference);
+ boost::shared_ptr<FileRegister> m_FileRegister;
+ boost::shared_ptr<OriginConnection> m_OriginConnection;
- void insert(const std::wstring &fileName, FilesOrigin &origin, FILETIME fileTime, const std::wstring &archive, int order) {
- std::wstring fileNameLower = ToLower(fileName);
- auto iter = m_Files.find(fileNameLower);
- FileEntry::Ptr file;
- if (iter != m_Files.end()) {
- file = m_FileRegister->getFile(iter->second);
- } else {
- file = m_FileRegister->createFile(fileName, this);
- // TODO this has been observed to cause a crash, no clue why
- m_Files[fileNameLower] = file->getIndex();
- }
- file->addOrigin(origin.getID(), fileTime, archive, order);
- origin.addFile(file->getIndex());
- }
+ std::wstring m_Name;
+ FilesMap m_Files;
+ FilesLookup m_FilesLookup;
+ SubDirectories m_SubDirectories;
+ SubDirectoriesLookup m_SubDirectoriesLookup;
- void addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset);
- void addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName, int order);
+ DirectoryEntry *m_Parent;
+ std::set<int> m_Origins;
+ bool m_Populated;
+ bool m_TopLevel;
- DirectoryEntry *getSubDirectory(const std::wstring &name, bool create, int originID = -1);
- DirectoryEntry *getSubDirectoryRecursive(const std::wstring &path, bool create, int originID = -1);
+ DirectoryEntry(const DirectoryEntry &reference);
- void removeDirRecursive();
+ FileEntry::Ptr insert(
+ const std::wstring &fileName, FilesOrigin &origin, FILETIME fileTime,
+ const std::wstring &archive, int order);
-private:
+ void addFiles(
+ FilesOrigin &origin, wchar_t *buffer, int bufferOffset);
- boost::shared_ptr<FileRegister> m_FileRegister;
- boost::shared_ptr<OriginConnection> m_OriginConnection;
+ void addFiles(
+ FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime,
+ const std::wstring &archiveName, int order);
- std::wstring m_Name;
- std::map<std::wstring, FileEntry::Index> m_Files;
- std::vector<DirectoryEntry*> m_SubDirectories;
+ DirectoryEntry *getSubDirectory(
+ const std::wstring &name, bool create, int originID = -1);
- DirectoryEntry *m_Parent;
- std::set<int> m_Origins;
+ DirectoryEntry *getSubDirectoryRecursive(
+ const std::wstring &path, bool create, int originID = -1);
- bool m_Populated;
+ void removeDirRecursive();
- bool m_TopLevel;
+ void addDirectoryToList(DirectoryEntry* e);
+ void removeDirectoryFromList(SubDirectories::iterator itor);
+ void addFileToList(std::wstring fileNameLower, FileEntry::Index index);
+ void removeFileFromList(FileEntry::Index index);
+ void removeFilesFromList(const std::set<FileEntry::Index>& indices);
};
-
} // namespace MOShared
+
+namespace std
+{
+ hash<MOShared::DirectoryEntryFileKey>::result_type
+ hash<MOShared::DirectoryEntryFileKey>::operator()(
+ const argument_type& key) const
+ {
+ return key.hash;
+ }
+}
+
#endif // DIRECTORYENTRY_H
diff --git a/src/shared/util.cpp b/src/shared/util.cpp
index baceddeb..aa4ad4b3 100644
--- a/src/shared/util.cpp
+++ b/src/shared/util.cpp
@@ -20,9 +20,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "util.h"
#include "windows_error.h"
#include "mainwindow.h"
+#include "env.h"
+#include <log.h>
#include <usvfs.h>
#include <usvfs_version.h>
+using namespace MOBase;
+
namespace MOShared
{
@@ -100,32 +104,28 @@ static auto locToLower = [] (char in) -> char {
return std::tolower(in, loc);
};
-std::string &ToLower(std::string &text)
+std::string& ToLowerInPlace(std::string& text)
{
- //std::transform(text.begin(), text.end(), text.begin(), locToLower);
CharLowerBuffA(const_cast<CHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
return text;
}
-std::string ToLower(const std::string &text)
+std::string ToLowerCopy(const std::string& text)
{
std::string result(text);
- //std::transform(result.begin(), result.end(), result.begin(), locToLower);
CharLowerBuffA(const_cast<CHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
return result;
}
-std::wstring &ToLower(std::wstring &text)
+std::wstring& ToLowerInPlace(std::wstring& text)
{
- //std::transform(text.begin(), text.end(), text.begin(), locToLowerW);
CharLowerBuffW(const_cast<WCHAR *>(text.c_str()), static_cast<DWORD>(text.size()));
return text;
}
-std::wstring ToLower(const std::wstring &text)
+std::wstring ToLowerCopy(const std::wstring& text)
{
std::wstring result(text);
- //std::transform(result.begin(), result.end(), result.begin(), locToLowerW);
CharLowerBuffW(const_cast<WCHAR *>(result.c_str()), static_cast<DWORD>(result.size()));
return result;
}
@@ -201,7 +201,7 @@ std::wstring GetFileVersionString(const std::wstring &fileName)
}
}
-MOBase::VersionInfo createVersionInfo()
+VersionInfo createVersionInfo()
{
VS_FIXEDFILEINFO version = GetFileVersion(QApplication::applicationFilePath().toStdWString());
@@ -224,25 +224,25 @@ MOBase::VersionInfo createVersionInfo()
if (noLetters)
{
// Default to pre-alpha when release type is unspecified
- return MOBase::VersionInfo(version.dwFileVersionMS >> 16,
- version.dwFileVersionMS & 0xFFFF,
- version.dwFileVersionLS >> 16,
- version.dwFileVersionLS & 0xFFFF,
- MOBase::VersionInfo::RELEASE_PREALPHA);
+ return VersionInfo(version.dwFileVersionMS >> 16,
+ version.dwFileVersionMS & 0xFFFF,
+ version.dwFileVersionLS >> 16,
+ version.dwFileVersionLS & 0xFFFF,
+ VersionInfo::RELEASE_PREALPHA);
}
else
{
// Trust the string to make sense
- return MOBase::VersionInfo(versionString);
+ return VersionInfo(versionString);
}
}
else
{
// Non-pre-release builds just need their version numbers reading
- return MOBase::VersionInfo(version.dwFileVersionMS >> 16,
- version.dwFileVersionMS & 0xFFFF,
- version.dwFileVersionLS >> 16,
- version.dwFileVersionLS & 0xFFFF);
+ return VersionInfo(version.dwFileVersionMS >> 16,
+ version.dwFileVersionMS & 0xFFFF,
+ version.dwFileVersionLS >> 16,
+ version.dwFileVersionLS & 0xFFFF);
}
}
@@ -290,9 +290,111 @@ QString getUsvfsVersionString()
}
}
+void SetThisThreadName(const QString& s)
+{
+ using SetThreadDescriptionType = HRESULT (
+ HANDLE hThread,
+ PCWSTR lpThreadDescription
+ );
+
+ static SetThreadDescriptionType* SetThreadDescription = [] {
+ SetThreadDescriptionType* p = nullptr;
+
+ env::LibraryPtr kernel32(LoadLibraryW(L"kernel32.dll"));
+ if (!kernel32) {
+ return p;
+ }
+
+ p = reinterpret_cast<SetThreadDescriptionType*>(
+ GetProcAddress(kernel32.get(), "SetThreadDescription"));
+
+ return p;
+ }();
+
+ if (SetThreadDescription) {
+ SetThreadDescription(GetCurrentThread(), s.toStdWString().c_str());
+ }
+}
+
+
+char shortcutChar(const QAction* a)
+{
+ const auto text = a->text();
+ char shortcut = 0;
+
+ for (int i=0; i<text.size(); ++i) {
+ const auto c = text[i];
+ if (c == '&') {
+ if (i >= (text.size() - 1)) {
+ log::error("ampersand at the end");
+ return 0;
+ }
+
+ return text[i + 1].toLatin1();
+ }
+ }
+
+ log::error("action {} has no shortcut", text);
+ return 0;
+}
+
+void checkDuplicateShortcuts(const QMenu& m)
+{
+ const auto actions = m.actions();
+
+ for (int i=0; i<actions.size(); ++i) {
+ const auto* action1 = actions[i];
+ if (action1->isSeparator()) {
+ continue;
+ }
+
+ const char shortcut1 = shortcutChar(action1);
+ if (shortcut1 == 0) {
+ continue;
+ }
+
+ for (int j=i+1; j<actions.size(); ++j) {
+ const auto* action2 = actions[j];
+ if (action2->isSeparator()) {
+ continue;
+ }
+
+ const char shortcut2 = shortcutChar(action2);
+
+ if (shortcut1 == shortcut2) {
+ log::error(
+ "duplicate shortcut {} for {} and {}",
+ shortcut1, action1->text(), action2->text());
+
+ break;
+ }
+ }
+ }
+}
+
} // namespace MOShared
+TimeThis::TimeThis(QString what)
+ : m_what(std::move(what)), m_start(Clock::now())
+{
+}
+
+TimeThis::~TimeThis()
+{
+ using namespace std::chrono;
+
+ const auto end = Clock::now();
+ const auto d = duration_cast<milliseconds>(end - m_start).count();
+
+ if (m_what.isEmpty()) {
+ log::debug("{} ms", d);
+ } else {
+ log::debug("{} {} ms", m_what, d);
+ }
+}
+
+
static bool g_exiting = false;
static bool g_canClose = false;
@@ -314,7 +416,7 @@ bool ExitModOrganizer(ExitFlags e)
}
g_exiting = true;
- MOBase::Guard g([&]{ g_exiting = false; });
+ Guard g([&]{ g_exiting = false; });
if (!e.testFlag(Exit::Force)) {
if (auto* mw=findMainWindow()) {
diff --git a/src/shared/util.h b/src/shared/util.h
index e8a58549..05522c2d 100644
--- a/src/shared/util.h
+++ b/src/shared/util.h
@@ -36,20 +36,37 @@ bool FileExists(const std::wstring &searchPath, const std::wstring &filename);
std::string ToString(const std::wstring &source, bool utf8);
std::wstring ToWString(const std::string &source, bool utf8);
-std::string &ToLower(std::string &text);
-std::string ToLower(const std::string &text);
+std::string& ToLowerInPlace(std::string& text);
+std::string ToLowerCopy(const std::string& text);
-std::wstring &ToLower(std::wstring &text);
-std::wstring ToLower(const std::wstring &text);
+std::wstring& ToLowerInPlace(std::wstring& text);
+std::wstring ToLowerCopy(const std::wstring& text);
bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs);
MOBase::VersionInfo createVersionInfo();
QString getUsvfsVersionString();
+void SetThisThreadName(const QString& s);
+void checkDuplicateShortcuts(const QMenu& m);
+
} // namespace MOShared
+class TimeThis
+{
+public:
+ TimeThis(QString what={});
+ ~TimeThis();
+
+private:
+ using Clock = std::chrono::high_resolution_clock;
+
+ QString m_what;
+ Clock::time_point m_start;
+};
+
+
enum class Exit
{
None = 0x00,
diff --git a/src/spawn.cpp b/src/spawn.cpp
index 33bdbc05..df6fa379 100644
--- a/src/spawn.cpp
+++ b/src/spawn.cpp
@@ -924,6 +924,15 @@ QFileInfo getCmdPath()
return systemDirectory + "cmd.exe";
}
+FileExecutionTypes getFileExecutionType(const QFileInfo& target)
+{
+ if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) {
+ return FileExecutionTypes::Executable;
+ }
+
+ return FileExecutionTypes::Other;
+}
+
FileExecutionContext getFileExecutionContext(
QWidget* parent, const QFileInfo& target)
{
diff --git a/src/spawn.h b/src/spawn.h
index a615b5ff..0628781e 100644
--- a/src/spawn.h
+++ b/src/spawn.h
@@ -85,6 +85,8 @@ QString findJavaInstallation(const QString& jarFile);
FileExecutionContext getFileExecutionContext(
QWidget* parent, const QFileInfo& target);
+FileExecutionTypes getFileExecutionType(const QFileInfo& target);
+
} // namespace
diff --git a/src/statusbar.cpp b/src/statusbar.cpp
index 4729c6ad..c3de2b0a 100644
--- a/src/statusbar.cpp
+++ b/src/statusbar.cpp
@@ -5,6 +5,7 @@
StatusBar::StatusBar(QWidget* parent) :
QStatusBar(parent), ui(nullptr), m_progress(new QProgressBar),
+ m_progressSpacer1(new QWidget), m_progressSpacer2(new QWidget),
m_notifications(nullptr), m_update(nullptr), m_api(new QLabel)
{
}
@@ -15,17 +16,17 @@ void StatusBar::setup(Ui::MainWindow* mainWindowUI, const Settings& settings)
m_notifications = new StatusBarAction(ui->actionNotifications);
m_update = new StatusBarAction(ui->actionUpdate);
- QWidget* spacer1 = new QWidget;
- spacer1->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- spacer1->setHidden(true);
- spacer1->setVisible(true);
- addPermanentWidget(spacer1, 0);
+ m_progressSpacer1->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ m_progressSpacer1->setHidden(true);
+ m_progressSpacer1->setVisible(true);
+ addPermanentWidget(m_progressSpacer1, 0);
addPermanentWidget(m_progress);
- QWidget* spacer2 = new QWidget;
- spacer2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- spacer2->setHidden(true);
- spacer2->setVisible(true);
- addPermanentWidget(spacer2,0);
+
+ m_progressSpacer2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ m_progressSpacer2->setHidden(true);
+ m_progressSpacer2->setVisible(true);
+ addPermanentWidget(m_progressSpacer2,0);
+
addPermanentWidget(m_notifications);
addPermanentWidget(m_update);
addPermanentWidget(m_api);
@@ -57,14 +58,19 @@ void StatusBar::setup(Ui::MainWindow* mainWindowUI, const Settings& settings)
void StatusBar::setProgress(int percent)
{
+ bool visible =true;
+
if (percent < 0 || percent >= 100) {
clearMessage();
- m_progress->setVisible(false);
+ visible = false;
} else {
showMessage(QObject::tr("Loading..."));
- m_progress->setVisible(true);
m_progress->setValue(percent);
}
+
+ m_progress->setVisible(visible);
+ m_progressSpacer1->setVisible(visible);
+ m_progressSpacer2->setVisible(visible);
}
void StatusBar::setNotifications(bool hasNotifications)
diff --git a/src/statusbar.h b/src/statusbar.h
index 3da45d41..708615a1 100644
--- a/src/statusbar.h
+++ b/src/statusbar.h
@@ -49,6 +49,8 @@ protected:
private:
Ui::MainWindow* ui;
QProgressBar* m_progress;
+ QWidget* m_progressSpacer1;
+ QWidget* m_progressSpacer2;
StatusBarAction* m_notifications;
StatusBarAction* m_update;
QLabel* m_api;
diff --git a/src/stylesheets/vs15 Dark-Green.qss b/src/stylesheets/vs15 Dark-Green.qss
index 6d95c6cc..0a3150ad 100644
--- a/src/stylesheets/vs15 Dark-Green.qss
+++ b/src/stylesheets/vs15 Dark-Green.qss
@@ -692,19 +692,6 @@ QTabBar QToolButton::left-arrow:hover {
image: url(./vs15/scrollbar-left-hover.png); }
/* Special styles */
-/* fix margins on tabs pane */
-QLineEdit#espFilterEdit {
- margin: 0 0 6px 0; }
-
-QTreeView#downloadView {
- margin: 4px 4px 0 4px; }
-
-QCheckBox#showHiddenBox {
- margin: 0 0 4px 4px; }
-
-QLineEdit#downloadFilterEdit {
- margin: 0 4px 4px 0; }
-
QWidget#tabImages QPushButton {
background-color: transparent;
margin: 0 .3em;
diff --git a/src/stylesheets/vs15 Dark-Orange.qss b/src/stylesheets/vs15 Dark-Orange.qss
index 2dd27df4..0e1d96b1 100644
--- a/src/stylesheets/vs15 Dark-Orange.qss
+++ b/src/stylesheets/vs15 Dark-Orange.qss
@@ -603,7 +603,7 @@ SaveGameInfoWidget {
border-width: 1px;
padding: 2px; }
- QStatusBar::item {border: None;}
+QStatusBar::item {border: None;}
/* Progress Bars (Downloads) #QProgressBar */
QProgressBar {
@@ -693,19 +693,6 @@ QTabBar QToolButton::left-arrow:hover {
image: url(./vs15/scrollbar-left-hover.png); }
/* Special styles */
-/* fix margins on tabs pane */
-QLineEdit#espFilterEdit {
- margin: 0 0 6px 0; }
-
-QTreeView#downloadView {
- margin: 4px 4px 0 4px; }
-
-QCheckBox#showHiddenBox {
- margin: 0 0 4px 4px; }
-
-QLineEdit#downloadFilterEdit {
- margin: 0 4px 4px 0; }
-
QWidget#tabImages QPushButton {
background-color: transparent;
margin: 0 .3em;
diff --git a/src/stylesheets/vs15 Dark-Purple.qss b/src/stylesheets/vs15 Dark-Purple.qss
index 116aaa7d..c19d188b 100644
--- a/src/stylesheets/vs15 Dark-Purple.qss
+++ b/src/stylesheets/vs15 Dark-Purple.qss
@@ -603,7 +603,7 @@ SaveGameInfoWidget {
border-width: 1px;
padding: 2px; }
- QStatusBar::item {border: None;}
+QStatusBar::item {border: None;}
/* Progress Bars (Downloads) #QProgressBar */
QProgressBar {
@@ -693,19 +693,6 @@ QTabBar QToolButton::left-arrow:hover {
image: url(./vs15/scrollbar-left-hover.png); }
/* Special styles */
-/* fix margins on tabs pane */
-QLineEdit#espFilterEdit {
- margin: 0 0 6px 0; }
-
-QTreeView#downloadView {
- margin: 4px 4px 0 4px; }
-
-QCheckBox#showHiddenBox {
- margin: 0 0 4px 4px; }
-
-QLineEdit#downloadFilterEdit {
- margin: 0 4px 4px 0; }
-
QWidget#tabImages QPushButton {
background-color: transparent;
margin: 0 .3em;
diff --git a/src/stylesheets/vs15 Dark-Red.qss b/src/stylesheets/vs15 Dark-Red.qss
index 60f565a1..a6221dff 100644
--- a/src/stylesheets/vs15 Dark-Red.qss
+++ b/src/stylesheets/vs15 Dark-Red.qss
@@ -603,7 +603,7 @@ SaveGameInfoWidget {
border-width: 1px;
padding: 2px; }
- QStatusBar::item {border: None;}
+QStatusBar::item {border: None;}
/* Progress Bars (Downloads) #QProgressBar */
QProgressBar {
@@ -693,19 +693,6 @@ QTabBar QToolButton::left-arrow:hover {
image: url(./vs15/scrollbar-left-hover.png); }
/* Special styles */
-/* fix margins on tabs pane */
-QLineEdit#espFilterEdit {
- margin: 0 0 6px 0; }
-
-QTreeView#downloadView {
- margin: 4px 4px 0 4px; }
-
-QCheckBox#showHiddenBox {
- margin: 0 0 4px 4px; }
-
-QLineEdit#downloadFilterEdit {
- margin: 0 4px 4px 0; }
-
QWidget#tabImages QPushButton {
background-color: transparent;
margin: 0 .3em;
diff --git a/src/stylesheets/vs15 Dark-Yellow.qss b/src/stylesheets/vs15 Dark-Yellow.qss
index bfaa4c94..fb2e0422 100644
--- a/src/stylesheets/vs15 Dark-Yellow.qss
+++ b/src/stylesheets/vs15 Dark-Yellow.qss
@@ -1,4 +1,3 @@
-/*base*/
/*!*************************************
VS15 Dark
****************************************
@@ -603,7 +602,7 @@ SaveGameInfoWidget {
border-width: 1px;
padding: 2px; }
- QStatusBar::item {border: None;}
+QStatusBar::item {border: None;}
/* Progress Bars (Downloads) #QProgressBar */
QProgressBar {
@@ -693,19 +692,6 @@ QTabBar QToolButton::left-arrow:hover {
image: url(./vs15/scrollbar-left-hover.png); }
/* Special styles */
-/* fix margins on tabs pane */
-QLineEdit#espFilterEdit {
- margin: 0 0 6px 0; }
-
-QTreeView#downloadView {
- margin: 4px 4px 0 4px; }
-
-QCheckBox#showHiddenBox {
- margin: 0 0 4px 4px; }
-
-QLineEdit#downloadFilterEdit {
- margin: 0 4px 4px 0; }
-
QWidget#tabImages QPushButton {
background-color: transparent;
margin: 0 .3em;
diff --git a/src/stylesheets/vs15 Dark.qss b/src/stylesheets/vs15 Dark.qss
index 1d27be17..17def4dd 100644
--- a/src/stylesheets/vs15 Dark.qss
+++ b/src/stylesheets/vs15 Dark.qss
@@ -692,19 +692,6 @@ QTabBar QToolButton::left-arrow:hover {
image: url(./vs15/scrollbar-left-hover.png); }
/* Special styles */
-/* fix margins on tabs pane */
-QLineEdit#espFilterEdit {
- margin: 0 0 6px 0; }
-
-QTreeView#downloadView {
- margin: 4px 4px 0 4px; }
-
-QCheckBox#showHiddenBox {
- margin: 0 0 4px 4px; }
-
-QLineEdit#downloadFilterEdit {
- margin: 0 4px 4px 0; }
-
QWidget#tabImages QPushButton {
background-color: transparent;
margin: 0 .3em;
diff --git a/src/version.rc b/src/version.rc
index 4e244560..a5fbb7a6 100644
--- a/src/version.rc
+++ b/src/version.rc
@@ -4,7 +4,7 @@
// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser
// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha
#define VER_FILEVERSION 2,3,0
-#define VER_FILEVERSION_STR "2.3.0alpha4\0"
+#define VER_FILEVERSION_STR "2.3.0alpha5\0"
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_FILEVERSION