diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2020-02-09 08:52:03 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-02-09 08:52:03 -0500 |
| commit | 1f1f99f253f981ae3d11e9df177a144d00dbce0e (patch) | |
| tree | 01d38125329508f5628f9221f797e303180c8fc3 /src | |
| parent | 718c2a56f91f824c5eab69d8cc16294f77243370 (diff) | |
| parent | 3a80685189967fbf4cfa002ac2b8a4fa421306ca (diff) | |
Merge pull request #994 from isanae/generic-file-list
New file tree
Diffstat (limited to 'src')
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);
@@ -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><html><head/><body><p>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.</p></body></html></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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.<br />There is a great tool named &quot;BOSS&quot; to automatically sort these files.</span></p></body></html></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
@@ -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 |
