From 54d98e291701f2187174a67c186f1ea762c6b959 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 16 Jul 2019 05:38:32 -0400 Subject: removed unused or redundant stuff in error_report.h renamed log() to vlog() for now extracted console creation to Console class rewrote LogBuffer to work with logging from uibase, renamed to LogModel added fmt dependency --- src/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f197211a..a359b8a9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -520,6 +520,9 @@ LINK_DIRECTORIES(${Boost_LIBRARY_DIRS}) FIND_PACKAGE(zlib REQUIRED) # TODO FindZlib doesn't find the static zlib library +# fmt +find_package(fmt REQUIRED) + INCLUDE_DIRECTORIES(${project_path}/uibase/src ${project_path}/bsatk/src ${project_path}/esptk/src @@ -551,10 +554,11 @@ ELSE() ENDIF() ADD_EXECUTABLE(ModOrganizer WIN32 ${organizer_HDRS} ${organizer_SRCS} ${organizer_UIS} ${organizer_RCS} ${organizer_QRCS} ${organizer_translations_qm}) + TARGET_LINK_LIBRARIES(ModOrganizer Qt5::Widgets Qt5::WinExtras Qt5::WebEngineWidgets Qt5::Quick Qt5::Qml Qt5::QuickWidgets Qt5::Network Qt5::WebSockets - ${Boost_LIBRARIES} + ${Boost_LIBRARIES} fmt::fmt zlibstatic uibase esptk bsatk githubpp ${usvfs_name} -- cgit v1.3.1 From 6217f910f095adea4b06f370726636b09efea4ed Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 16 Jul 2019 05:42:22 -0400 Subject: renamed logbuffer files to loglist --- src/CMakeLists.txt | 6 +- src/logbuffer.cpp | 171 -------------------------------------------------- src/logbuffer.h | 61 ------------------ src/loglist.cpp | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/loglist.h | 61 ++++++++++++++++++ src/main.cpp | 2 +- src/mainwindow.cpp | 2 +- src/organizercore.cpp | 1 - 8 files changed, 237 insertions(+), 238 deletions(-) delete mode 100644 src/logbuffer.cpp delete mode 100644 src/logbuffer.h create mode 100644 src/loglist.cpp create mode 100644 src/loglist.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a359b8a9..9dbab132 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,7 +73,7 @@ SET(organizer_SRCS mainwindow.cpp main.cpp loghighlighter.cpp - logbuffer.cpp + loglist.cpp lockeddialogbase.cpp lockeddialog.cpp waitingonclosedialog.cpp @@ -181,7 +181,7 @@ SET(organizer_HDRS messagedialog.h mainwindow.h loghighlighter.h - logbuffer.h + loglist.h lockeddialogbase.h lockeddialog.h waitingonclosedialog.h @@ -436,7 +436,7 @@ set(widgets filterwidget icondelegate lcdnumber - logbuffer + loglist loghighlighter modflagicondelegate modidlineedit diff --git a/src/logbuffer.cpp b/src/logbuffer.cpp deleted file mode 100644 index 9e3cd712..00000000 --- a/src/logbuffer.cpp +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "logbuffer.h" -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace MOBase; - -static LogModel* g_instance = nullptr; -const std::size_t MaxLines = 1000; - -LogModel::LogModel() -{ - connect(this, &LogModel::entryAdded, [&](auto&& e){ onEntryAdded(e); }); -} - -void LogModel::create() -{ - g_instance = new LogModel; -} - -LogModel& LogModel::instance() -{ - return *g_instance; -} - -void LogModel::add(MOBase::log::Entry e) -{ - emit entryAdded(std::move(e)); -} - -void LogModel::onEntryAdded(MOBase::log::Entry e) -{ - bool full = false; - if (m_messages.size() > MaxLines) { - m_messages.pop_front(); - full = true; - } - - const int row = static_cast(m_messages.size()); - - if (!full) { - beginInsertRows(QModelIndex(), row, row + 1); - } - - m_messages.emplace_back(std::move(e)); - - if (!full) { - endInsertRows(); - } else { - emit dataChanged( - createIndex(row, 0), - createIndex(row + 1, columnCount({}))); - } -} - -QModelIndex LogModel::index(int row, int column, const QModelIndex&) const -{ - return createIndex(row, column, row); -} - -QModelIndex LogModel::parent(const QModelIndex&) const -{ - return QModelIndex(); -} - -int LogModel::rowCount(const QModelIndex& parent) const -{ - if (parent.isValid()) - return 0; - else - return static_cast(m_messages.size()); -} - -int LogModel::columnCount(const QModelIndex&) const -{ - return 3; -} - -QVariant LogModel::data(const QModelIndex& index, int role) const -{ - using namespace std::chrono; - - const auto row = static_cast(index.row()); - if (row >= m_messages.size()) { - return {}; - } - - const auto& e = m_messages[row]; - - if (role == Qt::DisplayRole) { - if (index.column() == 1) { - const auto ms = duration_cast(e.time.time_since_epoch()); - const auto s = duration_cast(ms); - - const std::time_t t = s.count(); - const std::size_t frac = ms.count() % 1000; - - auto time = QDateTime::fromTime_t(t).time(); - time = time.addMSecs(frac); - - return time.toString("hh:mm:ss.zzz"); - } else if (index.column() == 2) { - return QString::fromStdString(e.message); - } - } - - if (role == Qt::DecorationRole) { - if (index.column() == 0) { - switch (e.level) { - case log::Warning: - return QIcon(":/MO/gui/warning"); - - case log::Error: - return QIcon(":/MO/gui/problem"); - - case log::Debug: // fall-through - case log::Info: - default: - return {}; - } - } - } - - return QVariant(); -} - -QVariant LogModel::headerData(int, Qt::Orientation, int) const -{ - return {}; -} - -void vlog(const char *format, ...) -{ - va_list argList; - va_start(argList, format); - - static const int BUFFERSIZE = 1000; - - char buffer[BUFFERSIZE + 1]; - buffer[BUFFERSIZE] = '\0'; - - vsnprintf(buffer, BUFFERSIZE, format, argList); - - qCritical("%s", buffer); - - va_end(argList); -} diff --git a/src/logbuffer.h b/src/logbuffer.h deleted file mode 100644 index 1bf8901b..00000000 --- a/src/logbuffer.h +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef LOGBUFFER_H -#define LOGBUFFER_H - -#include -#include -#include -#include -#include -#include -#include - -class LogModel : public QAbstractItemModel -{ - Q_OBJECT - -public: - static void create(); - static LogModel& instance(); - - void add(MOBase::log::Entry e); - -protected: - QModelIndex index(int row, int column, const QModelIndex& parent) const override; - QModelIndex parent(const QModelIndex &child) const override; - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - - QVariant headerData( - int section, Qt::Orientation ori, int role=Qt::DisplayRole) const override; - -signals: - void entryAdded(MOBase::log::Entry e); - -private: - std::deque m_messages; - - LogModel(); - void onEntryAdded(MOBase::log::Entry e); -}; - -#endif // LOGBUFFER_H diff --git a/src/loglist.cpp b/src/loglist.cpp new file mode 100644 index 00000000..cb927272 --- /dev/null +++ b/src/loglist.cpp @@ -0,0 +1,171 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "loglist.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MOBase; + +static LogModel* g_instance = nullptr; +const std::size_t MaxLines = 1000; + +LogModel::LogModel() +{ + connect(this, &LogModel::entryAdded, [&](auto&& e){ onEntryAdded(e); }); +} + +void LogModel::create() +{ + g_instance = new LogModel; +} + +LogModel& LogModel::instance() +{ + return *g_instance; +} + +void LogModel::add(MOBase::log::Entry e) +{ + emit entryAdded(std::move(e)); +} + +void LogModel::onEntryAdded(MOBase::log::Entry e) +{ + bool full = false; + if (m_messages.size() > MaxLines) { + m_messages.pop_front(); + full = true; + } + + const int row = static_cast(m_messages.size()); + + if (!full) { + beginInsertRows(QModelIndex(), row, row + 1); + } + + m_messages.emplace_back(std::move(e)); + + if (!full) { + endInsertRows(); + } else { + emit dataChanged( + createIndex(row, 0), + createIndex(row + 1, columnCount({}))); + } +} + +QModelIndex LogModel::index(int row, int column, const QModelIndex&) const +{ + return createIndex(row, column, row); +} + +QModelIndex LogModel::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + +int LogModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + else + return static_cast(m_messages.size()); +} + +int LogModel::columnCount(const QModelIndex&) const +{ + return 3; +} + +QVariant LogModel::data(const QModelIndex& index, int role) const +{ + using namespace std::chrono; + + const auto row = static_cast(index.row()); + if (row >= m_messages.size()) { + return {}; + } + + const auto& e = m_messages[row]; + + if (role == Qt::DisplayRole) { + if (index.column() == 1) { + const auto ms = duration_cast(e.time.time_since_epoch()); + const auto s = duration_cast(ms); + + const std::time_t t = s.count(); + const std::size_t frac = ms.count() % 1000; + + auto time = QDateTime::fromTime_t(t).time(); + time = time.addMSecs(frac); + + return time.toString("hh:mm:ss.zzz"); + } else if (index.column() == 2) { + return QString::fromStdString(e.message); + } + } + + if (role == Qt::DecorationRole) { + if (index.column() == 0) { + switch (e.level) { + case log::Warning: + return QIcon(":/MO/gui/warning"); + + case log::Error: + return QIcon(":/MO/gui/problem"); + + case log::Debug: // fall-through + case log::Info: + default: + return {}; + } + } + } + + return QVariant(); +} + +QVariant LogModel::headerData(int, Qt::Orientation, int) const +{ + return {}; +} + +void vlog(const char *format, ...) +{ + va_list argList; + va_start(argList, format); + + static const int BUFFERSIZE = 1000; + + char buffer[BUFFERSIZE + 1]; + buffer[BUFFERSIZE] = '\0'; + + vsnprintf(buffer, BUFFERSIZE, format, argList); + + qCritical("%s", buffer); + + va_end(argList); +} diff --git a/src/loglist.h b/src/loglist.h new file mode 100644 index 00000000..1bf8901b --- /dev/null +++ b/src/loglist.h @@ -0,0 +1,61 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef LOGBUFFER_H +#define LOGBUFFER_H + +#include +#include +#include +#include +#include +#include +#include + +class LogModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + static void create(); + static LogModel& instance(); + + void add(MOBase::log::Entry e); + +protected: + QModelIndex index(int row, int column, const QModelIndex& parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + QVariant headerData( + int section, Qt::Orientation ori, int role=Qt::DisplayRole) const override; + +signals: + void entryAdded(MOBase::log::Entry e); + +private: + std::deque m_messages; + + LogModel(); + void onEntryAdded(MOBase::log::Entry e); +}; + +#endif // LOGBUFFER_H diff --git a/src/main.cpp b/src/main.cpp index 23ea234a..8b5648c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,7 +39,7 @@ along with Mod Organizer. If not, see . #include "singleinstance.h" #include "utility.h" #include "helper.h" -#include "logbuffer.h" +#include "loglist.h" #include "selectiondialog.h" #include "moapplication.h" #include "tutorialmanager.h" diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cd224414..107f3e09 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -59,7 +59,7 @@ along with Mod Organizer. If not, see . #include "installationmanager.h" #include "lockeddialog.h" #include "waitingonclosedialog.h" -#include "logbuffer.h" +#include "loglist.h" #include "downloadlistsortproxy.h" #include "motddialog.h" #include "filedialogmemory.h" diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 5f5c3afe..d3cd54ee 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -14,7 +14,6 @@ #include "plugincontainer.h" #include "pluginlistsortproxy.h" #include "profile.h" -#include "logbuffer.h" #include "credentialsdialog.h" #include "filedialogmemory.h" #include "modinfodialog.h" -- cgit v1.3.1 From b2a1e1391fdd6bdee1c5e8d337b273447c70a506 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 18 Jul 2019 23:13:57 -0400 Subject: split env --- src/CMakeLists.txt | 25 +- src/env.cpp | 479 ++++++++++++ src/env.h | 117 +++ src/envmetrics.cpp | 224 ++++++ src/envmetrics.h | 28 + src/envmodule.cpp | 390 ++++++++++ src/envmodule.h | 98 +++ src/envsecurity.cpp | 418 ++++++++++ src/envsecurity.h | 49 ++ src/envshortcut.cpp | 376 +++++++++ src/envshortcut.h | 114 +++ src/envwindows.cpp | 236 ++++++ src/envwindows.h | 106 +++ src/main.cpp | 2 + src/mainwindow.cpp | 1 + src/shared/util.cpp | 2123 +-------------------------------------------------- src/shared/util.h | 443 ----------- 17 files changed, 2663 insertions(+), 2566 deletions(-) create mode 100644 src/env.cpp create mode 100644 src/env.h create mode 100644 src/envmetrics.cpp create mode 100644 src/envmetrics.h create mode 100644 src/envmodule.cpp create mode 100644 src/envmodule.h create mode 100644 src/envsecurity.cpp create mode 100644 src/envsecurity.h create mode 100644 src/envshortcut.cpp create mode 100644 src/envshortcut.h create mode 100644 src/envwindows.cpp create mode 100644 src/envwindows.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9dbab132..9785dc3d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -129,6 +129,12 @@ SET(organizer_SRCS filerenamer.cpp texteditor.cpp expanderwidget.cpp + env.cpp + envmetrics.cpp + envmodule.cpp + envsecurity.cpp + envshortcut.cpp + envwindows.cpp shared/windows_error.cpp shared/error_report.cpp @@ -239,6 +245,12 @@ SET(organizer_HDRS filerenamer.h texteditor.h expanderwidget.h + env.h + envmetrics.h + envmodule.h + envsecurity.h + envshortcut.h + envwindows.h shared/windows_error.h shared/error_report.h @@ -350,6 +362,15 @@ set(downloads downloadmanager ) +set(env + env + envmetrics + envmodule + envsecurity + envshortcut + envwindows +) + set(executables executableslist editexecutablesdialog @@ -447,8 +468,8 @@ set(widgets ) set(src_filters - application core browser dialogs downloads executables locking modinfo modinfo\\dialog - modlist plugins previews profiles settings utilities widgets + application core browser dialogs downloads env executables locking modinfo + modinfo\\dialog modlist plugins previews profiles settings utilities widgets ) foreach(filter in list ${src_filters}) diff --git a/src/env.cpp b/src/env.cpp new file mode 100644 index 00000000..641eb4a7 --- /dev/null +++ b/src/env.cpp @@ -0,0 +1,479 @@ +#include "env.h" +#include "envmetrics.h" +#include "envmodule.h" +#include "envsecurity.h" +#include "envshortcut.h" +#include "envwindows.h" +#include +#include + +namespace env +{ + +using namespace MOBase; + +Console::Console() + : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr) +{ + // open a console + if (!AllocConsole()) { + // failed, ignore + } + + m_hasConsole = true; + + // redirect stdin, stdout and stderr to it + freopen_s(&m_in, "CONIN$", "r", stdin); + freopen_s(&m_out, "CONOUT$", "w", stdout); + freopen_s(&m_err, "CONOUT$", "w", stderr); +} + +Console::~Console() +{ + // close redirected handles and redirect standard stream to NUL in case + // they're used after this + + if (m_err) { + std::fclose(m_err); + freopen_s(&m_err, "NUL", "w", stderr); + } + + if (m_out) { + std::fclose(m_out); + freopen_s(&m_out, "NUL", "w", stdout); + } + + if (m_in) { + std::fclose(m_in); + freopen_s(&m_in, "NUL", "r", stdin); + } + + // close console + if (m_hasConsole) { + FreeConsole(); + } +} + + +Environment::Environment() + : m_windows(new WindowsInfo), m_metrics(new Metrics) +{ + m_modules = getLoadedModules(); + m_security = getSecurityProducts(); +} + +// anchor +Environment::~Environment() = default; + +const std::vector& Environment::loadedModules() const +{ + return m_modules; +} + +const WindowsInfo& Environment::windowsInfo() const +{ + return *m_windows; +} + +const std::vector& Environment::securityProducts() const +{ + return m_security; +} + +const Metrics& Environment::metrics() const +{ + return *m_metrics; +} + +void Environment::dump() const +{ + log::debug("windows: {}", m_windows->toString()); + + if (m_windows->compatibilityMode()) { + log::warn("MO seems to be running in compatibility mode"); + } + + log::debug("security products:"); + for (const auto& sp : m_security) { + log::debug(" . {}", sp.toString()); + } + + log::debug("modules loaded in process:"); + for (const auto& m : m_modules) { + log::debug(" . {}", m.toString()); + } + + log::debug("displays:"); + for (const auto& d : m_metrics->displays()) { + log::debug(" . {}", d.toString()); + } +} + + +struct Process +{ + std::wstring filename; + DWORD pid; + + Process(std::wstring f, DWORD id) + : filename(std::move(f)), pid(id) + { + } +}; + + +// returns the filename of the given process or the current one +// +std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE) +{ + // double the buffer size 10 times + const int MaxTries = 10; + + DWORD bufferSize = MAX_PATH; + + for (int tries=0; tries(bufferSize + 1); + std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0); + + DWORD writtenSize = 0; + + if (process == INVALID_HANDLE_VALUE) { + // query this process + writtenSize = GetModuleFileNameW(0, buffer.get(), bufferSize); + } else { + // query another process + writtenSize = GetModuleBaseNameW(process, 0, buffer.get(), bufferSize); + } + + if (writtenSize == 0) { + // hard failure + const auto e = GetLastError(); + std::wcerr << formatSystemMessage(e) << L"\n"; + break; + } else if (writtenSize >= bufferSize) { + // buffer is too small, try again + bufferSize *= 2; + } else { + // if GetModuleFileName() works, `writtenSize` does not include the null + // terminator + const std::wstring s(buffer.get(), writtenSize); + const std::filesystem::path path(s); + + return path.filename().native(); + } + } + + // something failed or the path is way too long to make sense + + std::wstring what; + if (process == INVALID_HANDLE_VALUE) { + what = L"the current process"; + } else { + what = L"pid " + std::to_wstring(reinterpret_cast(process)); + } + + std::wcerr << L"failed to get filename for " << what << L"\n"; + return {}; +} + +std::vector runningProcessesIds() +{ + // double the buffer size 10 times + const int MaxTries = 10; + + // initial size of 300 processes, unlikely to be more than that + std::size_t size = 300; + + for (int tries=0; tries(size); + std::fill(ids.get(), ids.get() + size, 0); + + DWORD bytesGiven = static_cast(size * sizeof(ids[0])); + DWORD bytesWritten = 0; + + if (!EnumProcesses(ids.get(), bytesGiven, &bytesWritten)) + { + const auto e = GetLastError(); + + std::wcerr + << L"failed to enumerate processes, " + << formatSystemMessage(e) << L"\n"; + + return {}; + } + + if (bytesWritten == bytesGiven) { + // no way to distinguish between an exact fit and not enough space, + // just try again + size *= 2; + continue; + } + + const auto count = bytesWritten / sizeof(ids[0]); + return std::vector(ids.get(), ids.get() + count); + } + + std::cerr << L"too many processes to enumerate"; + return {}; +} + +std::vector runningProcesses() +{ + const auto pids = runningProcessesIds(); + std::vector v; + + for (const auto& pid : pids) { + if (pid == 0) { + // the idle process has pid 0 and seems to be picked up by EnumProcesses() + continue; + } + + HandlePtr h(OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid)); + + if (!h) { + const auto e = GetLastError(); + + if (e != ERROR_ACCESS_DENIED) { + // don't log access denied, will happen a lot for system processes, even + // when elevated + std::wcerr + << L"failed to open process " << pid << L", " + << formatSystemMessage(e) << L"\n"; + } + + continue; + } + + auto filename = processFilename(h.get()); + if (!filename.empty()) { + v.emplace_back(std::move(filename), pid); + } + } + + return v; +} + +DWORD findOtherPid() +{ + const std::wstring defaultName = L"ModOrganizer.exe"; + + std::wclog << L"looking for the other process...\n"; + + // used to skip the current process below + const auto thisPid = GetCurrentProcessId(); + std::wclog << L"this process id is " << thisPid << L"\n"; + + // getting the filename for this process, assumes the other process has the + // smae one + auto filename = processFilename(); + if (filename.empty()) { + std::wcerr + << L"can't get current process filename, defaulting to " + << defaultName << L"\n"; + + filename = defaultName; + } else { + std::wclog << L"this process filename is " << filename << L"\n"; + } + + // getting all running processes + const auto processes = runningProcesses(); + std::wclog << L"there are " << processes.size() << L" processes running\n"; + + // going through processes, trying to find one with the same name and a + // different pid than this process has + for (const auto& p : processes) { + if (p.filename == filename) { + if (p.pid != thisPid) { + return p.pid; + } + } + } + + std::wclog + << L"no process with this filename\n" + << L"MO may not be running, or it may be running as administrator\n" + << L"you can try running this again as administrator\n"; + + return 0; +} + +std::wstring tempDir() +{ + const DWORD bufferSize = MAX_PATH + 1; + wchar_t buffer[bufferSize + 1] = {}; + + const auto written = GetTempPathW(bufferSize, buffer); + if (written == 0) { + const auto e = GetLastError(); + + std::wcerr + << L"failed to get temp path, " << formatSystemMessage(e) << L"\n"; + + return {}; + } + + // `written` does not include the null terminator + return std::wstring(buffer, buffer + written); +} + +HandlePtr tempFile(const std::wstring dir) +{ + // maximum tries of incrementing the counter + const int MaxTries = 100; + + // UTC time and date will be in the filename + const auto now = std::time(0); + const auto tm = std::gmtime(&now); + + // "ModOrganizer-YYYYMMDDThhmmss.dmp", with a possible "-i" appended, where + // i can go until MaxTries + std::wostringstream oss; + oss + << L"ModOrganizer-" + << std::setw(4) << (1900 + tm->tm_year) + << std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1) + << std::setw(2) << std::setfill(L'0') << tm->tm_mday << "T" + << std::setw(2) << std::setfill(L'0') << tm->tm_hour + << std::setw(2) << std::setfill(L'0') << tm->tm_min + << std::setw(2) << std::setfill(L'0') << tm->tm_sec; + + const std::wstring prefix = oss.str(); + const std::wstring ext = L".dmp"; + + // first path to try, without counter in it + std::wstring path = dir + L"\\" + prefix + ext; + + for (int i=0; i; + + +struct LibraryFreer +{ + using pointer = HINSTANCE; + + void operator()(HINSTANCE h) + { + if (h != 0) { + ::FreeLibrary(h); + } + } +}; + +struct COMReleaser +{ + void operator()(IUnknown* p) + { + if (p) { + p->Release(); + } + } +}; + + +template +using COMPtr = std::unique_ptr; + + +class Console +{ +public: + Console(); + ~Console(); + +private: + bool m_hasConsole; + FILE* m_in; + FILE* m_out; + FILE* m_err; +}; + + +// represents the process's environment +// +class Environment +{ +public: + Environment(); + ~Environment(); + + // list of loaded modules in the current process + // + const std::vector& loadedModules() const; + + // information about the operating system + // + const WindowsInfo& windowsInfo() const; + + // information about the installed security products + // + const std::vector& securityProducts() const; + + // information about displays + // + const Metrics& metrics() const; + + // logs the environment + // + void dump() const; + +private: + std::vector m_modules; + std::unique_ptr m_windows; + std::vector m_security; + std::unique_ptr m_metrics; +}; + + +enum class CoreDumpTypes +{ + Mini = 1, + Data, + Full +}; + +// creates a minidump file for the given process +// +bool coredump(CoreDumpTypes type); + +// finds another process with the same name as this one and creates a minidump +// file for it +// +bool coredumpOther(CoreDumpTypes type); + +} // namespace env diff --git a/src/envmetrics.cpp b/src/envmetrics.cpp new file mode 100644 index 00000000..a6988909 --- /dev/null +++ b/src/envmetrics.cpp @@ -0,0 +1,224 @@ +#include "envmetrics.h" +#include "env.h" +#include +#include +#include +#include + +namespace env +{ + +using namespace MOBase; + +class DisplayEnumerator +{ +public: + DisplayEnumerator() + : m_GetDpiForMonitor(nullptr) + { + m_shcore.reset(LoadLibraryW(L"Shcore.dll")); + + if (m_shcore) { + // windows 8.1+ only + m_GetDpiForMonitor = reinterpret_cast( + GetProcAddress(m_shcore.get(), "GetDpiForMonitor")); + } + + // gets all monitors and the device they're running on + getDisplayDevices(); + } + + std::vector&& displays() && + { + return std::move(m_displays); + } + + const std::vector& displays() const & + { + return m_displays; + } + +private: + using GetDpiForMonitorFunction = + HRESULT WINAPI (HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); + + std::unique_ptr m_shcore; + GetDpiForMonitorFunction* m_GetDpiForMonitor; + std::vector m_displays; + + void getDisplayDevices() + { + // don't bother if it goes over 100 + for (int i=0; i<100; ++i) { + DISPLAY_DEVICEW device = {}; + device.cb = sizeof(device); + + if (!EnumDisplayDevicesW(nullptr, i, &device, 0)) { + // no more + break; + } + + // EnumDisplayDevices() seems to be returning a lot of devices that are + // not actually monitors, but those don't have the + // DISPLAY_DEVICE_ATTACHED_TO_DESKTOP bit set + if ((device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0) { + continue; + } + + m_displays.push_back(createDisplay(device)); + } + } + + Metrics::Display createDisplay(const DISPLAY_DEVICEW& device) + { + Metrics::Display d; + + d.adapter = QString::fromWCharArray(device.DeviceString); + d.monitor = QString::fromWCharArray(device.DeviceName); + d.primary = (device.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE); + + getDisplaySettings(device.DeviceName, d); + getDpi(d); + + return d; + } + + void getDisplaySettings(const wchar_t* monitorName, Metrics::Display& d) + { + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + + if (!EnumDisplaySettingsW(monitorName, ENUM_CURRENT_SETTINGS, &dm)) { + log::error("EnumDisplaySettings() failed for '{}'", d.monitor); + return; + } + + // all these fields should be available + + if (dm.dmFields & DM_DISPLAYFREQUENCY) { + d.refreshRate = dm.dmDisplayFrequency; + } + + if (dm.dmFields & DM_PELSWIDTH) { + d.resX = dm.dmPelsWidth; + } + + if (dm.dmFields & DM_PELSHEIGHT) { + d.resY = dm.dmPelsHeight; + } + } + + void getDpi(Metrics::Display& d) + { + if (!m_GetDpiForMonitor) { + // this happens on windows 7, get the desktop dpi instead + getDesktopDpi(d); + return; + } + + // there's no way to get an HMONITOR from a device name, so all monitors + // will have to be enumerated and their name checked + HMONITOR hm = findMonitor(d.monitor); + if (!hm) { + log::error("can't get dpi for monitor '{}', not found", d.monitor); + return; + } + + UINT dpiX=0, dpiY=0; + const auto r = m_GetDpiForMonitor(hm, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); + + if (FAILED(r)) { + log::error( + "GetDpiForMonitor() failed for '{}', {}", + d.monitor, formatSystemMessageQ(r)); + + return; + } + + // dpiX and dpiY are always identical, as per the documentation + d.dpi = dpiX; + } + + void getDesktopDpi(Metrics::Display& d) + { + // desktop dc + HDC dc = GetDC(0); + + if (!dc) { + const auto e = GetLastError(); + log::error("can't get desktop DC, {}", formatSystemMessageQ(e)); + return; + } + + d.dpi = GetDeviceCaps(dc, LOGPIXELSX); + + ReleaseDC(0, dc); + } + + HMONITOR findMonitor(const QString& name) + { + // passed to the enumeration callback + struct Data + { + DisplayEnumerator* self; + QString name; + HMONITOR hm; + }; + + Data data = {this, name, 0}; + + // for each monitor + EnumDisplayMonitors(0, nullptr, [](HMONITOR hm, HDC, RECT*, LPARAM lp) { + auto& data = *reinterpret_cast(lp); + + MONITORINFOEX mi = {}; + mi.cbSize = sizeof(mi); + + // monitor info will include the name + if (!GetMonitorInfoW(hm, &mi)) { + const auto e = GetLastError(); + log::error( + "GetMonitorInfo() failed for '{}', {}", + data.name, formatSystemMessageQ(e)); + + // error for this monitor, but continue + return TRUE; + } + + if (QString::fromWCharArray(mi.szDevice) == data.name) { + // found, stop + data.hm = hm; + return FALSE; + } + + // not found, continue to the next monitor + return TRUE; + }, reinterpret_cast(&data)); + + return data.hm; + } +}; + + +Metrics::Metrics() +{ + m_displays = DisplayEnumerator().displays(); +} + +const std::vector& Metrics::displays() const +{ + return m_displays; +} + +QString Metrics::Display::toString() const +{ + return QString("%1*%2 %3hz dpi=%4 on %5%6") + .arg(resX) + .arg(resY) + .arg(refreshRate) + .arg(dpi) + .arg(adapter) + .arg(primary ? " (primary)" : ""); +} + +} // namespace diff --git a/src/envmetrics.h b/src/envmetrics.h new file mode 100644 index 00000000..62fc8c49 --- /dev/null +++ b/src/envmetrics.h @@ -0,0 +1,28 @@ +#include +#include + +namespace env +{ + +class Metrics +{ +public: + struct Display + { + int resX=0, resY=0, dpi=0; + bool primary=false; + int refreshRate = 0; + QString monitor, adapter; + + QString toString() const; + }; + + Metrics(); + + const std::vector& displays() const; + +private: + std::vector m_displays; +}; + +} // namespace diff --git a/src/envmodule.cpp b/src/envmodule.cpp new file mode 100644 index 00000000..1717da15 --- /dev/null +++ b/src/envmodule.cpp @@ -0,0 +1,390 @@ +#include "envmodule.h" +#include "env.h" +#include + +namespace env +{ + +using namespace MOBase; + +Module::Module(QString path, std::size_t fileSize) + : m_path(std::move(path)), m_fileSize(fileSize) +{ + const auto fi = getFileInfo(); + + m_version = getVersion(fi.ffi); + m_timestamp = getTimestamp(fi.ffi); + m_versionString = fi.fileDescription; + m_md5 = getMD5(); +} + +const QString& Module::path() const +{ + return m_path; +} + +QString Module::displayPath() const +{ + return QDir::fromNativeSeparators(m_path.toLower()); +} + +std::size_t Module::fileSize() const +{ + return m_fileSize; +} + +const QString& Module::version() const +{ + return m_version; +} + +const QString& Module::versionString() const +{ + return m_versionString; +} + +const QDateTime& Module::timestamp() const +{ + return m_timestamp; +} + +const QString& Module::md5() const +{ + return m_md5; +} + +QString Module::timestampString() const +{ + if (!m_timestamp.isValid()) { + return "(no timestamp)"; + } + + return m_timestamp.toString(Qt::DateFormat::ISODate); +} + +QString Module::toString() const +{ + QStringList sl; + + // file size + sl.push_back(displayPath()); + sl.push_back(QString("%1 B").arg(m_fileSize)); + + // version + if (m_version.isEmpty() && m_versionString.isEmpty()) { + sl.push_back("(no version)"); + } else { + if (!m_version.isEmpty()) { + sl.push_back(m_version); + } + + if (!m_versionString.isEmpty() && m_versionString != m_version) { + sl.push_back(versionString()); + } + } + + // timestamp + if (m_timestamp.isValid()) { + sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate)); + } else { + sl.push_back("(no timestamp)"); + } + + // md5 + if (!m_md5.isEmpty()) { + sl.push_back(m_md5); + } + + return sl.join(", "); +} + +Module::FileInfo Module::getFileInfo() const +{ + const auto wspath = m_path.toStdWString(); + + // getting version info size + DWORD dummy = 0; + const DWORD size = GetFileVersionInfoSizeW(wspath.c_str(), &dummy); + + if (size == 0) { + const auto e = GetLastError(); + + if (e == ERROR_RESOURCE_TYPE_NOT_FOUND) { + // not an error, no version information built into that module + return {}; + } + + qCritical().nospace().noquote() + << "GetFileVersionInfoSizeW() failed on '" << m_path << "', " + << formatSystemMessageQ(e); + + return {}; + } + + // getting version info + auto buffer = std::make_unique(size); + + if (!GetFileVersionInfoW(wspath.c_str(), 0, size, buffer.get())) { + const auto e = GetLastError(); + + qCritical().nospace().noquote() + << "GetFileVersionInfoW() failed on '" << m_path << "', " + << formatSystemMessageQ(e); + + return {}; + } + + // the version info has two major parts: a fixed version and a localizable + // set of strings + + FileInfo fi; + fi.ffi = getFixedFileInfo(buffer.get()); + fi.fileDescription = getFileDescription(buffer.get()); + + return fi; +} + +VS_FIXEDFILEINFO Module::getFixedFileInfo(std::byte* buffer) const +{ + void* valuePointer = nullptr; + unsigned int valueSize = 0; + + // the fixed version info is in the root + const auto ret = VerQueryValueW(buffer, L"\\", &valuePointer, &valueSize); + + if (!ret || !valuePointer || valueSize == 0) { + // not an error, no fixed file info + return {}; + } + + const auto* fi = static_cast(valuePointer); + + // signature is always 0xfeef04bd + if (fi->dwSignature != 0xfeef04bd) { + qCritical().nospace().noquote() + << "bad file info signature 0x" << hex << fi->dwSignature << " for " + << "'" << m_path << "'"; + + return {}; + } + + return *fi; +} + +QString Module::getFileDescription(std::byte* buffer) const +{ + struct LANGANDCODEPAGE + { + WORD wLanguage; + WORD wCodePage; + }; + + void* valuePointer = nullptr; + unsigned int valueSize = 0; + + // getting list of available languages + auto ret = VerQueryValueW( + buffer, L"\\VarFileInfo\\Translation", &valuePointer, &valueSize); + + if (!ret || !valuePointer || valueSize == 0) { + qCritical().nospace().noquote() + << "VerQueryValueW() for translations failed on '" << m_path << "'"; + + return {}; + } + + // number of languages + const auto count = valueSize / sizeof(LANGANDCODEPAGE); + if (count == 0) { + return {}; + } + + // using the first language in the list to get FileVersion + const auto* lcp = static_cast(valuePointer); + + const auto subBlock = QString("\\StringFileInfo\\%1%2\\FileVersion") + .arg(lcp->wLanguage, 4, 16, QChar('0')) + .arg(lcp->wCodePage, 4, 16, QChar('0')); + + ret = VerQueryValueW( + buffer, subBlock.toStdWString().c_str(), &valuePointer, &valueSize); + + if (!ret || !valuePointer || valueSize == 0) { + // not an error, no file version + return {}; + } + + // valueSize includes the null terminator + return QString::fromWCharArray( + static_cast(valuePointer), valueSize - 1); +} + +QString Module::getVersion(const VS_FIXEDFILEINFO& fi) const +{ + if (fi.dwSignature == 0) { + return {}; + } + + const DWORD major = (fi.dwFileVersionMS >> 16 ) & 0xffff; + const DWORD minor = (fi.dwFileVersionMS >> 0 ) & 0xffff; + const DWORD maintenance = (fi.dwFileVersionLS >> 16 ) & 0xffff; + const DWORD build = (fi.dwFileVersionLS >> 0 ) & 0xffff; + + if (major == 0 && minor == 0 && maintenance == 0 && build == 0) { + return {}; + } + + return QString("%1.%2.%3.%4") + .arg(major).arg(minor).arg(maintenance).arg(build); +} + +QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const +{ + FILETIME ft = {}; + + if (fi.dwSignature == 0 || (fi.dwFileDateMS == 0 && fi.dwFileDateLS == 0)) { + // if the file info is invalid or doesn't have a date, use the creation + // time on the file + + // opening the file + HandlePtr h(CreateFileW( + m_path.toStdWString().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)); + + if (h.get() == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + + qCritical().nospace().noquote() + << "can't open file '" << m_path << "' for timestamp, " + << formatSystemMessageQ(e); + + return {}; + } + + // getting the file time + if (!GetFileTime(h.get(), &ft, nullptr, nullptr)) { + const auto e = GetLastError(); + qCritical().nospace().noquote() + << "can't get file time for '" << m_path << "', " + << formatSystemMessageQ(e); + + return {}; + } + } else { + // use the time from the file info + ft.dwHighDateTime = fi.dwFileDateMS; + ft.dwLowDateTime = fi.dwFileDateLS; + } + + + // converting to SYSTEMTIME + SYSTEMTIME utc = {}; + + if (!FileTimeToSystemTime(&ft, &utc)) { + qCritical().nospace().noquote() + << "FileTimeToSystemTime() failed on timestamp " + << "high=0x" << hex << ft.dwHighDateTime << " " + << "low=0x" << hex << ft.dwLowDateTime << " for " + << "'" << m_path << "'"; + + return {}; + } + + return QDateTime( + QDate(utc.wYear, utc.wMonth, utc.wDay), + QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds)); +} + +QString Module::getMD5() const +{ + if (m_path.contains("\\windows\\", Qt::CaseInsensitive)) { + // don't calculate md5 for system files, it's not really relevant and + // it takes a while + return {}; + } + + // opening the file + QFile f(m_path); + + if (!f.open(QFile::ReadOnly)) { + qCritical().nospace().noquote() + << "failed to open file '" << m_path << "' for md5"; + + return {}; + } + + // hashing + QCryptographicHash hash(QCryptographicHash::Md5); + if (!hash.addData(&f)) { + qCritical().nospace().noquote() + << "failed to calculate md5 for '" << m_path << "'"; + + return {}; + } + + return hash.result().toHex(); +} + + +std::vector getLoadedModules() +{ + HandlePtr snapshot(CreateToolhelp32Snapshot( + TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId())); + + if (snapshot.get() == INVALID_HANDLE_VALUE) + { + const auto e = GetLastError(); + + qCritical().nospace().noquote() + << "CreateToolhelp32Snapshot() failed, " + << formatSystemMessageQ(e); + + return {}; + } + + MODULEENTRY32 me = {}; + me.dwSize = sizeof(me); + + // first module, this shouldn't fail because there's at least the executable + if (!Module32First(snapshot.get(), &me)) + { + const auto e = GetLastError(); + + qCritical().nospace().noquote() + << "Module32First() failed, " << formatSystemMessageQ(e); + + return {}; + } + + std::vector v; + + for (;;) + { + const auto path = QString::fromWCharArray(me.szExePath); + if (!path.isEmpty()) { + v.push_back(Module(path, me.modBaseSize)); + } + + // next module + if (!Module32Next(snapshot.get(), &me)) { + const auto e = GetLastError(); + + // no more modules is not an error + if (e != ERROR_NO_MORE_FILES) { + qCritical().nospace().noquote() + << "Module32Next() failed, " << formatSystemMessageQ(e); + } + + break; + } + } + + // sorting by display name + std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) { + return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0); + }); + + return v; +} + +} // namespace diff --git a/src/envmodule.h b/src/envmodule.h new file mode 100644 index 00000000..ea1156bd --- /dev/null +++ b/src/envmodule.h @@ -0,0 +1,98 @@ +#include +#include + +namespace env +{ + +// represents one module +// +class Module +{ +public: + explicit Module(QString path, std::size_t fileSize); + + // returns the module's path + // + const QString& path() const; + + // returns the module's path in lowercase and using forward slashes + // + QString displayPath() const; + + // returns the size in bytes, may be 0 + // + std::size_t fileSize() const; + + // returns the x.x.x.x version embedded from the version info, may be empty + // + const QString& version() const; + + // returns the FileVersion entry from the resource file, returns + // "(no version)" if not available + // + const QString& versionString() const; + + // returns the build date from the version info, or the creation time of the + // file on the filesystem, may be empty + // + const QDateTime& timestamp() const; + + // returns the md5 of the file, may be empty for system files + // + const QString& md5() const; + + // converts timestamp() to a string for display, returns "(no timestamp)" if + // not available + // + QString timestampString() const; + + // returns a string with all the above information on one line + // + QString toString() const; + +private: + // contains the information from the version resource + // + struct FileInfo + { + VS_FIXEDFILEINFO ffi; + QString fileDescription; + }; + + QString m_path; + std::size_t m_fileSize; + QString m_version; + QDateTime m_timestamp; + QString m_versionString; + QString m_md5; + + // returns information from the version resource + // + FileInfo getFileInfo() const; + + // uses VS_FIXEDFILEINFO to build the version string + // + QString getVersion(const VS_FIXEDFILEINFO& fi) const; + + // uses the file date from VS_FIXEDFILEINFO if available, or gets the + // creation date on the file + // + QDateTime getTimestamp(const VS_FIXEDFILEINFO& fi) const; + + // returns the md5 hash unless the path contains "\windows\" + // + QString getMD5() const; + + // gets VS_FIXEDFILEINFO from the file version info buffer + // + VS_FIXEDFILEINFO getFixedFileInfo(std::byte* buffer) const; + + // gets FileVersion from the file version info buffer + // + QString getFileDescription(std::byte* buffer) const; +}; + + +std::vector getLoadedModules(); + +} // namespace env diff --git a/src/envsecurity.cpp b/src/envsecurity.cpp new file mode 100644 index 00000000..559ce4ad --- /dev/null +++ b/src/envsecurity.cpp @@ -0,0 +1,418 @@ +#include "envsecurity.h" +#include "env.h" +#include + +#include +#include +#include +#include +#pragma comment(lib, "Wbemuuid.lib") + +namespace env +{ + +using namespace MOBase; + +class WMI +{ +public: + class failed {}; + + WMI(const std::string& ns) + { + try + { + createLocator(); + createService(ns); + setSecurity(); + } + catch(failed&) + { + } + } + + template + void query(const std::string& q, F&& f) + { + if (!m_locator || !m_service) { + return; + } + + auto enumerator = getEnumerator(q); + if (!enumerator) { + return; + } + + for (;;) + { + COMPtr object; + + { + IWbemClassObject* rawObject = nullptr; + ULONG count = 0; + auto ret = enumerator->Next(WBEM_INFINITE, 1, &rawObject, &count); + + if (count == 0 || !rawObject) { + break; + } + + if (FAILED(ret)) { + qCritical() + << "enumerator->next() failed, " << formatSystemMessageQ(ret); + break; + } + + object.reset(rawObject); + } + + f(object.get()); + } + } + +private: + COMPtr m_locator; + COMPtr m_service; + + void createLocator() + { + void* rawLocator = nullptr; + + const auto ret = CoCreateInstance( + CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, + IID_IWbemLocator, &rawLocator); + + if (FAILED(ret) || !rawLocator) { + qCritical() + << "CoCreateInstance for WbemLocator failed, " + << formatSystemMessageQ(ret); + + throw failed(); + } + + m_locator.reset(static_cast(rawLocator)); + } + + void createService(const std::string& ns) + { + IWbemServices* rawService = nullptr; + + const auto res = m_locator->ConnectServer( + _bstr_t(ns.c_str()), + nullptr, nullptr, nullptr, 0, nullptr, nullptr, + &rawService); + + if (FAILED(res) || !rawService) { + qCritical() + << "locator->ConnectServer() failed for namespace " + << "'" << QString::fromStdString(ns) << "', " + << formatSystemMessageQ(res); + + throw failed(); + } + + m_service.reset(rawService); + } + + void setSecurity() + { + auto ret = CoSetProxyBlanket( + m_service.get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, + RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE); + + if (FAILED(ret)) + { + qCritical() + << "CoSetProxyBlanket() failed, " << formatSystemMessageQ(ret); + + throw failed(); + } + } + + COMPtr getEnumerator( + const std::string& query) + { + IEnumWbemClassObject* rawEnumerator = NULL; + + auto ret = m_service->ExecQuery( + bstr_t("WQL"), + bstr_t(query.c_str()), + WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, + NULL, + &rawEnumerator); + + if (FAILED(ret) || !rawEnumerator) + { + qCritical() + << "query '" << QString::fromStdString(query) << "' failed, " + << formatSystemMessageQ(ret); + + return {}; + } + + return COMPtr(rawEnumerator); + } +}; + + +SecurityProduct::SecurityProduct( + QUuid guid, QString name, int provider, + bool active, bool upToDate) : + m_guid(std::move(guid)), m_name(std::move(name)), m_provider(provider), + m_active(active), m_upToDate(upToDate) +{ +} + +const QString& SecurityProduct::name() const +{ + return m_name; +} + +int SecurityProduct::provider() const +{ + return m_provider; +} + +bool SecurityProduct::active() const +{ + return m_active; +} + +bool SecurityProduct::upToDate() const +{ + return m_upToDate; +} + +QString SecurityProduct::toString() const +{ + QString s; + + s += m_name + " (" + providerToString() + ")"; + + if (!m_active) { + s += ", inactive"; + } + + if (!m_upToDate) { + s += ", definitions outdated"; + } + + if (!m_guid.isNull()) { + s += ", " + m_guid.toString(QUuid::QUuid::WithoutBraces); + } + + return s; +} + +QString SecurityProduct::providerToString() const +{ + QStringList ps; + + if (m_provider & WSC_SECURITY_PROVIDER_FIREWALL) { + ps.push_back("firewall"); + } + + if (m_provider & WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS) { + ps.push_back("autoupdate"); + } + + if (m_provider & WSC_SECURITY_PROVIDER_ANTIVIRUS) { + ps.push_back("antivirus"); + } + + if (m_provider & WSC_SECURITY_PROVIDER_ANTISPYWARE) { + ps.push_back("antispyware"); + } + + if (m_provider & WSC_SECURITY_PROVIDER_INTERNET_SETTINGS) { + ps.push_back("settings"); + } + + if (m_provider & WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL) { + ps.push_back("uac"); + } + + if (m_provider & WSC_SECURITY_PROVIDER_SERVICE) { + ps.push_back("service"); + } + + if (ps.empty()) { + return "doesn't provider anything"; + } + + return ps.join("|"); +} + + +std::vector getSecurityProductsFromWMI() +{ + // some products may be present in multiple queries, such as a product marked + // as both antivirus and antispyware, but they'll have the same GUID, so use + // that to avoid duplicating entries + std::map map; + + auto handleProduct = [&](auto* o) { + VARIANT prop; + + // display name + auto ret = o->Get(L"displayName", 0, &prop, 0, 0); + if (FAILED(ret)) { + qCritical() + << "failed to get displayName, " + << formatSystemMessageQ(ret); + + return; + } + + if (prop.vt != VT_BSTR) { + qCritical() << "displayName is a " << prop.vt << ", not a bstr"; + return; + } + + const std::wstring name = prop.bstrVal; + VariantClear(&prop); + + // product state + ret = o->Get(L"productState", 0, &prop, 0, 0); + if (FAILED(ret)) { + qCritical() + << "failed to get productState, " + << formatSystemMessageQ(ret); + + return; + } + + if (prop.vt != VT_UI4 && prop.vt != VT_I4) { + qCritical() << "productState is a " << prop.vt << ", is not a VT_UI4"; + return; + } + + DWORD state = 0; + if (prop.vt == VT_I4) { + state = prop.lVal; + } else { + state = prop.ulVal; + } + + VariantClear(&prop); + + // guid + ret = o->Get(L"instanceGuid", 0, &prop, 0, 0); + if (FAILED(ret)) { + qCritical() + << "failed to get instanceGuid, " + << formatSystemMessageQ(ret); + + return; + } + + if (prop.vt != VT_BSTR) { + qCritical() << "instanceGuid is a " << prop.vt << ", is not a bstr"; + return; + } + + const QUuid guid(QString::fromWCharArray(prop.bstrVal)); + VariantClear(&prop); + + const auto provider = static_cast((state >> 16) & 0xff); + const auto scanner = (state >> 8) & 0xff; + const auto definitions = state & 0xff; + + const bool active = ((scanner & 0x10) != 0); + const bool upToDate = (definitions == 0); + + map.insert({ + guid, + {guid, QString::fromStdWString(name), provider, active, upToDate}}); + }; + + { + WMI wmi("root\\SecurityCenter2"); + wmi.query("select * from AntivirusProduct", handleProduct); + wmi.query("select * from FirewallProduct", handleProduct); + wmi.query("select * from AntiSpywareProduct", handleProduct); + } + + { + WMI wmi("root\\SecurityCenter"); + wmi.query("select * from AntivirusProduct", handleProduct); + wmi.query("select * from FirewallProduct", handleProduct); + wmi.query("select * from AntiSpywareProduct", handleProduct); + } + + std::vector v; + + for (auto&& p : map) { + v.push_back(p.second); + } + + return v; +} + +std::optional getWindowsFirewall() +{ + HRESULT hr = 0; + + COMPtr policy; + + { + void* rawPolicy = nullptr; + + hr = CoCreateInstance( + __uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER, + __uuidof(INetFwPolicy2), &rawPolicy); + + if (FAILED(hr) || !rawPolicy) { + qCritical() + << "CoCreateInstance for NetFwPolicy2 failed, " + << formatSystemMessageQ(hr); + + return {}; + } + + policy.reset(static_cast(rawPolicy)); + } + + VARIANT_BOOL enabledVariant; + + if (policy) { + hr = policy->get_FirewallEnabled(NET_FW_PROFILE2_PUBLIC, &enabledVariant); + if (FAILED(hr)) + { + qCritical() + << "get_FirewallEnabled failed, " + << formatSystemMessageQ(hr); + + return {}; + } + } + + const auto enabled = (enabledVariant != VARIANT_FALSE); + if (!enabled) { + return {}; + } + + return SecurityProduct( + {}, "Windows Firewall", WSC_SECURITY_PROVIDER_FIREWALL, true, true); +} + + +std::vector getSecurityProducts() +{ + std::vector v; + + { + auto fromWMI = getSecurityProductsFromWMI(); + v.insert( + v.end(), + std::make_move_iterator(fromWMI.begin()), + std::make_move_iterator(fromWMI.end())); + } + + if (auto p=getWindowsFirewall()) { + v.push_back(std::move(*p)); + } + + return v; +} + +} // namespace diff --git a/src/envsecurity.h b/src/envsecurity.h new file mode 100644 index 00000000..200cb531 --- /dev/null +++ b/src/envsecurity.h @@ -0,0 +1,49 @@ +#include +#include + +namespace env +{ + +// represents a security product, such as an antivirus or a firewall +// +class SecurityProduct +{ +public: + SecurityProduct( + QUuid guid, QString name, int provider, + bool active, bool upToDate); + + // display name of the product + // + const QString& name() const; + + // a bunch of _WSC_SECURITY_PROVIDER flags + // + int provider() const; + + // whether the product is active + // + bool active() const; + + // whether its definitions are up-to-date + // + bool upToDate() const; + + // string representation of the above + // + QString toString() const; + +private: + QUuid m_guid; + QString m_name; + int m_provider; + bool m_active; + bool m_upToDate; + + QString providerToString() const; +}; + + +std::vector getSecurityProducts(); + +} // namespace env diff --git a/src/envshortcut.cpp b/src/envshortcut.cpp new file mode 100644 index 00000000..30ef4633 --- /dev/null +++ b/src/envshortcut.cpp @@ -0,0 +1,376 @@ +#include "envshortcut.h" +#include "env.h" +#include "executableslist.h" +#include "instancemanager.h" +#include + +namespace env +{ + +using namespace MOBase; + +class ShellLinkException +{ +public: + ShellLinkException(QString s) + : m_what(std::move(s)) + { + } + + const QString& what() const + { + return m_what; + } + +private: + QString m_what; +}; + +// just a wrapper around IShellLink operations that throws ShellLinkException +// on errors +// +class ShellLinkWrapper +{ +public: + ShellLinkWrapper() + { + m_link = createShellLink(); + m_file = createPersistFile(); + } + + void setPath(const QString& s) + { + if (s.isEmpty()) { + throw ShellLinkException("path cannot be empty"); + } + + const auto r = m_link->SetPath(s.toStdWString().c_str()); + throwOnFail(r, QString("failed to set target path '%1'").arg(s)); + } + + void setArguments(const QString& s) + { + const auto r = m_link->SetArguments(s.toStdWString().c_str()); + throwOnFail(r, QString("failed to set arguments '%1'").arg(s)); + } + + void setDescription(const QString& s) + { + if (s.isEmpty()) { + return; + } + + const auto r = m_link->SetDescription(s.toStdWString().c_str()); + throwOnFail(r, QString("failed to set description '%1'").arg(s)); + } + + void setIcon(const QString& file, int i) + { + if (file.isEmpty()) { + return; + } + + const auto r = m_link->SetIconLocation(file.toStdWString().c_str(), i); + throwOnFail(r, QString("failed to set icon '%1' @ %2").arg(file).arg(i)); + } + + void setWorkingDirectory(const QString& s) + { + if (s.isEmpty()) { + return; + } + + const auto r = m_link->SetWorkingDirectory(s.toStdWString().c_str()); + throwOnFail(r, QString("failed to set working directory '%1'").arg(s)); + } + + void save(const QString& path) + { + const auto r = m_file->Save(path.toStdWString().c_str(), TRUE); + throwOnFail(r, QString("failed to save link '%1'").arg(path)); + } + +private: + COMPtr m_link; + COMPtr m_file; + + void throwOnFail(HRESULT r, const QString& s) + { + if (FAILED(r)) { + throw ShellLinkException(QString("%1, %2") + .arg(s) + .arg(formatSystemMessageQ(r))); + } + } + + COMPtr createShellLink() + { + void* link = nullptr; + + const auto r = CoCreateInstance( + CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLink, &link); + + throwOnFail(r, "failed to create IShellLink instance"); + + if (!link) { + throw ShellLinkException("creating IShellLink worked, pointer is null"); + } + + return COMPtr(static_cast(link)); + } + + COMPtr createPersistFile() + { + void* file = nullptr; + + const auto r = m_link->QueryInterface(IID_IPersistFile, &file); + throwOnFail(r, "failed to get IPersistFile interface"); + + if (!file) { + throw ShellLinkException("querying IPersistFile worked, pointer is null"); + } + + return COMPtr(static_cast(file)); + } +}; + + +Shortcut::Shortcut() + : m_iconIndex(0) +{ +} + +Shortcut::Shortcut(const Executable& exe) + : Shortcut() +{ + m_name = exe.title(); + m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath(); + + m_arguments = QString("\"moshortcut://%1:%2\"") + .arg(InstanceManager::instance().currentInstance()) + .arg(exe.title()); + + m_description = QString("Run %1 with ModOrganizer").arg(exe.title()); + + if (exe.usesOwnIcon()) { + m_icon = exe.binaryInfo().absoluteFilePath(); + } + + m_workingDirectory = qApp->applicationDirPath(); +} + +Shortcut& Shortcut::name(const QString& s) +{ + m_name = s; + return *this; +} + +Shortcut& Shortcut::target(const QString& s) +{ + m_target = s; + return *this; +} + +Shortcut& Shortcut::arguments(const QString& s) +{ + m_arguments = s; + return *this; +} + +Shortcut& Shortcut::description(const QString& s) +{ + m_description = s; + return *this; +} + +Shortcut& Shortcut::icon(const QString& s, int index) +{ + m_icon = s; + m_iconIndex = index; + return *this; +} + +Shortcut& Shortcut::workingDirectory(const QString& s) +{ + m_workingDirectory = s; + return *this; +} + +bool Shortcut::exists(Locations loc) const +{ + const auto path = shortcutPath(loc); + if (path.isEmpty()) { + return false; + } + + return QFileInfo(path).exists(); +} + +bool Shortcut::toggle(Locations loc) +{ + if (exists(loc)) { + return remove(loc); + } else { + return add(loc); + } +} + +bool Shortcut::add(Locations loc) +{ + debug() + << "adding shortcut to " << toString(loc) << ":\n" + << " . name: '" << m_name << "'\n" + << " . target: '" << m_target << "'\n" + << " . arguments: '" << m_arguments << "'\n" + << " . description: '" << m_description << "'\n" + << " . icon: '" << m_icon << "' @ " << m_iconIndex << "\n" + << " . working directory: '" << m_workingDirectory << "'"; + + if (m_target.isEmpty()) { + critical() << "target is empty"; + return false; + } + + const auto path = shortcutPath(loc); + if (path.isEmpty()) { + return false; + } + + debug() << "shorcut file will be saved at '" << path << "'"; + + try + { + ShellLinkWrapper link; + + link.setPath(m_target); + link.setArguments(m_arguments); + link.setDescription(m_description); + link.setIcon(m_icon, m_iconIndex); + link.setWorkingDirectory(m_workingDirectory); + + link.save(path); + + return true; + } + catch(ShellLinkException& e) + { + critical() << e.what() << "\nshortcut file was not saved"; + } + + return false; +} + +bool Shortcut::remove(Locations loc) +{ + debug() << "removing shortcut for '" << m_name << "' from " << toString(loc); + + const auto path = shortcutPath(loc); + if (path.isEmpty()) { + return false; + } + + debug() << "path to shortcut file is '" << path << "'"; + + if (!QFile::exists(path)) { + critical() << "can't remove '" << path << "', file not found"; + return false; + } + + if (!MOBase::shellDelete({path})) { + const auto e = ::GetLastError(); + + critical() + << "failed to remove '" << path << "', " + << formatSystemMessageQ(e); + + return false; + } + + return true; +} + +QString Shortcut::shortcutPath(Locations loc) const +{ + const auto dir = shortcutDirectory(loc); + if (dir.isEmpty()) { + return {}; + } + + const auto file = shortcutFilename(); + if (file.isEmpty()) { + return {}; + } + + return dir + QDir::separator() + file; +} + +QString Shortcut::shortcutDirectory(Locations loc) const +{ + QString dir; + + try + { + switch (loc) + { + case Desktop: + dir = MOBase::getDesktopDirectory(); + break; + + case StartMenu: + dir = MOBase::getStartMenuDirectory(); + break; + + case None: + default: + critical() << "bad location " << loc; + break; + } + } + catch(std::exception&) + { + } + + return QDir::toNativeSeparators(dir); +} + +QString Shortcut::shortcutFilename() const +{ + if (m_name.isEmpty()) { + critical() << "name is empty"; + return {}; + } + + return m_name + ".lnk"; +} + +QDebug Shortcut::debug() const +{ + return qDebug().noquote().nospace() << "system shortcut: "; +} + +QDebug Shortcut::critical() const +{ + return qCritical().noquote().nospace() << "system shortcut: "; +} + + +QString toString(Shortcut::Locations loc) +{ + switch (loc) + { + case Shortcut::None: + return "none"; + + case Shortcut::Desktop: + return "desktop"; + + case Shortcut::StartMenu: + return "start menu"; + + default: + return QString("? (%1)").arg(static_cast(loc)); + } +} + +} // namespace diff --git a/src/envshortcut.h b/src/envshortcut.h new file mode 100644 index 00000000..904b3ab7 --- /dev/null +++ b/src/envshortcut.h @@ -0,0 +1,114 @@ +#include + +class Executable; + +namespace env +{ + +// an application shortcut that can be either on the desktop or the start menu +// +class Shortcut +{ +public: + // location of a shortcut + // + enum Locations + { + None = 0, + + // on the desktop + Desktop, + + // in the start menu + StartMenu + }; + + + // empty shortcut + // + Shortcut(); + + // shortcut from an executable + // + explicit Shortcut(const Executable& exe); + + // sets the name of the shortcut, shown on icons and start menu entries + // + Shortcut& name(const QString& s); + + // the program to start + // + Shortcut& target(const QString& s); + + // arguments to pass + // + Shortcut& arguments(const QString& s); + + // shows in the status bar of explorer, for example + // + Shortcut& description(const QString& s); + + // path to a binary that contains the icon and its index + // + Shortcut& icon(const QString& s, int index=0); + + // "start in" option for this shortcut + // + Shortcut& workingDirectory(const QString& s); + + + // returns whether this shortcut already exists at the given location; this + // does not check whether the shortcut parameters are different, it merely if + // the .lnk file exists + // + bool exists(Locations loc) const; + + // calls remove() if exists(), or add() + // + bool toggle(Locations loc); + + // adds the shortcut to the given location + // + bool add(Locations loc); + + // removes the shortcut from the given location + // + bool remove(Locations loc); + +private: + QString m_name; + QString m_target; + QString m_arguments; + QString m_description; + QString m_icon; + int m_iconIndex; + QString m_workingDirectory; + + // returns a qCritical() logger with a prefix already logged + // + QDebug critical() const; + + // returns a qDebug() logger with a prefix already logged + // + QDebug debug() const; + + + // returns the path where the shortcut file should be saved + // + QString shortcutPath(Locations loc) const; + + // returns the directory where the shortcut file should be saved + // + QString shortcutDirectory(Locations loc) const; + + // returns the filename of the shortcut file that should be used when saving + // + QString shortcutFilename() const; +}; + + +// returns a string representation of the given location +// +QString toString(Shortcut::Locations loc); + +} // namespace diff --git a/src/envwindows.cpp b/src/envwindows.cpp new file mode 100644 index 00000000..718cf2ce --- /dev/null +++ b/src/envwindows.cpp @@ -0,0 +1,236 @@ +#include "envwindows.h" +#include "env.h" +#include + +namespace env +{ + +using namespace MOBase; + +WindowsInfo::WindowsInfo() +{ + // loading ntdll.dll, the functions will be found with GetProcAddress() + std::unique_ptr ntdll(LoadLibraryW(L"ntdll.dll")); + + if (!ntdll) { + qCritical() << "failed to load ntdll.dll while getting version"; + return; + } else { + m_reported = getReportedVersion(ntdll.get()); + m_real = getRealVersion(ntdll.get()); + } + + m_release = getRelease(); + m_elevated = getElevated(); +} + +bool WindowsInfo::compatibilityMode() const +{ + if (m_real == Version()) { + // don't know the real version, can't guess compatibility mode + return false; + } + + return (m_real != m_reported); +} + +const WindowsInfo::Version& WindowsInfo::reportedVersion() const +{ + return m_reported; +} + +const WindowsInfo::Version& WindowsInfo::realVersion() const +{ + return m_real; +} + +const WindowsInfo::Release& WindowsInfo::release() const +{ + return m_release; +} + +std::optional WindowsInfo::isElevated() const +{ + return m_elevated; +} + +QString WindowsInfo::toString() const +{ + QStringList sl; + + const QString reported = m_reported.toString(); + const QString real = m_real.toString(); + + // version + sl.push_back("version " + reported); + + // real version if different + if (compatibilityMode()) { + sl.push_back("real version " + real); + } + + // build.UBR, such as 17763.557 + if (m_release.UBR != 0) { + DWORD build = 0; + + if (compatibilityMode()) { + build = m_real.build; + } else { + build = m_reported.build; + } + + sl.push_back(QString("%1.%2").arg(build).arg(m_release.UBR)); + } + + // release ID + if (!m_release.ID.isEmpty()) { + sl.push_back("release " + m_release.ID); + } + + // buildlab string + if (!m_release.buildLab.isEmpty()) { + sl.push_back(m_release.buildLab); + } + + // product name + if (!m_release.productName.isEmpty()) { + sl.push_back(m_release.productName); + } + + // elevated + QString elevated = "?"; + if (m_elevated.has_value()) { + elevated = (*m_elevated ? "yes" : "no"); + } + + sl.push_back("elevated: " + elevated); + + return sl.join(", "); +} + +WindowsInfo::Version WindowsInfo::getReportedVersion(HINSTANCE ntdll) const +{ + // windows has been deprecating pretty much all the functions having to do + // with getting version information because apparently, people keep misusing + // them for feature detection + // + // there's still RtlGetVersion() though + + using RtlGetVersionType = NTSTATUS (NTAPI)(PRTL_OSVERSIONINFOW); + + auto* RtlGetVersion = reinterpret_cast( + GetProcAddress(ntdll, "RtlGetVersion")); + + if (!RtlGetVersion) { + qCritical() << "RtlGetVersion() not found in ntdll.dll"; + return {}; + } + + OSVERSIONINFOEX vi = {}; + vi.dwOSVersionInfoSize = sizeof(vi); + + // this apparently never fails + RtlGetVersion((RTL_OSVERSIONINFOW*)&vi); + + return {vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber}; +} + +WindowsInfo::Version WindowsInfo::getRealVersion(HINSTANCE ntdll) const +{ + // getting the actual windows version is more difficult because all the + // functions are lying when running in compatibility mode + // + // RtlGetNtVersionNumbers() is an undocumented function that seems to work + // fine, but it might not in the future + + using RtlGetNtVersionNumbersType = void (NTAPI)(DWORD*, DWORD*, DWORD*); + + auto* RtlGetNtVersionNumbers = reinterpret_cast( + GetProcAddress(ntdll, "RtlGetNtVersionNumbers")); + + if (!RtlGetNtVersionNumbers) { + qCritical() << "RtlGetNtVersionNumbers not found in ntdll.dll"; + return {}; + } + + DWORD major=0, minor=0, build=0; + RtlGetNtVersionNumbers(&major, &minor, &build); + + // for whatever reason, the build number has 0xf0000000 set + build = 0x0fffffff & build; + + return {major, minor, build}; +} + +WindowsInfo::Release WindowsInfo::getRelease() const +{ + // there are several interesting items in the registry, but most of them + // are undocumented, not always available, and localizable + // + // most of them are used to provide as much information as possible in case + // any of the other versions fail to work + + QSettings settings( + R"(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion)", + QSettings::NativeFormat); + + Release r; + + // buildlab seems to be an internal name from the build system + r.buildLab = settings.value("BuildLabEx", "").toString(); + if (r.buildLab.isEmpty()) { + r.buildLab = settings.value("BuildLab", "").toString(); + if (r.buildLab.isEmpty()) { + r.buildLab = settings.value("BuildBranch", "").toString(); + } + } + + // localized name of windows, such as "Windows 10 Pro" + r.productName = settings.value("ProductName", "").toString(); + + // release ID, such as 1803 + r.ID = settings.value("ReleaseId", "").toString(); + + // some other build number, shown in winver.exe + r.UBR = settings.value("UBR", 0).toUInt(); + + return r; +} + +std::optional WindowsInfo::getElevated() const +{ + HandlePtr token; + + { + HANDLE rawToken = 0; + + if (!OpenProcessToken(GetCurrentProcess( ), TOKEN_QUERY, &rawToken)) { + const auto e = GetLastError(); + + qCritical() + << "while trying to check if process is elevated, " + << "OpenProcessToken() failed: " << formatSystemMessageQ(e); + + return {}; + } + + token.reset(rawToken); + } + + TOKEN_ELEVATION e = {}; + DWORD size = sizeof(TOKEN_ELEVATION); + + if (!GetTokenInformation(token.get(), TokenElevation, &e, sizeof(e), &size)) { + const auto e = GetLastError(); + + qCritical() + << "while trying to check if process is elevated, " + << "GetTokenInformation() failed: " << formatSystemMessageQ(e); + + return {}; + } + + return (e.TokenIsElevated != 0); +} + +} // namespace diff --git a/src/envwindows.h b/src/envwindows.h new file mode 100644 index 00000000..c23f99f4 --- /dev/null +++ b/src/envwindows.h @@ -0,0 +1,106 @@ +#include +#include + +namespace env +{ + +// a variety of information on windows +// +class WindowsInfo +{ +public: + struct Version + { + DWORD major=0, minor=0, build=0; + + QString toString() const + { + return QString("%1.%2.%3").arg(major).arg(minor).arg(build); + } + + friend bool operator==(const Version& a, const Version& b) + { + return + a.major == b.major && + a.minor == b.minor && + a.build == b.build; + } + + friend bool operator!=(const Version& a, const Version& b) + { + return !(a == b); + } + }; + + struct Release + { + // the BuildLab entry from the registry, may be empty + QString buildLab; + + // product name such as "Windows 10 Pro", may not be in English, may be + // empty + QString productName; + + // release ID such as 1809, may be mepty + QString ID; + + // some sub-build number, undocumented, may be empty + DWORD UBR; + + Release() + : UBR(0) + { + } + }; + + + WindowsInfo(); + + // tries to guess whether this process is running in compatibility mode + // + bool compatibilityMode() const; + + // returns the Windows version, may not correspond to the actual version + // if the process is running in compatibility mode + // + const Version& reportedVersion() const; + + // tries to guess the real Windows version that's running, can be empty + // + const Version& realVersion() const; + + // various information about the current release + // + const Release& release() const; + + // whether this process is running as administrator, may be empty if the + // information is not available + std::optional isElevated() const; + + // returns a string with all the above information on one line + // + QString toString() const; + +private: + Version m_reported, m_real; + Release m_release; + std::optional m_elevated; + + // uses RtlGetVersion() to get the version number as reported by Windows + // + Version getReportedVersion(HINSTANCE ntdll) const; + + // uses RtlGetNtVersionNumbers() to get the real version number + // + Version getRealVersion(HINSTANCE ntdll) const; + + // gets various information from the registry + // + Release getRelease() const; + + // gets whether the process is elevated + // + std::optional getElevated() const; +}; + +} // namespace diff --git a/src/main.cpp b/src/main.cpp index 09da9408..5c5ce945 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,6 +47,8 @@ along with Mod Organizer. If not, see . #include "instancemanager.h" #include "moshortcut.h" #include "organizercore.h" +#include "env.h" +#include "envmodule.h" #include #include diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6c2ab389..87ee2c8e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -86,6 +86,7 @@ along with Mod Organizer. If not, see . #include #include "localsavegames.h" #include "listdialog.h" +#include "envshortcut.h" #include #include diff --git a/src/shared/util.cpp b/src/shared/util.cpp index 4ee4b766..07983e12 100644 --- a/src/shared/util.cpp +++ b/src/shared/util.cpp @@ -19,35 +19,9 @@ along with Mod Organizer. If not, see . #include "util.h" #include "windows_error.h" -#include "error_report.h" -#include "executableslist.h" -#include "instancemanager.h" -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -#pragma comment(lib, "Wbemuuid.lib") - -using namespace MOBase; -namespace fs = std::filesystem; - -namespace MOShared { +namespace MOShared +{ bool FileExists(const std::string &filename) { @@ -269,2097 +243,4 @@ MOBase::VersionInfo createVersionInfo() } } - -namespace env -{ - -struct HandleCloser -{ - using pointer = HANDLE; - - void operator()(HANDLE h) - { - if (h != INVALID_HANDLE_VALUE) { - ::CloseHandle(h); - } - } -}; - -using HandlePtr = std::unique_ptr; - - -struct LibraryFreer -{ - using pointer = HINSTANCE; - - void operator()(HINSTANCE h) - { - if (h != 0) { - ::FreeLibrary(h); - } - } -}; - -struct COMReleaser -{ - void operator()(IUnknown* p) - { - if (p) { - p->Release(); - } - } -}; - - -template -using COMPtr = std::unique_ptr; - - -class ShellLinkException -{ -public: - ShellLinkException(QString s) - : m_what(std::move(s)) - { - } - - const QString& what() const - { - return m_what; - } - -private: - QString m_what; -}; - -// just a wrapper around IShellLink operations that throws ShellLinkException -// on errors -// -class ShellLinkWrapper -{ -public: - ShellLinkWrapper() - { - m_link = createShellLink(); - m_file = createPersistFile(); - } - - void setPath(const QString& s) - { - if (s.isEmpty()) { - throw ShellLinkException("path cannot be empty"); - } - - const auto r = m_link->SetPath(s.toStdWString().c_str()); - throwOnFail(r, QString("failed to set target path '%1'").arg(s)); - } - - void setArguments(const QString& s) - { - const auto r = m_link->SetArguments(s.toStdWString().c_str()); - throwOnFail(r, QString("failed to set arguments '%1'").arg(s)); - } - - void setDescription(const QString& s) - { - if (s.isEmpty()) { - return; - } - - const auto r = m_link->SetDescription(s.toStdWString().c_str()); - throwOnFail(r, QString("failed to set description '%1'").arg(s)); - } - - void setIcon(const QString& file, int i) - { - if (file.isEmpty()) { - return; - } - - const auto r = m_link->SetIconLocation(file.toStdWString().c_str(), i); - throwOnFail(r, QString("failed to set icon '%1' @ %2").arg(file).arg(i)); - } - - void setWorkingDirectory(const QString& s) - { - if (s.isEmpty()) { - return; - } - - const auto r = m_link->SetWorkingDirectory(s.toStdWString().c_str()); - throwOnFail(r, QString("failed to set working directory '%1'").arg(s)); - } - - void save(const QString& path) - { - const auto r = m_file->Save(path.toStdWString().c_str(), TRUE); - throwOnFail(r, QString("failed to save link '%1'").arg(path)); - } - -private: - COMPtr m_link; - COMPtr m_file; - - void throwOnFail(HRESULT r, const QString& s) - { - if (FAILED(r)) { - throw ShellLinkException(QString("%1, %2") - .arg(s) - .arg(formatSystemMessageQ(r))); - } - } - - COMPtr createShellLink() - { - void* link = nullptr; - - const auto r = CoCreateInstance( - CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, - IID_IShellLink, &link); - - throwOnFail(r, "failed to create IShellLink instance"); - - if (!link) { - throw ShellLinkException("creating IShellLink worked, pointer is null"); - } - - return COMPtr(static_cast(link)); - } - - COMPtr createPersistFile() - { - void* file = nullptr; - - const auto r = m_link->QueryInterface(IID_IPersistFile, &file); - throwOnFail(r, "failed to get IPersistFile interface"); - - if (!file) { - throw ShellLinkException("querying IPersistFile worked, pointer is null"); - } - - return COMPtr(static_cast(file)); - } -}; - - -Console::Console() - : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr) -{ - // open a console - if (!AllocConsole()) { - // failed, ignore - } - - m_hasConsole = true; - - // redirect stdin, stdout and stderr to it - freopen_s(&m_in, "CONIN$", "r", stdin); - freopen_s(&m_out, "CONOUT$", "w", stdout); - freopen_s(&m_err, "CONOUT$", "w", stderr); -} - -Console::~Console() -{ - // close redirected handles and redirect standard stream to NUL in case - // they're used after this - - if (m_err) { - std::fclose(m_err); - freopen_s(&m_err, "NUL", "w", stderr); - } - - if (m_out) { - std::fclose(m_out); - freopen_s(&m_out, "NUL", "w", stdout); - } - - if (m_in) { - std::fclose(m_in); - freopen_s(&m_in, "NUL", "r", stdin); - } - - // close console - if (m_hasConsole) { - FreeConsole(); - } -} - - -Shortcut::Shortcut() - : m_iconIndex(0) -{ -} - -Shortcut::Shortcut(const Executable& exe) - : Shortcut() -{ - m_name = exe.title(); - m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath(); - - m_arguments = QString("\"moshortcut://%1:%2\"") - .arg(InstanceManager::instance().currentInstance()) - .arg(exe.title()); - - m_description = QString("Run %1 with ModOrganizer").arg(exe.title()); - - if (exe.usesOwnIcon()) { - m_icon = exe.binaryInfo().absoluteFilePath(); - } - - m_workingDirectory = qApp->applicationDirPath(); -} - -Shortcut& Shortcut::name(const QString& s) -{ - m_name = s; - return *this; -} - -Shortcut& Shortcut::target(const QString& s) -{ - m_target = s; - return *this; -} - -Shortcut& Shortcut::arguments(const QString& s) -{ - m_arguments = s; - return *this; -} - -Shortcut& Shortcut::description(const QString& s) -{ - m_description = s; - return *this; -} - -Shortcut& Shortcut::icon(const QString& s, int index) -{ - m_icon = s; - m_iconIndex = index; - return *this; -} - -Shortcut& Shortcut::workingDirectory(const QString& s) -{ - m_workingDirectory = s; - return *this; -} - -bool Shortcut::exists(Locations loc) const -{ - const auto path = shortcutPath(loc); - if (path.isEmpty()) { - return false; - } - - return QFileInfo(path).exists(); -} - -bool Shortcut::toggle(Locations loc) -{ - if (exists(loc)) { - return remove(loc); - } else { - return add(loc); - } -} - -bool Shortcut::add(Locations loc) -{ - debug() - << "adding shortcut to " << toString(loc) << ":\n" - << " . name: '" << m_name << "'\n" - << " . target: '" << m_target << "'\n" - << " . arguments: '" << m_arguments << "'\n" - << " . description: '" << m_description << "'\n" - << " . icon: '" << m_icon << "' @ " << m_iconIndex << "\n" - << " . working directory: '" << m_workingDirectory << "'"; - - if (m_target.isEmpty()) { - critical() << "target is empty"; - return false; - } - - const auto path = shortcutPath(loc); - if (path.isEmpty()) { - return false; - } - - debug() << "shorcut file will be saved at '" << path << "'"; - - try - { - ShellLinkWrapper link; - - link.setPath(m_target); - link.setArguments(m_arguments); - link.setDescription(m_description); - link.setIcon(m_icon, m_iconIndex); - link.setWorkingDirectory(m_workingDirectory); - - link.save(path); - - return true; - } - catch(ShellLinkException& e) - { - critical() << e.what() << "\nshortcut file was not saved"; - } - - return false; -} - -bool Shortcut::remove(Locations loc) -{ - debug() << "removing shortcut for '" << m_name << "' from " << toString(loc); - - const auto path = shortcutPath(loc); - if (path.isEmpty()) { - return false; - } - - debug() << "path to shortcut file is '" << path << "'"; - - if (!QFile::exists(path)) { - critical() << "can't remove '" << path << "', file not found"; - return false; - } - - if (!MOBase::shellDelete({path})) { - const auto e = ::GetLastError(); - - critical() - << "failed to remove '" << path << "', " - << formatSystemMessageQ(e); - - return false; - } - - return true; -} - -QString Shortcut::shortcutPath(Locations loc) const -{ - const auto dir = shortcutDirectory(loc); - if (dir.isEmpty()) { - return {}; - } - - const auto file = shortcutFilename(); - if (file.isEmpty()) { - return {}; - } - - return dir + QDir::separator() + file; -} - -QString Shortcut::shortcutDirectory(Locations loc) const -{ - QString dir; - - try - { - switch (loc) - { - case Desktop: - dir = MOBase::getDesktopDirectory(); - break; - - case StartMenu: - dir = MOBase::getStartMenuDirectory(); - break; - - case None: - default: - critical() << "bad location " << loc; - break; - } - } - catch(std::exception&) - { - } - - return QDir::toNativeSeparators(dir); -} - -QString Shortcut::shortcutFilename() const -{ - if (m_name.isEmpty()) { - critical() << "name is empty"; - return {}; - } - - return m_name + ".lnk"; -} - -QDebug Shortcut::debug() const -{ - return qDebug().noquote().nospace() << "system shortcut: "; -} - -QDebug Shortcut::critical() const -{ - return qCritical().noquote().nospace() << "system shortcut: "; -} - - -QString toString(Shortcut::Locations loc) -{ - switch (loc) - { - case Shortcut::None: - return "none"; - - case Shortcut::Desktop: - return "desktop"; - - case Shortcut::StartMenu: - return "start menu"; - - default: - return QString("? (%1)").arg(static_cast(loc)); - } -} - - - -class WMI -{ -public: - class failed {}; - - WMI(const std::string& ns) - { - try - { - createLocator(); - createService(ns); - setSecurity(); - } - catch(failed&) - { - } - } - - template - void query(const std::string& q, F&& f) - { - if (!m_locator || !m_service) { - return; - } - - auto enumerator = getEnumerator(q); - if (!enumerator) { - return; - } - - for (;;) - { - COMPtr object; - - { - IWbemClassObject* rawObject = nullptr; - ULONG count = 0; - auto ret = enumerator->Next(WBEM_INFINITE, 1, &rawObject, &count); - - if (count == 0 || !rawObject) { - break; - } - - if (FAILED(ret)) { - qCritical() - << "enumerator->next() failed, " << formatSystemMessageQ(ret); - break; - } - - object.reset(rawObject); - } - - f(object.get()); - } - } - -private: - COMPtr m_locator; - COMPtr m_service; - - void createLocator() - { - void* rawLocator = nullptr; - - const auto ret = CoCreateInstance( - CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, - IID_IWbemLocator, &rawLocator); - - if (FAILED(ret) || !rawLocator) { - qCritical() - << "CoCreateInstance for WbemLocator failed, " - << formatSystemMessageQ(ret); - - throw failed(); - } - - m_locator.reset(static_cast(rawLocator)); - } - - void createService(const std::string& ns) - { - IWbemServices* rawService = nullptr; - - const auto res = m_locator->ConnectServer( - _bstr_t(ns.c_str()), - nullptr, nullptr, nullptr, 0, nullptr, nullptr, - &rawService); - - if (FAILED(res) || !rawService) { - qCritical() - << "locator->ConnectServer() failed for namespace " - << "'" << QString::fromStdString(ns) << "', " - << formatSystemMessageQ(res); - - throw failed(); - } - - m_service.reset(rawService); - } - - void setSecurity() - { - auto ret = CoSetProxyBlanket( - m_service.get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, - RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE); - - if (FAILED(ret)) - { - qCritical() - << "CoSetProxyBlanket() failed, " << formatSystemMessageQ(ret); - - throw failed(); - } - } - - COMPtr getEnumerator( - const std::string& query) - { - IEnumWbemClassObject* rawEnumerator = NULL; - - auto ret = m_service->ExecQuery( - bstr_t("WQL"), - bstr_t(query.c_str()), - WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, - NULL, - &rawEnumerator); - - if (FAILED(ret) || !rawEnumerator) - { - qCritical() - << "query '" << QString::fromStdString(query) << "' failed, " - << formatSystemMessageQ(ret); - - return {}; - } - - return COMPtr(rawEnumerator); - } -}; - - -class DisplayEnumerator -{ -public: - DisplayEnumerator() - : m_GetDpiForMonitor(nullptr) - { - m_shcore.reset(LoadLibraryW(L"Shcore.dll")); - - if (m_shcore) { - // windows 8.1+ only - m_GetDpiForMonitor = reinterpret_cast( - GetProcAddress(m_shcore.get(), "GetDpiForMonitor")); - } - - // gets all monitors and the device they're running on - getDisplayDevices(); - } - - std::vector&& displays() && - { - return std::move(m_displays); - } - - const std::vector& displays() const & - { - return m_displays; - } - -private: - using GetDpiForMonitorFunction = - HRESULT WINAPI (HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); - - std::unique_ptr m_shcore; - GetDpiForMonitorFunction* m_GetDpiForMonitor; - std::vector m_displays; - - void getDisplayDevices() - { - // don't bother if it goes over 100 - for (int i=0; i<100; ++i) { - DISPLAY_DEVICEW device = {}; - device.cb = sizeof(device); - - if (!EnumDisplayDevicesW(nullptr, i, &device, 0)) { - // no more - break; - } - - // EnumDisplayDevices() seems to be returning a lot of devices that are - // not actually monitors, but those don't have the - // DISPLAY_DEVICE_ATTACHED_TO_DESKTOP bit set - if ((device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0) { - continue; - } - - m_displays.push_back(createDisplay(device)); - } - } - - Metrics::Display createDisplay(const DISPLAY_DEVICEW& device) - { - Metrics::Display d; - - d.adapter = QString::fromWCharArray(device.DeviceString); - d.monitor = QString::fromWCharArray(device.DeviceName); - d.primary = (device.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE); - - getDisplaySettings(device.DeviceName, d); - getDpi(d); - - return d; - } - - void getDisplaySettings(const wchar_t* monitorName, Metrics::Display& d) - { - DEVMODEW dm = {}; - dm.dmSize = sizeof(dm); - - if (!EnumDisplaySettingsW(monitorName, ENUM_CURRENT_SETTINGS, &dm)) { - log::error("EnumDisplaySettings() failed for '{}'", d.monitor); - return; - } - - // all these fields should be available - - if (dm.dmFields & DM_DISPLAYFREQUENCY) { - d.refreshRate = dm.dmDisplayFrequency; - } - - if (dm.dmFields & DM_PELSWIDTH) { - d.resX = dm.dmPelsWidth; - } - - if (dm.dmFields & DM_PELSHEIGHT) { - d.resY = dm.dmPelsHeight; - } - } - - void getDpi(Metrics::Display& d) - { - if (!m_GetDpiForMonitor) { - // this happens on windows 7, get the desktop dpi instead - getDesktopDpi(d); - return; - } - - // there's no way to get an HMONITOR from a device name, so all monitors - // will have to be enumerated and their name checked - HMONITOR hm = findMonitor(d.monitor); - if (!hm) { - log::error("can't get dpi for monitor '{}', not found", d.monitor); - return; - } - - UINT dpiX=0, dpiY=0; - const auto r = m_GetDpiForMonitor(hm, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); - - if (FAILED(r)) { - log::error( - "GetDpiForMonitor() failed for '{}', {}", - d.monitor, formatSystemMessageQ(r)); - - return; - } - - // dpiX and dpiY are always identical, as per the documentation - d.dpi = dpiX; - } - - void getDesktopDpi(Metrics::Display& d) - { - // desktop dc - HDC dc = GetDC(0); - - if (!dc) { - const auto e = GetLastError(); - log::error("can't get desktop DC, {}", formatSystemMessageQ(e)); - return; - } - - d.dpi = GetDeviceCaps(dc, LOGPIXELSX); - - ReleaseDC(0, dc); - } - - HMONITOR findMonitor(const QString& name) - { - // passed to the enumeration callback - struct Data - { - DisplayEnumerator* self; - QString name; - HMONITOR hm; - }; - - Data data = {this, name, 0}; - - // for each monitor - EnumDisplayMonitors(0, nullptr, [](HMONITOR hm, HDC, RECT*, LPARAM lp) { - auto& data = *reinterpret_cast(lp); - - MONITORINFOEX mi = {}; - mi.cbSize = sizeof(mi); - - // monitor info will include the name - if (!GetMonitorInfoW(hm, &mi)) { - const auto e = GetLastError(); - log::error( - "GetMonitorInfo() failed for '{}', {}", - data.name, formatSystemMessageQ(e)); - - // error for this monitor, but continue - return TRUE; - } - - if (QString::fromWCharArray(mi.szDevice) == data.name) { - // found, stop - data.hm = hm; - return FALSE; - } - - // not found, continue to the next monitor - return TRUE; - }, reinterpret_cast(&data)); - - return data.hm; - } -}; - - -Environment::Environment() -{ - m_modules = getLoadedModules(); - m_security = getSecurityProducts(); -} - -const std::vector& Environment::loadedModules() const -{ - return m_modules; -} - -const WindowsInfo& Environment::windowsInfo() const -{ - return m_windows; -} - -const std::vector& Environment::securityProducts() const -{ - return m_security; -} - -const Metrics& Environment::metrics() const -{ - return m_metrics; -} - -void Environment::dump() const -{ - log::debug("windows: {}", windowsInfo().toString()); - - if (windowsInfo().compatibilityMode()) { - log::warn("MO seems to be running in compatibility mode"); - } - - log::debug("security products:"); - for (const auto& sp : securityProducts()) { - log::debug(" . {}", sp.toString()); - } - - log::debug("modules loaded in process:"); - for (const auto& m : loadedModules()) { - log::debug(" . {}", m.toString()); - } - - log::debug("displays:"); - for (const auto& d : m_metrics.displays()) { - log::debug(" . {}", d.toString()); - } -} - -std::vector Environment::getLoadedModules() const -{ - HandlePtr snapshot(CreateToolhelp32Snapshot( - TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId())); - - if (snapshot.get() == INVALID_HANDLE_VALUE) - { - const auto e = GetLastError(); - - qCritical().nospace().noquote() - << "CreateToolhelp32Snapshot() failed, " - << formatSystemMessageQ(e); - - return {}; - } - - MODULEENTRY32 me = {}; - me.dwSize = sizeof(me); - - // first module, this shouldn't fail because there's at least the executable - if (!Module32First(snapshot.get(), &me)) - { - const auto e = GetLastError(); - - qCritical().nospace().noquote() - << "Module32First() failed, " << formatSystemMessageQ(e); - - return {}; - } - - std::vector v; - - for (;;) - { - const auto path = QString::fromWCharArray(me.szExePath); - if (!path.isEmpty()) { - v.push_back(Module(path, me.modBaseSize)); - } - - // next module - if (!Module32Next(snapshot.get(), &me)) { - const auto e = GetLastError(); - - // no more modules is not an error - if (e != ERROR_NO_MORE_FILES) { - qCritical().nospace().noquote() - << "Module32Next() failed, " << formatSystemMessageQ(e); - } - - break; - } - } - - // sorting by display name - std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) { - return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0); - }); - - return v; -} - -std::vector Environment::getSecurityProducts() const -{ - std::vector v; - - { - auto fromWMI = getSecurityProductsFromWMI(); - v.insert( - v.end(), - std::make_move_iterator(fromWMI.begin()), - std::make_move_iterator(fromWMI.end())); - } - - if (auto p=getWindowsFirewall()) { - v.push_back(std::move(*p)); - } - - return v; -} - -std::vector Environment::getSecurityProductsFromWMI() const -{ - // some products may be present in multiple queries, such as a product marked - // as both antivirus and antispyware, but they'll have the same GUID, so use - // that to avoid duplicating entries - std::map map; - - auto handleProduct = [&](auto* o) { - VARIANT prop; - - // display name - auto ret = o->Get(L"displayName", 0, &prop, 0, 0); - if (FAILED(ret)) { - qCritical() - << "failed to get displayName, " - << formatSystemMessageQ(ret); - - return; - } - - if (prop.vt != VT_BSTR) { - qCritical() << "displayName is a " << prop.vt << ", not a bstr"; - return; - } - - const std::wstring name = prop.bstrVal; - VariantClear(&prop); - - // product state - ret = o->Get(L"productState", 0, &prop, 0, 0); - if (FAILED(ret)) { - qCritical() - << "failed to get productState, " - << formatSystemMessageQ(ret); - - return; - } - - if (prop.vt != VT_UI4 && prop.vt != VT_I4) { - qCritical() << "productState is a " << prop.vt << ", is not a VT_UI4"; - return; - } - - DWORD state = 0; - if (prop.vt == VT_I4) { - state = prop.lVal; - } else { - state = prop.ulVal; - } - - VariantClear(&prop); - - // guid - ret = o->Get(L"instanceGuid", 0, &prop, 0, 0); - if (FAILED(ret)) { - qCritical() - << "failed to get instanceGuid, " - << formatSystemMessageQ(ret); - - return; - } - - if (prop.vt != VT_BSTR) { - qCritical() << "instanceGuid is a " << prop.vt << ", is not a bstr"; - return; - } - - const QUuid guid(QString::fromWCharArray(prop.bstrVal)); - VariantClear(&prop); - - const auto provider = static_cast((state >> 16) & 0xff); - const auto scanner = (state >> 8) & 0xff; - const auto definitions = state & 0xff; - - const bool active = ((scanner & 0x10) != 0); - const bool upToDate = (definitions == 0); - - map.insert({ - guid, - {guid, QString::fromStdWString(name), provider, active, upToDate}}); - }; - - { - WMI wmi("root\\SecurityCenter2"); - wmi.query("select * from AntivirusProduct", handleProduct); - wmi.query("select * from FirewallProduct", handleProduct); - wmi.query("select * from AntiSpywareProduct", handleProduct); - } - - { - WMI wmi("root\\SecurityCenter"); - wmi.query("select * from AntivirusProduct", handleProduct); - wmi.query("select * from FirewallProduct", handleProduct); - wmi.query("select * from AntiSpywareProduct", handleProduct); - } - - std::vector v; - - for (auto&& p : map) { - v.push_back(p.second); - } - - return v; -} - -std::optional Environment::getWindowsFirewall() const -{ - HRESULT hr = 0; - - COMPtr policy; - - { - void* rawPolicy = nullptr; - - hr = CoCreateInstance( - __uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER, - __uuidof(INetFwPolicy2), &rawPolicy); - - if (FAILED(hr) || !rawPolicy) { - qCritical() - << "CoCreateInstance for NetFwPolicy2 failed, " - << formatSystemMessageQ(hr); - - return {}; - } - - policy.reset(static_cast(rawPolicy)); - } - - VARIANT_BOOL enabledVariant; - - if (policy) { - hr = policy->get_FirewallEnabled(NET_FW_PROFILE2_PUBLIC, &enabledVariant); - if (FAILED(hr)) - { - qCritical() - << "get_FirewallEnabled failed, " - << formatSystemMessageQ(hr); - - return {}; - } - } - - const auto enabled = (enabledVariant != VARIANT_FALSE); - if (!enabled) { - return {}; - } - - return SecurityProduct( - {}, "Windows Firewall", WSC_SECURITY_PROVIDER_FIREWALL, true, true); -} - - -Metrics::Metrics() -{ - m_displays = DisplayEnumerator().displays(); -} - -const std::vector& Metrics::displays() const -{ - return m_displays; -} - -QString Metrics::Display::toString() const -{ - return QString("%1*%2 %3hz dpi=%4 on %5%6") - .arg(resX) - .arg(resY) - .arg(refreshRate) - .arg(dpi) - .arg(adapter) - .arg(primary ? " (primary)" : ""); -} - - -Module::Module(QString path, std::size_t fileSize) - : m_path(std::move(path)), m_fileSize(fileSize) -{ - const auto fi = getFileInfo(); - - m_version = getVersion(fi.ffi); - m_timestamp = getTimestamp(fi.ffi); - m_versionString = fi.fileDescription; - m_md5 = getMD5(); -} - -const QString& Module::path() const -{ - return m_path; -} - -QString Module::displayPath() const -{ - return QDir::fromNativeSeparators(m_path.toLower()); -} - -std::size_t Module::fileSize() const -{ - return m_fileSize; -} - -const QString& Module::version() const -{ - return m_version; -} - -const QString& Module::versionString() const -{ - return m_versionString; -} - -const QDateTime& Module::timestamp() const -{ - return m_timestamp; -} - -const QString& Module::md5() const -{ - return m_md5; -} - -QString Module::timestampString() const -{ - if (!m_timestamp.isValid()) { - return "(no timestamp)"; - } - - return m_timestamp.toString(Qt::DateFormat::ISODate); -} - -QString Module::toString() const -{ - QStringList sl; - - // file size - sl.push_back(displayPath()); - sl.push_back(QString("%1 B").arg(m_fileSize)); - - // version - if (m_version.isEmpty() && m_versionString.isEmpty()) { - sl.push_back("(no version)"); - } else { - if (!m_version.isEmpty()) { - sl.push_back(m_version); - } - - if (!m_versionString.isEmpty() && m_versionString != m_version) { - sl.push_back(versionString()); - } - } - - // timestamp - if (m_timestamp.isValid()) { - sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate)); - } else { - sl.push_back("(no timestamp)"); - } - - // md5 - if (!m_md5.isEmpty()) { - sl.push_back(m_md5); - } - - return sl.join(", "); -} - -Module::FileInfo Module::getFileInfo() const -{ - const auto wspath = m_path.toStdWString(); - - // getting version info size - DWORD dummy = 0; - const DWORD size = GetFileVersionInfoSizeW(wspath.c_str(), &dummy); - - if (size == 0) { - const auto e = GetLastError(); - - if (e == ERROR_RESOURCE_TYPE_NOT_FOUND) { - // not an error, no version information built into that module - return {}; - } - - qCritical().nospace().noquote() - << "GetFileVersionInfoSizeW() failed on '" << m_path << "', " - << formatSystemMessageQ(e); - - return {}; - } - - // getting version info - auto buffer = std::make_unique(size); - - if (!GetFileVersionInfoW(wspath.c_str(), 0, size, buffer.get())) { - const auto e = GetLastError(); - - qCritical().nospace().noquote() - << "GetFileVersionInfoW() failed on '" << m_path << "', " - << formatSystemMessageQ(e); - - return {}; - } - - // the version info has two major parts: a fixed version and a localizable - // set of strings - - FileInfo fi; - fi.ffi = getFixedFileInfo(buffer.get()); - fi.fileDescription = getFileDescription(buffer.get()); - - return fi; -} - -VS_FIXEDFILEINFO Module::getFixedFileInfo(std::byte* buffer) const -{ - void* valuePointer = nullptr; - unsigned int valueSize = 0; - - // the fixed version info is in the root - const auto ret = VerQueryValueW(buffer, L"\\", &valuePointer, &valueSize); - - if (!ret || !valuePointer || valueSize == 0) { - // not an error, no fixed file info - return {}; - } - - const auto* fi = static_cast(valuePointer); - - // signature is always 0xfeef04bd - if (fi->dwSignature != 0xfeef04bd) { - qCritical().nospace().noquote() - << "bad file info signature 0x" << hex << fi->dwSignature << " for " - << "'" << m_path << "'"; - - return {}; - } - - return *fi; -} - -QString Module::getFileDescription(std::byte* buffer) const -{ - struct LANGANDCODEPAGE - { - WORD wLanguage; - WORD wCodePage; - }; - - void* valuePointer = nullptr; - unsigned int valueSize = 0; - - // getting list of available languages - auto ret = VerQueryValueW( - buffer, L"\\VarFileInfo\\Translation", &valuePointer, &valueSize); - - if (!ret || !valuePointer || valueSize == 0) { - qCritical().nospace().noquote() - << "VerQueryValueW() for translations failed on '" << m_path << "'"; - - return {}; - } - - // number of languages - const auto count = valueSize / sizeof(LANGANDCODEPAGE); - if (count == 0) { - return {}; - } - - // using the first language in the list to get FileVersion - const auto* lcp = static_cast(valuePointer); - - const auto subBlock = QString("\\StringFileInfo\\%1%2\\FileVersion") - .arg(lcp->wLanguage, 4, 16, QChar('0')) - .arg(lcp->wCodePage, 4, 16, QChar('0')); - - ret = VerQueryValueW( - buffer, subBlock.toStdWString().c_str(), &valuePointer, &valueSize); - - if (!ret || !valuePointer || valueSize == 0) { - // not an error, no file version - return {}; - } - - // valueSize includes the null terminator - return QString::fromWCharArray( - static_cast(valuePointer), valueSize - 1); -} - -QString Module::getVersion(const VS_FIXEDFILEINFO& fi) const -{ - if (fi.dwSignature == 0) { - return {}; - } - - const DWORD major = (fi.dwFileVersionMS >> 16 ) & 0xffff; - const DWORD minor = (fi.dwFileVersionMS >> 0 ) & 0xffff; - const DWORD maintenance = (fi.dwFileVersionLS >> 16 ) & 0xffff; - const DWORD build = (fi.dwFileVersionLS >> 0 ) & 0xffff; - - if (major == 0 && minor == 0 && maintenance == 0 && build == 0) { - return {}; - } - - return QString("%1.%2.%3.%4") - .arg(major).arg(minor).arg(maintenance).arg(build); -} - -QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const -{ - FILETIME ft = {}; - - if (fi.dwSignature == 0 || (fi.dwFileDateMS == 0 && fi.dwFileDateLS == 0)) { - // if the file info is invalid or doesn't have a date, use the creation - // time on the file - - // opening the file - HandlePtr h(CreateFileW( - m_path.toStdWString().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)); - - if (h.get() == INVALID_HANDLE_VALUE) { - const auto e = GetLastError(); - - qCritical().nospace().noquote() - << "can't open file '" << m_path << "' for timestamp, " - << formatSystemMessageQ(e); - - return {}; - } - - // getting the file time - if (!GetFileTime(h.get(), &ft, nullptr, nullptr)) { - const auto e = GetLastError(); - qCritical().nospace().noquote() - << "can't get file time for '" << m_path << "', " - << formatSystemMessageQ(e); - - return {}; - } - } else { - // use the time from the file info - ft.dwHighDateTime = fi.dwFileDateMS; - ft.dwLowDateTime = fi.dwFileDateLS; - } - - - // converting to SYSTEMTIME - SYSTEMTIME utc = {}; - - if (!FileTimeToSystemTime(&ft, &utc)) { - qCritical().nospace().noquote() - << "FileTimeToSystemTime() failed on timestamp " - << "high=0x" << hex << ft.dwHighDateTime << " " - << "low=0x" << hex << ft.dwLowDateTime << " for " - << "'" << m_path << "'"; - - return {}; - } - - return QDateTime( - QDate(utc.wYear, utc.wMonth, utc.wDay), - QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds)); -} - -QString Module::getMD5() const -{ - if (m_path.contains("\\windows\\", Qt::CaseInsensitive)) { - // don't calculate md5 for system files, it's not really relevant and - // it takes a while - return {}; - } - - // opening the file - QFile f(m_path); - - if (!f.open(QFile::ReadOnly)) { - qCritical().nospace().noquote() - << "failed to open file '" << m_path << "' for md5"; - - return {}; - } - - // hashing - QCryptographicHash hash(QCryptographicHash::Md5); - if (!hash.addData(&f)) { - qCritical().nospace().noquote() - << "failed to calculate md5 for '" << m_path << "'"; - - return {}; - } - - return hash.result().toHex(); -} - - -WindowsInfo::WindowsInfo() -{ - // loading ntdll.dll, the functions will be found with GetProcAddress() - std::unique_ptr ntdll(LoadLibraryW(L"ntdll.dll")); - - if (!ntdll) { - qCritical() << "failed to load ntdll.dll while getting version"; - return; - } else { - m_reported = getReportedVersion(ntdll.get()); - m_real = getRealVersion(ntdll.get()); - } - - m_release = getRelease(); - m_elevated = getElevated(); -} - -bool WindowsInfo::compatibilityMode() const -{ - if (m_real == Version()) { - // don't know the real version, can't guess compatibility mode - return false; - } - - return (m_real != m_reported); -} - -const WindowsInfo::Version& WindowsInfo::reportedVersion() const -{ - return m_reported; -} - -const WindowsInfo::Version& WindowsInfo::realVersion() const -{ - return m_real; -} - -const WindowsInfo::Release& WindowsInfo::release() const -{ - return m_release; -} - -std::optional WindowsInfo::isElevated() const -{ - return m_elevated; -} - -QString WindowsInfo::toString() const -{ - QStringList sl; - - const QString reported = m_reported.toString(); - const QString real = m_real.toString(); - - // version - sl.push_back("version " + reported); - - // real version if different - if (compatibilityMode()) { - sl.push_back("real version " + real); - } - - // build.UBR, such as 17763.557 - if (m_release.UBR != 0) { - DWORD build = 0; - - if (compatibilityMode()) { - build = m_real.build; - } else { - build = m_reported.build; - } - - sl.push_back(QString("%1.%2").arg(build).arg(m_release.UBR)); - } - - // release ID - if (!m_release.ID.isEmpty()) { - sl.push_back("release " + m_release.ID); - } - - // buildlab string - if (!m_release.buildLab.isEmpty()) { - sl.push_back(m_release.buildLab); - } - - // product name - if (!m_release.productName.isEmpty()) { - sl.push_back(m_release.productName); - } - - // elevated - QString elevated = "?"; - if (m_elevated.has_value()) { - elevated = (*m_elevated ? "yes" : "no"); - } - - sl.push_back("elevated: " + elevated); - - return sl.join(", "); -} - -WindowsInfo::Version WindowsInfo::getReportedVersion(HINSTANCE ntdll) const -{ - // windows has been deprecating pretty much all the functions having to do - // with getting version information because apparently, people keep misusing - // them for feature detection - // - // there's still RtlGetVersion() though - - using RtlGetVersionType = NTSTATUS (NTAPI)(PRTL_OSVERSIONINFOW); - - auto* RtlGetVersion = reinterpret_cast( - GetProcAddress(ntdll, "RtlGetVersion")); - - if (!RtlGetVersion) { - qCritical() << "RtlGetVersion() not found in ntdll.dll"; - return {}; - } - - OSVERSIONINFOEX vi = {}; - vi.dwOSVersionInfoSize = sizeof(vi); - - // this apparently never fails - RtlGetVersion((RTL_OSVERSIONINFOW*)&vi); - - return {vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber}; -} - -WindowsInfo::Version WindowsInfo::getRealVersion(HINSTANCE ntdll) const -{ - // getting the actual windows version is more difficult because all the - // functions are lying when running in compatibility mode - // - // RtlGetNtVersionNumbers() is an undocumented function that seems to work - // fine, but it might not in the future - - using RtlGetNtVersionNumbersType = void (NTAPI)(DWORD*, DWORD*, DWORD*); - - auto* RtlGetNtVersionNumbers = reinterpret_cast( - GetProcAddress(ntdll, "RtlGetNtVersionNumbers")); - - if (!RtlGetNtVersionNumbers) { - qCritical() << "RtlGetNtVersionNumbers not found in ntdll.dll"; - return {}; - } - - DWORD major=0, minor=0, build=0; - RtlGetNtVersionNumbers(&major, &minor, &build); - - // for whatever reason, the build number has 0xf0000000 set - build = 0x0fffffff & build; - - return {major, minor, build}; -} - -WindowsInfo::Release WindowsInfo::getRelease() const -{ - // there are several interesting items in the registry, but most of them - // are undocumented, not always available, and localizable - // - // most of them are used to provide as much information as possible in case - // any of the other versions fail to work - - QSettings settings( - R"(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion)", - QSettings::NativeFormat); - - Release r; - - // buildlab seems to be an internal name from the build system - r.buildLab = settings.value("BuildLabEx", "").toString(); - if (r.buildLab.isEmpty()) { - r.buildLab = settings.value("BuildLab", "").toString(); - if (r.buildLab.isEmpty()) { - r.buildLab = settings.value("BuildBranch", "").toString(); - } - } - - // localized name of windows, such as "Windows 10 Pro" - r.productName = settings.value("ProductName", "").toString(); - - // release ID, such as 1803 - r.ID = settings.value("ReleaseId", "").toString(); - - // some other build number, shown in winver.exe - r.UBR = settings.value("UBR", 0).toUInt(); - - return r; -} - -std::optional WindowsInfo::getElevated() const -{ - HandlePtr token; - - { - HANDLE rawToken = 0; - - if (!OpenProcessToken(GetCurrentProcess( ), TOKEN_QUERY, &rawToken)) { - const auto e = GetLastError(); - - qCritical() - << "while trying to check if process is elevated, " - << "OpenProcessToken() failed: " << formatSystemMessageQ(e); - - return {}; - } - - token.reset(rawToken); - } - - TOKEN_ELEVATION e = {}; - DWORD size = sizeof(TOKEN_ELEVATION); - - if (!GetTokenInformation(token.get(), TokenElevation, &e, sizeof(e), &size)) { - const auto e = GetLastError(); - - qCritical() - << "while trying to check if process is elevated, " - << "GetTokenInformation() failed: " << formatSystemMessageQ(e); - - return {}; - } - - return (e.TokenIsElevated != 0); -} - - -SecurityProduct::SecurityProduct( - QUuid guid, QString name, int provider, - bool active, bool upToDate) : - m_guid(std::move(guid)), m_name(std::move(name)), m_provider(provider), - m_active(active), m_upToDate(upToDate) -{ -} - -const QString& SecurityProduct::name() const -{ - return m_name; -} - -int SecurityProduct::provider() const -{ - return m_provider; -} - -bool SecurityProduct::active() const -{ - return m_active; -} - -bool SecurityProduct::upToDate() const -{ - return m_upToDate; -} - -QString SecurityProduct::toString() const -{ - QString s; - - s += m_name + " (" + providerToString() + ")"; - - if (!m_active) { - s += ", inactive"; - } - - if (!m_upToDate) { - s += ", definitions outdated"; - } - - if (!m_guid.isNull()) { - s += ", " + m_guid.toString(QUuid::QUuid::WithoutBraces); - } - - return s; -} - -QString SecurityProduct::providerToString() const -{ - QStringList ps; - - if (m_provider & WSC_SECURITY_PROVIDER_FIREWALL) { - ps.push_back("firewall"); - } - - if (m_provider & WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS) { - ps.push_back("autoupdate"); - } - - if (m_provider & WSC_SECURITY_PROVIDER_ANTIVIRUS) { - ps.push_back("antivirus"); - } - - if (m_provider & WSC_SECURITY_PROVIDER_ANTISPYWARE) { - ps.push_back("antispyware"); - } - - if (m_provider & WSC_SECURITY_PROVIDER_INTERNET_SETTINGS) { - ps.push_back("settings"); - } - - if (m_provider & WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL) { - ps.push_back("uac"); - } - - if (m_provider & WSC_SECURITY_PROVIDER_SERVICE) { - ps.push_back("service"); - } - - if (ps.empty()) { - return "doesn't provider anything"; - } - - return ps.join("|"); -} - - -struct Process -{ - std::wstring filename; - DWORD pid; - - Process(std::wstring f, DWORD id) - : filename(std::move(f)), pid(id) - { - } -}; - -// returns the filename of the given process or the current one -// -std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE) -{ - // double the buffer size 10 times - const int MaxTries = 10; - - DWORD bufferSize = MAX_PATH; - - for (int tries=0; tries(bufferSize + 1); - std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0); - - DWORD writtenSize = 0; - - if (process == INVALID_HANDLE_VALUE) { - // query this process - writtenSize = GetModuleFileNameW(0, buffer.get(), bufferSize); - } else { - // query another process - writtenSize = GetModuleBaseNameW(process, 0, buffer.get(), bufferSize); - } - - if (writtenSize == 0) { - // hard failure - const auto e = GetLastError(); - std::wcerr << formatSystemMessage(e) << L"\n"; - break; - } else if (writtenSize >= bufferSize) { - // buffer is too small, try again - bufferSize *= 2; - } else { - // if GetModuleFileName() works, `writtenSize` does not include the null - // terminator - const std::wstring s(buffer.get(), writtenSize); - const fs::path path(s); - - return path.filename().native(); - } - } - - // something failed or the path is way too long to make sense - - std::wstring what; - if (process == INVALID_HANDLE_VALUE) { - what = L"the current process"; - } else { - what = L"pid " + std::to_wstring(reinterpret_cast(process)); - } - - std::wcerr << L"failed to get filename for " << what << L"\n"; - return {}; -} - -std::vector runningProcessesIds() -{ - // double the buffer size 10 times - const int MaxTries = 10; - - // initial size of 300 processes, unlikely to be more than that - std::size_t size = 300; - - for (int tries=0; tries(size); - std::fill(ids.get(), ids.get() + size, 0); - - DWORD bytesGiven = static_cast(size * sizeof(ids[0])); - DWORD bytesWritten = 0; - - if (!EnumProcesses(ids.get(), bytesGiven, &bytesWritten)) - { - const auto e = GetLastError(); - - std::wcerr - << L"failed to enumerate processes, " - << formatSystemMessage(e) << L"\n"; - - return {}; - } - - if (bytesWritten == bytesGiven) { - // no way to distinguish between an exact fit and not enough space, - // just try again - size *= 2; - continue; - } - - const auto count = bytesWritten / sizeof(ids[0]); - return std::vector(ids.get(), ids.get() + count); - } - - std::cerr << L"too many processes to enumerate"; - return {}; -} - -std::vector runningProcesses() -{ - const auto pids = runningProcessesIds(); - std::vector v; - - for (const auto& pid : pids) { - if (pid == 0) { - // the idle process has pid 0 and seems to be picked up by EnumProcesses() - continue; - } - - HandlePtr h(OpenProcess( - PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid)); - - if (!h) { - const auto e = GetLastError(); - - if (e != ERROR_ACCESS_DENIED) { - // don't log access denied, will happen a lot for system processes, even - // when elevated - std::wcerr - << L"failed to open process " << pid << L", " - << formatSystemMessage(e) << L"\n"; - } - - continue; - } - - auto filename = processFilename(h.get()); - if (!filename.empty()) { - v.emplace_back(std::move(filename), pid); - } - } - - return v; -} - -DWORD findOtherPid() -{ - const std::wstring defaultName = L"ModOrganizer.exe"; - - std::wclog << L"looking for the other process...\n"; - - // used to skip the current process below - const auto thisPid = GetCurrentProcessId(); - std::wclog << L"this process id is " << thisPid << L"\n"; - - // getting the filename for this process, assumes the other process has the - // smae one - auto filename = processFilename(); - if (filename.empty()) { - std::wcerr - << L"can't get current process filename, defaulting to " - << defaultName << L"\n"; - - filename = defaultName; - } else { - std::wclog << L"this process filename is " << filename << L"\n"; - } - - // getting all running processes - const auto processes = runningProcesses(); - std::wclog << L"there are " << processes.size() << L" processes running\n"; - - // going through processes, trying to find one with the same name and a - // different pid than this process has - for (const auto& p : processes) { - if (p.filename == filename) { - if (p.pid != thisPid) { - return p.pid; - } - } - } - - std::wclog - << L"no process with this filename\n" - << L"MO may not be running, or it may be running as administrator\n" - << L"you can try running this again as administrator\n"; - - return 0; -} - -std::wstring tempDir() -{ - const DWORD bufferSize = MAX_PATH + 1; - wchar_t buffer[bufferSize + 1] = {}; - - const auto written = GetTempPathW(bufferSize, buffer); - if (written == 0) { - const auto e = GetLastError(); - - std::wcerr - << L"failed to get temp path, " << formatSystemMessage(e) << L"\n"; - - return {}; - } - - // `written` does not include the null terminator - return std::wstring(buffer, buffer + written); -} - -HandlePtr tempFile(const std::wstring dir) -{ - // maximum tries of incrementing the counter - const int MaxTries = 100; - - // UTC time and date will be in the filename - const auto now = std::time(0); - const auto tm = std::gmtime(&now); - - // "ModOrganizer-YYYYMMDDThhmmss.dmp", with a possible "-i" appended, where - // i can go until MaxTries - std::wostringstream oss; - oss - << L"ModOrganizer-" - << std::setw(4) << (1900 + tm->tm_year) - << std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1) - << std::setw(2) << std::setfill(L'0') << tm->tm_mday << "T" - << std::setw(2) << std::setfill(L'0') << tm->tm_hour - << std::setw(2) << std::setfill(L'0') << tm->tm_min - << std::setw(2) << std::setfill(L'0') << tm->tm_sec; - - const std::wstring prefix = oss.str(); - const std::wstring ext = L".dmp"; - - // first path to try, without counter in it - std::wstring path = dir + L"\\" + prefix + ext; - - for (int i=0; i. #ifndef UTIL_H #define UTIL_H - #include -#include - -#define WIN32_LEAN_AND_MEAN -#include - #include -#include class Executable; @@ -51,442 +44,6 @@ std::wstring ToLower(const std::wstring &text); bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs); - -namespace env -{ - -class Console -{ -public: - Console(); - ~Console(); - -private: - bool m_hasConsole; - FILE* m_in; - FILE* m_out; - FILE* m_err; -}; - - -// an application shortcut that can be either on the desktop or the start menu -// -class Shortcut -{ -public: - // location of a shortcut - // - enum Locations - { - None = 0, - - // on the desktop - Desktop, - - // in the start menu - StartMenu - }; - - - // empty shortcut - // - Shortcut(); - - // shortcut from an executable - // - explicit Shortcut(const Executable& exe); - - // sets the name of the shortcut, shown on icons and start menu entries - // - Shortcut& name(const QString& s); - - // the program to start - // - Shortcut& target(const QString& s); - - // arguments to pass - // - Shortcut& arguments(const QString& s); - - // shows in the status bar of explorer, for example - // - Shortcut& description(const QString& s); - - // path to a binary that contains the icon and its index - // - Shortcut& icon(const QString& s, int index=0); - - // "start in" option for this shortcut - // - Shortcut& workingDirectory(const QString& s); - - - // returns whether this shortcut already exists at the given location; this - // does not check whether the shortcut parameters are different, it merely if - // the .lnk file exists - // - bool exists(Locations loc) const; - - // calls remove() if exists(), or add() - // - bool toggle(Locations loc); - - // adds the shortcut to the given location - // - bool add(Locations loc); - - // removes the shortcut from the given location - // - bool remove(Locations loc); - -private: - QString m_name; - QString m_target; - QString m_arguments; - QString m_description; - QString m_icon; - int m_iconIndex; - QString m_workingDirectory; - - // returns a qCritical() logger with a prefix already logged - // - QDebug critical() const; - - // returns a qDebug() logger with a prefix already logged - // - QDebug debug() const; - - - // returns the path where the shortcut file should be saved - // - QString shortcutPath(Locations loc) const; - - // returns the directory where the shortcut file should be saved - // - QString shortcutDirectory(Locations loc) const; - - // returns the filename of the shortcut file that should be used when saving - // - QString shortcutFilename() const; -}; - - -// returns a string representation of the given location -// -QString toString(Shortcut::Locations loc); - - -// represents one module -// -class Module -{ -public: - explicit Module(QString path, std::size_t fileSize); - - // returns the module's path - // - const QString& path() const; - - // returns the module's path in lowercase and using forward slashes - // - QString displayPath() const; - - // returns the size in bytes, may be 0 - // - std::size_t fileSize() const; - - // returns the x.x.x.x version embedded from the version info, may be empty - // - const QString& version() const; - - // returns the FileVersion entry from the resource file, returns - // "(no version)" if not available - // - const QString& versionString() const; - - // returns the build date from the version info, or the creation time of the - // file on the filesystem, may be empty - // - const QDateTime& timestamp() const; - - // returns the md5 of the file, may be empty for system files - // - const QString& md5() const; - - // converts timestamp() to a string for display, returns "(no timestamp)" if - // not available - // - QString timestampString() const; - - // returns a string with all the above information on one line - // - QString toString() const; - -private: - // contains the information from the version resource - // - struct FileInfo - { - VS_FIXEDFILEINFO ffi; - QString fileDescription; - }; - - QString m_path; - std::size_t m_fileSize; - QString m_version; - QDateTime m_timestamp; - QString m_versionString; - QString m_md5; - - // returns information from the version resource - // - FileInfo getFileInfo() const; - - // uses VS_FIXEDFILEINFO to build the version string - // - QString getVersion(const VS_FIXEDFILEINFO& fi) const; - - // uses the file date from VS_FIXEDFILEINFO if available, or gets the - // creation date on the file - // - QDateTime getTimestamp(const VS_FIXEDFILEINFO& fi) const; - - // returns the md5 hash unless the path contains "\windows\" - // - QString getMD5() const; - - // gets VS_FIXEDFILEINFO from the file version info buffer - // - VS_FIXEDFILEINFO getFixedFileInfo(std::byte* buffer) const; - - // gets FileVersion from the file version info buffer - // - QString getFileDescription(std::byte* buffer) const; -}; - - -// a variety of information on windows -// -class WindowsInfo -{ -public: - struct Version - { - DWORD major=0, minor=0, build=0; - - QString toString() const - { - return QString("%1.%2.%3").arg(major).arg(minor).arg(build); - } - - friend bool operator==(const Version& a, const Version& b) - { - return - a.major == b.major && - a.minor == b.minor && - a.build == b.build; - } - - friend bool operator!=(const Version& a, const Version& b) - { - return !(a == b); - } - }; - - struct Release - { - // the BuildLab entry from the registry, may be empty - QString buildLab; - - // product name such as "Windows 10 Pro", may not be in English, may be - // empty - QString productName; - - // release ID such as 1809, may be mepty - QString ID; - - // some sub-build number, undocumented, may be empty - DWORD UBR; - - Release() - : UBR(0) - { - } - }; - - - WindowsInfo(); - - // tries to guess whether this process is running in compatibility mode - // - bool compatibilityMode() const; - - // returns the Windows version, may not correspond to the actual version - // if the process is running in compatibility mode - // - const Version& reportedVersion() const; - - // tries to guess the real Windows version that's running, can be empty - // - const Version& realVersion() const; - - // various information about the current release - // - const Release& release() const; - - // whether this process is running as administrator, may be empty if the - // information is not available - std::optional isElevated() const; - - // returns a string with all the above information on one line - // - QString toString() const; - -private: - Version m_reported, m_real; - Release m_release; - std::optional m_elevated; - - // uses RtlGetVersion() to get the version number as reported by Windows - // - Version getReportedVersion(HINSTANCE ntdll) const; - - // uses RtlGetNtVersionNumbers() to get the real version number - // - Version getRealVersion(HINSTANCE ntdll) const; - - // gets various information from the registry - // - Release getRelease() const; - - // gets whether the process is elevated - // - std::optional getElevated() const; -}; - - -// represents a security product, such as an antivirus or a firewall -// -class SecurityProduct -{ -public: - SecurityProduct( - QUuid guid, QString name, int provider, - bool active, bool upToDate); - - // display name of the product - // - const QString& name() const; - - // a bunch of _WSC_SECURITY_PROVIDER flags - // - int provider() const; - - // whether the product is active - // - bool active() const; - - // whether its definitions are up-to-date - // - bool upToDate() const; - - // string representation of the above - // - QString toString() const; - -private: - QUuid m_guid; - QString m_name; - int m_provider; - bool m_active; - bool m_upToDate; - - QString providerToString() const; -}; - - -class Metrics -{ -public: - struct Display - { - int resX=0, resY=0, dpi=0; - bool primary=false; - int refreshRate = 0; - QString monitor, adapter; - - QString toString() const; - }; - - Metrics(); - - const std::vector& displays() const; - -private: - std::vector m_displays; -}; - - -// represents the process's environment -// -class Environment -{ -public: - Environment(); - - // list of loaded modules in the current process - // - const std::vector& loadedModules() const; - - // information about the operating system - // - const WindowsInfo& windowsInfo() const; - - // information about the installed security products - // - const std::vector& securityProducts() const; - - // information about displays - // - const Metrics& metrics() const; - - // logs the environment - // - void dump() const; - -private: - std::vector m_modules; - WindowsInfo m_windows; - std::vector m_security; - Metrics m_metrics; - - std::vector getLoadedModules() const; - std::vector getSecurityProducts() const; - - std::vector getSecurityProductsFromWMI() const; - std::optional getWindowsFirewall() const; -}; - - -enum class CoreDumpTypes -{ - Mini = 1, - Data, - Full -}; - -// creates a minidump file for the given process -// -bool coredump(CoreDumpTypes type); - -// finds another process with the same name as this one and creates a minidump -// file for it -// -bool coredumpOther(CoreDumpTypes type); - -} // namespace env - - MOBase::VersionInfo createVersionInfo(); } // namespace MOShared -- cgit v1.3.1 From 6b5c9675ae1e8b343dcbc9192d43c63e482a30bd Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 08:43:46 -0400 Subject: moved SettingsTab out of Settings split general tab --- src/CMakeLists.txt | 3 + src/settings.cpp | 204 ++++--------------------------- src/settings.h | 65 ++++------ src/settingsdialog.cpp | 105 ---------------- src/settingsdialog.h | 63 +++------- src/settingsdialoggeneral.cpp | 274 ++++++++++++++++++++++++++++++++++++++++++ src/settingsdialoggeneral.h | 53 ++++++++ 7 files changed, 392 insertions(+), 375 deletions(-) create mode 100644 src/settingsdialoggeneral.cpp create mode 100644 src/settingsdialoggeneral.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9785dc3d..29c419b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,7 @@ SET(organizer_SRCS spawn.cpp singleinstance.cpp settingsdialog.cpp + settingsdialoggeneral.cpp settings.cpp selfupdater.cpp selectiondialog.cpp @@ -151,6 +152,7 @@ SET(organizer_HDRS spawn.h singleinstance.h settingsdialog.h + settingsdialoggeneral.h settings.h selfupdater.h selectiondialog.h @@ -431,6 +433,7 @@ set(profiles set(settings settings settingsdialog + settingsdialoggeneral ) set(utilities diff --git a/src/settings.cpp b/src/settings.cpp index 5ad066b2..0911b155 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -22,6 +22,7 @@ along with Mod Organizer. If not, see . #include "pluginsetting.h" #include "serverinfo.h" #include "settingsdialog.h" +#include "settingsdialoggeneral.h" #include "versioninfo.h" #include "appconfig.h" #include "organizercore.h" @@ -81,6 +82,23 @@ private: }; +SettingsTab::SettingsTab(Settings *m_parent, SettingsDialog &m_dialog) + : m_parent(m_parent) + , m_Settings(m_parent->settingsRef()) + , m_dialog(m_dialog) + , ui(m_dialog.ui) +{ +} + +SettingsTab::~SettingsTab() +{} + +QWidget* SettingsTab::parentWidget() +{ + return &m_dialog; +} + + Settings *Settings::s_Instance = nullptr; @@ -663,63 +681,9 @@ void Settings::writePluginBlacklist() m_Settings.endArray(); } -void Settings::addLanguages(QComboBox *languageBox) -{ - std::vector> languages; - - QDirIterator langIter(QCoreApplication::applicationDirPath() + "/translations", QDir::Files); - QString pattern = ToQString(AppConfig::translationPrefix()) + "_([a-z]{2,3}(_[A-Z]{2,2})?).qm"; - QRegExp exp(pattern); - while (langIter.hasNext()) { - langIter.next(); - QString file = langIter.fileName(); - if (exp.exactMatch(file)) { - QString languageCode = exp.cap(1); - QLocale locale(languageCode); - QString languageString = QString("%1 (%2)").arg(locale.nativeLanguageName()).arg(locale.nativeCountryName()); //QLocale::languageToString(locale.language()); - if (locale.language() == QLocale::Chinese) { - if (languageCode == "zh_TW") { - languageString = "Chinese (traditional)"; - } else { - languageString = "Chinese (simplified)"; - } - } - languages.push_back(std::make_pair(QString("%1").arg(languageString), exp.cap(1))); - //languageBox->addItem(QString("%1").arg(languageString), exp.cap(1)); - } - } - if (!languageBox->findText("English")) { - languages.push_back(std::make_pair(QString("English"), QString("en_US"))); - //languageBox->addItem("English", "en_US"); - } - std::sort(languages.begin(), languages.end()); - for (const auto &lang : languages) { - languageBox->addItem(lang.first, lang.second); - } -} - -void Settings::addStyles(QComboBox *styleBox) -{ - styleBox->addItem("None", ""); - styleBox->addItem("Fusion", "Fusion"); - - QDirIterator langIter(QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::stylesheetsPath()), QStringList("*.qss"), QDir::Files); - while (langIter.hasNext()) { - langIter.next(); - QString style = langIter.fileName(); - styleBox->addItem(style, style); - } -} - -void Settings::resetDialogs() -{ - QuestionBoxMemory::resetDialogs(); -} - void Settings::query(PluginContainer *pluginContainer, QWidget *parent) { SettingsDialog dialog(pluginContainer, this, parent); - connect(&dialog, SIGNAL(resetDialogs()), this, SLOT(resetDialogs())); std::vector> tabs; @@ -787,128 +751,6 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) } -Settings::SettingsTab::SettingsTab(Settings *m_parent, SettingsDialog &m_dialog) - : m_parent(m_parent) - , m_Settings(m_parent->m_Settings) - , m_dialog(m_dialog) -{ -} - -Settings::SettingsTab::~SettingsTab() -{} - -Settings::GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) - , m_languageBox(m_dialog.findChild("languageBox")) - , m_styleBox(m_dialog.findChild("styleBox")) - , m_compactBox(m_dialog.findChild("compactBox")) - , m_showMetaBox(m_dialog.findChild("showMetaBox")) - , m_usePrereleaseBox(m_dialog.findChild("usePrereleaseBox")) - , m_overwritingBtn(m_dialog.findChild("overwritingBtn")) - , m_overwrittenBtn(m_dialog.findChild("overwrittenBtn")) - , m_overwritingArchiveBtn(m_dialog.findChild("overwritingArchiveBtn")) - , m_overwrittenArchiveBtn(m_dialog.findChild("overwrittenArchiveBtn")) - , m_containsBtn(m_dialog.findChild("containsBtn")) - , m_containedBtn(m_dialog.findChild("containedBtn")) - , m_colorSeparatorsBox(m_dialog.findChild("colorSeparatorsBox")) -{ - // FIXME I think 'addLanguages' lives in here not in parent - m_parent->addLanguages(m_languageBox); - { - QString languageCode = m_parent->language(); - int currentID = m_languageBox->findData(languageCode); - // I made a mess. :( Most languages are stored with only the iso country - // code (2 characters like "de") but chinese - // with the exact language variant (zh_TW) so I have to search for both - // variants - if (currentID == -1) { - currentID = m_languageBox->findData(languageCode.mid(0, 2)); - } - if (currentID != -1) { - m_languageBox->setCurrentIndex(currentID); - } - } - - // FIXME I think addStyles lives in here not in parent - m_parent->addStyles(m_styleBox); - { - int currentID = m_styleBox->findData( - m_Settings.value("Settings/style", "").toString()); - if (currentID != -1) { - m_styleBox->setCurrentIndex(currentID); - } - } - /* verision using palette only works with fusion theme for some stupid reason... - m_overwritingBtn->setAutoFillBackground(true); - m_overwrittenBtn->setAutoFillBackground(true); - m_containsBtn->setAutoFillBackground(true); - m_containedBtn->setAutoFillBackground(true); - m_overwritingBtn->setPalette(QPalette(m_parent->modlistOverwritingLooseColor())); - m_overwrittenBtn->setPalette(QPalette(m_parent->modlistOverwrittenLooseColor())); - m_containsBtn->setPalette(QPalette(m_parent->modlistContainsPluginColor())); - m_containedBtn->setPalette(QPalette(m_parent->pluginListContainedColor())); - QPalette palette1 = m_overwritingBtn->palette(); - QPalette palette2 = m_overwrittenBtn->palette(); - QPalette palette3 = m_containsBtn->palette(); - QPalette palette4 = m_containedBtn->palette(); - palette1.setColor(QPalette::Background, m_parent->modlistOverwritingLooseColor()); - palette2.setColor(QPalette::Background, m_parent->modlistOverwrittenLooseColor()); - palette3.setColor(QPalette::Background, m_parent->modlistContainsPluginColor()); - palette4.setColor(QPalette::Background, m_parent->pluginListContainedColor()); - m_overwritingBtn->setPalette(palette1); - m_overwrittenBtn->setPalette(palette2); - m_containsBtn->setPalette(palette3); - m_containedBtn->setPalette(palette4); - */ - - //version with stylesheet - m_dialog.setButtonColor(m_overwritingBtn, m_parent->modlistOverwritingLooseColor()); - m_dialog.setButtonColor(m_overwrittenBtn, m_parent->modlistOverwrittenLooseColor()); - m_dialog.setButtonColor(m_overwritingArchiveBtn, m_parent->modlistOverwritingArchiveColor()); - m_dialog.setButtonColor(m_overwrittenArchiveBtn, m_parent->modlistOverwrittenArchiveColor()); - m_dialog.setButtonColor(m_containsBtn, m_parent->modlistContainsPluginColor()); - m_dialog.setButtonColor(m_containedBtn, m_parent->pluginListContainedColor()); - - m_dialog.setOverwritingColor(m_parent->modlistOverwritingLooseColor()); - m_dialog.setOverwrittenColor(m_parent->modlistOverwrittenLooseColor()); - m_dialog.setOverwritingArchiveColor(m_parent->modlistOverwritingArchiveColor()); - m_dialog.setOverwrittenArchiveColor(m_parent->modlistOverwrittenArchiveColor()); - m_dialog.setContainsColor(m_parent->modlistContainsPluginColor()); - m_dialog.setContainedColor(m_parent->pluginListContainedColor()); - - m_compactBox->setChecked(m_parent->compactDownloads()); - m_showMetaBox->setChecked(m_parent->metaDownloads()); - m_usePrereleaseBox->setChecked(m_parent->usePrereleases()); - m_colorSeparatorsBox->setChecked(m_parent->colorSeparatorScrollbar()); -} - -void Settings::GeneralTab::update() -{ - QString oldLanguage = m_parent->language(); - QString newLanguage = m_languageBox->itemData(m_languageBox->currentIndex()).toString(); - if (newLanguage != oldLanguage) { - m_Settings.setValue("Settings/language", newLanguage); - emit m_parent->languageChanged(newLanguage); - } - - QString oldStyle = m_Settings.value("Settings/style", "").toString(); - QString newStyle = m_styleBox->itemData(m_styleBox->currentIndex()).toString(); - if (oldStyle != newStyle) { - m_Settings.setValue("Settings/style", newStyle); - emit m_parent->styleChanged(newStyle); - } - - m_Settings.setValue("Settings/overwritingLooseFilesColor", m_dialog.getOverwritingColor()); - m_Settings.setValue("Settings/overwrittenLooseFilesColor", m_dialog.getOverwrittenColor()); - m_Settings.setValue("Settings/overwritingArchiveFilesColor", m_dialog.getOverwritingArchiveColor()); - m_Settings.setValue("Settings/overwrittenArchiveFilesColor", m_dialog.getOverwrittenArchiveColor()); - m_Settings.setValue("Settings/containsPluginColor", m_dialog.getContainsColor()); - m_Settings.setValue("Settings/containedColor", m_dialog.getContainedColor()); - m_Settings.setValue("Settings/compact_downloads", m_compactBox->isChecked()); - m_Settings.setValue("Settings/meta_downloads", m_showMetaBox->isChecked()); - m_Settings.setValue("Settings/use_prereleases", m_usePrereleaseBox->isChecked()); - m_Settings.setValue("Settings/colorSeparatorScrollbars", m_colorSeparatorsBox->isChecked()); -} Settings::PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) : SettingsTab(parent, dialog) @@ -991,7 +833,7 @@ void Settings::PathsTab::update() } Settings::DiagnosticsTab::DiagnosticsTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) + : SettingsTab(m_parent, m_dialog) , m_logLevelBox(m_dialog.findChild("logLevelBox")) , m_dumpsTypeBox(m_dialog.findChild("dumpsTypeBox")) , m_dumpsMaxEdit(m_dialog.findChild("dumpsMaxEdit")) @@ -1036,7 +878,7 @@ void Settings::DiagnosticsTab::setLevelsBox() } Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) - : Settings::SettingsTab(parent, dialog) + : SettingsTab(parent, dialog) , m_offlineBox(dialog.findChild("offlineBox")) , m_proxyBox(dialog.findChild("proxyBox")) , m_knownServersList(dialog.findChild("knownServersList")) @@ -1114,7 +956,7 @@ void Settings::NexusTab::update() } Settings::SteamTab::SteamTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) + : SettingsTab(m_parent, m_dialog) , m_steamUserEdit(m_dialog.findChild("steamUserEdit")) , m_steamPassEdit(m_dialog.findChild("steamPassEdit")) { @@ -1134,7 +976,7 @@ void Settings::SteamTab::update() } Settings::PluginsTab::PluginsTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) + : SettingsTab(m_parent, m_dialog) , m_pluginsList(m_dialog.findChild("pluginsList")) , m_pluginBlacklistList(m_dialog.findChild("pluginBlacklist")) { @@ -1181,7 +1023,7 @@ void Settings::PluginsTab::update() Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) + : SettingsTab(m_parent, m_dialog) , m_appIDEdit(m_dialog.findChild("appIDEdit")) , m_mechanismBox(m_dialog.findChild("mechanismBox")) , m_hideUncheckedBox(m_dialog.findChild("hideUncheckedBox")) diff --git a/src/settings.h b/src/settings.h index c66eb94c..e88080ba 100644 --- a/src/settings.h +++ b/src/settings.h @@ -55,8 +55,30 @@ namespace MOBase { class IPluginGame; } +namespace Ui { + class SettingsDialog; +} + class SettingsDialog; class PluginContainer; +class Settings; + +class SettingsTab +{ +public: + SettingsTab(Settings *m_parent, SettingsDialog &m_dialog); + virtual ~SettingsTab(); + + virtual void update() = 0; + +protected: + Settings *m_parent; + QSettings &m_Settings; + SettingsDialog &m_dialog; + Ui::SettingsDialog* ui; + + QWidget* parentWidget(); +}; /** * manages the settings for Mod Organizer. The settings are not cached @@ -404,6 +426,8 @@ public: */ bool colorSeparatorScrollbar() const; + QSettings& settingsRef() { return m_Settings; } + public slots: void managedGameChanged(MOBase::IPluginGame const *gamePlugin); @@ -414,49 +438,10 @@ private: static bool obfuscate(const QString key, const QString data); static QString deObfuscate(const QString key); - void addLanguages(QComboBox *languageBox); - void addStyles(QComboBox *styleBox); void readPluginBlacklist(); void writePluginBlacklist(); QString getConfigurablePath(const QString &key, const QString &def, bool resolve) const; - class SettingsTab - { - public: - SettingsTab(Settings *m_parent, SettingsDialog &m_dialog); - virtual ~SettingsTab(); - - virtual void update() = 0; - - protected: - Settings *m_parent; - QSettings &m_Settings; - SettingsDialog &m_dialog; - - }; - - /** Display/store the configuration in the 'general' tab of the settings dialogue */ - class GeneralTab : public SettingsTab - { - public: - GeneralTab(Settings *m_parent, SettingsDialog &m_dialog); - - void update(); - - private: - QComboBox *m_languageBox; - QComboBox *m_styleBox; - QCheckBox *m_compactBox; - QCheckBox *m_showMetaBox; - QCheckBox *m_usePrereleaseBox; - QPushButton *m_overwritingBtn; - QPushButton *m_overwrittenBtn; - QPushButton *m_overwritingArchiveBtn; - QPushButton *m_overwrittenArchiveBtn; - QPushButton *m_containsBtn; - QPushButton *m_containedBtn; - QCheckBox *m_colorSeparatorsBox; - }; class PathsTab : public SettingsTab { @@ -554,8 +539,6 @@ private: private slots: - void resetDialogs(); - signals: void languageChanged(const QString &newLanguage); diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 0dae31ac..f922cfb9 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -131,23 +131,6 @@ QString SettingsDialog::getColoredButtonStyleSheet() const "}"); } -void SettingsDialog::setButtonColor(QPushButton *button, const QColor &color) -{ - button->setStyleSheet( - QString("QPushButton {" - "background-color: rgba(%1, %2, %3, %4);" - "color: %5;" - "border: 1px solid;" - "padding: 3px;" - "}") - .arg(color.red()) - .arg(color.green()) - .arg(color.blue()) - .arg(color.alpha()) - .arg(Settings::getIdealTextColor(color).name()) - ); -}; - void SettingsDialog::accept() { QString newModPath = ui->modDirEdit->text(); @@ -181,14 +164,6 @@ bool SettingsDialog::getApiKeyChanged() return m_keyChanged; } -void SettingsDialog::on_categoriesBtn_clicked() -{ - CategoriesDialog dialog(this); - if (dialog.exec() == QDialog::Accepted) { - dialog.commitChanges(); - } -} - void SettingsDialog::on_execBlacklistBtn_clicked() { bool ok = false; @@ -299,86 +274,6 @@ void SettingsDialog::on_browseGameDirBtn_clicked() } } -void SettingsDialog::on_containsBtn_clicked() -{ - QColor result = QColorDialog::getColor(m_ContainsColor, this, "Color Picker: Mod contains selected plugin", QColorDialog::ShowAlphaChannel); - if (result.isValid()) { - m_ContainsColor = result; - setButtonColor(ui->containsBtn, result); - } -} - -void SettingsDialog::on_containedBtn_clicked() -{ - QColor result = QColorDialog::getColor(m_ContainedColor, this, "ColorPicker: Plugin is Contained in selected Mod", QColorDialog::ShowAlphaChannel); - if (result.isValid()) { - m_ContainedColor = result; - setButtonColor(ui->containedBtn, result); - } -} - -void SettingsDialog::on_overwrittenBtn_clicked() -{ - QColor result = QColorDialog::getColor(m_OverwrittenColor, this, "ColorPicker: Is overwritten (loose files)", QColorDialog::ShowAlphaChannel); - if (result.isValid()) { - m_OverwrittenColor = result; - setButtonColor(ui->overwrittenBtn, result); - } -} - -void SettingsDialog::on_overwritingBtn_clicked() -{ - QColor result = QColorDialog::getColor(m_OverwritingColor, this, "ColorPicker: Is overwriting (loose files)", QColorDialog::ShowAlphaChannel); - if (result.isValid()) { - m_OverwritingColor = result; - setButtonColor(ui->overwritingBtn, result); - } -} - -void SettingsDialog::on_overwrittenArchiveBtn_clicked() -{ - QColor result = QColorDialog::getColor(m_OverwrittenArchiveColor, this, "ColorPicker: Is overwritten (archive files)", QColorDialog::ShowAlphaChannel); - if (result.isValid()) { - m_OverwrittenArchiveColor = result; - setButtonColor(ui->overwrittenArchiveBtn, result); - } -} - -void SettingsDialog::on_overwritingArchiveBtn_clicked() -{ - QColor result = QColorDialog::getColor(m_OverwritingArchiveColor, this, "ColorPicker: Is overwriting (archive files)", QColorDialog::ShowAlphaChannel); - if (result.isValid()) { - m_OverwritingArchiveColor = result; - setButtonColor(ui->overwritingArchiveBtn, result); - } -} - -void SettingsDialog::on_resetColorsBtn_clicked() -{ - m_OverwritingColor = QColor(255, 0, 0, 64); - m_OverwrittenColor = QColor(0, 255, 0, 64); - m_OverwritingArchiveColor = QColor(255, 0, 255, 64); - m_OverwrittenArchiveColor = QColor(0, 255, 255, 64); - m_ContainsColor = QColor(0, 0, 255, 64); - m_ContainedColor = QColor(0, 0, 255, 64); - - setButtonColor(ui->overwritingBtn, m_OverwritingColor); - setButtonColor(ui->overwrittenBtn, m_OverwrittenColor); - setButtonColor(ui->overwritingArchiveBtn, m_OverwritingArchiveColor); - setButtonColor(ui->overwrittenArchiveBtn, m_OverwrittenArchiveColor); - setButtonColor(ui->containsBtn, m_ContainsColor); - setButtonColor(ui->containedBtn, m_ContainedColor); -} - -void SettingsDialog::on_resetDialogsButton_clicked() -{ - if (QMessageBox::question(this, tr("Confirm?"), - tr("This will make all dialogs show up again where you checked the \"Remember selection\"-box. Continue?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - emit resetDialogs(); - } -} - void SettingsDialog::on_nexusConnect_clicked() { if (m_nexusLogin && m_nexusLogin->isActive()) { diff --git a/src/settingsdialog.h b/src/settingsdialog.h index c5f487fd..01a0afa2 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -54,7 +54,7 @@ public: */ QString getColoredButtonStyleSheet() const; - void setButtonColor(QPushButton *button, const QColor &color); + Ui::SettingsDialog *ui; public slots: @@ -62,7 +62,6 @@ public slots: signals: - void resetDialogs(); void retryApiConnection(); private: @@ -71,73 +70,41 @@ private: void normalizePath(QLineEdit *lineEdit); public: - - QColor getOverwritingColor() { return m_OverwritingColor; } - QColor getOverwrittenColor() { return m_OverwrittenColor; } - QColor getOverwritingArchiveColor() { return m_OverwritingArchiveColor; } - QColor getOverwrittenArchiveColor() { return m_OverwrittenArchiveColor; } - QColor getContainsColor() { return m_ContainsColor; } - QColor getContainedColor() { return m_ContainedColor; } QString getExecutableBlacklist() { return m_ExecutableBlacklist; } + void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } bool getResetGeometries(); bool getApiKeyChanged(); - void setOverwritingColor(QColor col) { m_OverwritingColor = col; } - void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; } - void setOverwritingArchiveColor(QColor col) { m_OverwritingArchiveColor = col; } - void setOverwrittenArchiveColor(QColor col) { m_OverwrittenArchiveColor = col; } - void setContainsColor(QColor col) { m_ContainsColor = col; } - void setContainedColor(QColor col) { m_ContainedColor = col; } - void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } - - private slots: - void on_categoriesBtn_clicked(); - void on_execBlacklistBtn_clicked(); - void on_bsaDateBtn_clicked(); - void on_browseDownloadDirBtn_clicked(); - void on_browseModDirBtn_clicked(); - void on_browseCacheDirBtn_clicked(); - void on_resetDialogsButton_clicked(); - void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); void on_associateButton_clicked(); - void on_clearCacheButton_clicked(); - void on_nexusDisconnect_clicked(); + void on_baseDirEdit_editingFinished(); void on_browseBaseDirBtn_clicked(); + void on_browseCacheDirBtn_clicked(); + void on_browseDownloadDirBtn_clicked(); + void on_browseGameDirBtn_clicked(); + void on_browseModDirBtn_clicked(); void on_browseOverwriteDirBtn_clicked(); void on_browseProfilesDirBtn_clicked(); - void on_browseGameDirBtn_clicked(); - void on_overwritingBtn_clicked(); - void on_overwrittenBtn_clicked(); - void on_overwritingArchiveBtn_clicked(); - void on_overwrittenArchiveBtn_clicked(); - void on_containsBtn_clicked(); - void on_containedBtn_clicked(); - void on_resetColorsBtn_clicked(); - void on_baseDirEdit_editingFinished(); + void on_bsaDateBtn_clicked(); + void on_cacheDirEdit_editingFinished(); + void on_clearCacheButton_clicked(); void on_downloadDirEdit_editingFinished(); + void on_execBlacklistBtn_clicked(); void on_modDirEdit_editingFinished(); - void on_cacheDirEdit_editingFinished(); - void on_profilesDirEdit_editingFinished(); - void on_overwriteDirEdit_editingFinished(); void on_nexusConnect_clicked(); + void on_nexusDisconnect_clicked(); void on_nexusManualKey_clicked(); + void on_overwriteDirEdit_editingFinished(); + void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void on_profilesDirEdit_editingFinished(); void on_resetGeometryBtn_clicked(); void deleteBlacklistItem(); private: - Ui::SettingsDialog *ui; Settings* m_settings; PluginContainer *m_PluginContainer; - QColor m_OverwritingColor; - QColor m_OverwrittenColor; - QColor m_OverwritingArchiveColor; - QColor m_OverwrittenArchiveColor; - QColor m_ContainsColor; - QColor m_ContainedColor; - bool m_GeometriesReset; bool m_keyChanged; diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp new file mode 100644 index 00000000..b22b04fd --- /dev/null +++ b/src/settingsdialoggeneral.cpp @@ -0,0 +1,274 @@ +#include "settingsdialoggeneral.h" +#include "ui_settingsdialog.h" +#include "appconfig.h" +#include "categoriesdialog.h" +#include + +using MOBase::QuestionBoxMemory; + +GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog) + : SettingsTab(m_parent, m_dialog) +{ + addLanguages(); + { + QString languageCode = m_parent->language(); + int currentID = ui->languageBox->findData(languageCode); + // I made a mess. :( Most languages are stored with only the iso country + // code (2 characters like "de") but chinese + // with the exact language variant (zh_TW) so I have to search for both + // variants + if (currentID == -1) { + currentID = ui->languageBox->findData(languageCode.mid(0, 2)); + } + if (currentID != -1) { + ui->languageBox->setCurrentIndex(currentID); + } + } + + addStyles(); + { + int currentID = ui->styleBox->findData( + m_Settings.value("Settings/style", "").toString()); + if (currentID != -1) { + ui->styleBox->setCurrentIndex(currentID); + } + } + /* verision using palette only works with fusion theme for some stupid reason... + m_overwritingBtn->setAutoFillBackground(true); + m_overwrittenBtn->setAutoFillBackground(true); + m_containsBtn->setAutoFillBackground(true); + m_containedBtn->setAutoFillBackground(true); + m_overwritingBtn->setPalette(QPalette(m_parent->modlistOverwritingLooseColor())); + m_overwrittenBtn->setPalette(QPalette(m_parent->modlistOverwrittenLooseColor())); + m_containsBtn->setPalette(QPalette(m_parent->modlistContainsPluginColor())); + m_containedBtn->setPalette(QPalette(m_parent->pluginListContainedColor())); + QPalette palette1 = m_overwritingBtn->palette(); + QPalette palette2 = m_overwrittenBtn->palette(); + QPalette palette3 = m_containsBtn->palette(); + QPalette palette4 = m_containedBtn->palette(); + palette1.setColor(QPalette::Background, m_parent->modlistOverwritingLooseColor()); + palette2.setColor(QPalette::Background, m_parent->modlistOverwrittenLooseColor()); + palette3.setColor(QPalette::Background, m_parent->modlistContainsPluginColor()); + palette4.setColor(QPalette::Background, m_parent->pluginListContainedColor()); + m_overwritingBtn->setPalette(palette1); + m_overwrittenBtn->setPalette(palette2); + m_containsBtn->setPalette(palette3); + m_containedBtn->setPalette(palette4); + */ + + //version with stylesheet + setButtonColor(ui->overwritingBtn, m_parent->modlistOverwritingLooseColor()); + setButtonColor(ui->overwrittenBtn, m_parent->modlistOverwrittenLooseColor()); + setButtonColor(ui->overwritingArchiveBtn, m_parent->modlistOverwritingArchiveColor()); + setButtonColor(ui->overwrittenArchiveBtn, m_parent->modlistOverwrittenArchiveColor()); + setButtonColor(ui->containsBtn, m_parent->modlistContainsPluginColor()); + setButtonColor(ui->containedBtn, m_parent->pluginListContainedColor()); + + setOverwritingColor(m_parent->modlistOverwritingLooseColor()); + setOverwrittenColor(m_parent->modlistOverwrittenLooseColor()); + setOverwritingArchiveColor(m_parent->modlistOverwritingArchiveColor()); + setOverwrittenArchiveColor(m_parent->modlistOverwrittenArchiveColor()); + setContainsColor(m_parent->modlistContainsPluginColor()); + setContainedColor(m_parent->pluginListContainedColor()); + + ui->compactBox->setChecked(m_parent->compactDownloads()); + ui->showMetaBox->setChecked(m_parent->metaDownloads()); + ui->usePrereleaseBox->setChecked(m_parent->usePrereleases()); + ui->colorSeparatorsBox->setChecked(m_parent->colorSeparatorScrollbar()); + + QObject::connect(ui->overwritingArchiveBtn, &QPushButton::clicked, [&]{ on_overwritingArchiveBtn_clicked(); }); + QObject::connect(ui->overwritingBtn, &QPushButton::clicked, [&]{ on_overwritingBtn_clicked(); }); + QObject::connect(ui->overwrittenArchiveBtn, &QPushButton::clicked, [&]{ on_overwrittenArchiveBtn_clicked(); }); + QObject::connect(ui->overwrittenBtn, &QPushButton::clicked, [&]{ on_overwrittenBtn_clicked(); }); + QObject::connect(ui->containedBtn, &QPushButton::clicked, [&]{ on_containedBtn_clicked(); }); + QObject::connect(ui->containsBtn, &QPushButton::clicked, [&]{ on_containsBtn_clicked(); }); + QObject::connect(ui->categoriesBtn, &QPushButton::clicked, [&]{ on_categoriesBtn_clicked(); }); + QObject::connect(ui->resetColorsBtn, &QPushButton::clicked, [&]{ on_resetColorsBtn_clicked(); }); + QObject::connect(ui->resetDialogsButton, &QPushButton::clicked, [&]{ on_resetDialogsButton_clicked(); }); +} + +void GeneralTab::update() +{ + QString oldLanguage = m_parent->language(); + QString newLanguage = ui->languageBox->itemData(ui->languageBox->currentIndex()).toString(); + if (newLanguage != oldLanguage) { + m_Settings.setValue("Settings/language", newLanguage); + emit m_parent->languageChanged(newLanguage); + } + + QString oldStyle = m_Settings.value("Settings/style", "").toString(); + QString newStyle = ui->styleBox->itemData(ui->styleBox->currentIndex()).toString(); + if (oldStyle != newStyle) { + m_Settings.setValue("Settings/style", newStyle); + emit m_parent->styleChanged(newStyle); + } + + m_Settings.setValue("Settings/overwritingLooseFilesColor", getOverwritingColor()); + m_Settings.setValue("Settings/overwrittenLooseFilesColor", getOverwrittenColor()); + m_Settings.setValue("Settings/overwritingArchiveFilesColor", getOverwritingArchiveColor()); + m_Settings.setValue("Settings/overwrittenArchiveFilesColor", getOverwrittenArchiveColor()); + m_Settings.setValue("Settings/containsPluginColor", getContainsColor()); + m_Settings.setValue("Settings/containedColor", getContainedColor()); + m_Settings.setValue("Settings/compact_downloads", ui->compactBox->isChecked()); + m_Settings.setValue("Settings/meta_downloads", ui->showMetaBox->isChecked()); + m_Settings.setValue("Settings/use_prereleases", ui->usePrereleaseBox->isChecked()); + m_Settings.setValue("Settings/colorSeparatorScrollbars", ui->colorSeparatorsBox->isChecked()); +} + +void GeneralTab::addLanguages() +{ + std::vector> languages; + + QDirIterator langIter(QCoreApplication::applicationDirPath() + "/translations", QDir::Files); + QString pattern = QString::fromStdWString(AppConfig::translationPrefix()) + "_([a-z]{2,3}(_[A-Z]{2,2})?).qm"; + QRegExp exp(pattern); + while (langIter.hasNext()) { + langIter.next(); + QString file = langIter.fileName(); + if (exp.exactMatch(file)) { + QString languageCode = exp.cap(1); + QLocale locale(languageCode); + QString languageString = QString("%1 (%2)").arg(locale.nativeLanguageName()).arg(locale.nativeCountryName()); //QLocale::languageToString(locale.language()); + if (locale.language() == QLocale::Chinese) { + if (languageCode == "zh_TW") { + languageString = "Chinese (traditional)"; + } else { + languageString = "Chinese (simplified)"; + } + } + languages.push_back(std::make_pair(QString("%1").arg(languageString), exp.cap(1))); + //languageBox->addItem(QString("%1").arg(languageString), exp.cap(1)); + } + } + if (!ui->languageBox->findText("English")) { + languages.push_back(std::make_pair(QString("English"), QString("en_US"))); + //languageBox->addItem("English", "en_US"); + } + std::sort(languages.begin(), languages.end()); + for (const auto &lang : languages) { + ui->languageBox->addItem(lang.first, lang.second); + } +} + +void GeneralTab::addStyles() +{ + ui->styleBox->addItem("None", ""); + ui->styleBox->addItem("Fusion", "Fusion"); + + QDirIterator langIter(QCoreApplication::applicationDirPath() + "/" + QString::fromStdWString(AppConfig::stylesheetsPath()), QStringList("*.qss"), QDir::Files); + while (langIter.hasNext()) { + langIter.next(); + QString style = langIter.fileName(); + ui->styleBox->addItem(style, style); + } +} + +void GeneralTab::resetDialogs() +{ + QuestionBoxMemory::resetDialogs(); +} + +void GeneralTab::setButtonColor(QPushButton *button, const QColor &color) +{ + button->setStyleSheet( + QString("QPushButton {" + "background-color: rgba(%1, %2, %3, %4);" + "color: %5;" + "border: 1px solid;" + "padding: 3px;" + "}") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue()) + .arg(color.alpha()) + .arg(Settings::getIdealTextColor(color).name()) + ); +}; + +void GeneralTab::on_containsBtn_clicked() +{ + QColor result = QColorDialog::getColor(m_ContainsColor, parentWidget(), "Color Picker: Mod contains selected plugin", QColorDialog::ShowAlphaChannel); + if (result.isValid()) { + m_ContainsColor = result; + setButtonColor(ui->containsBtn, result); + } +} + +void GeneralTab::on_containedBtn_clicked() +{ + QColor result = QColorDialog::getColor(m_ContainedColor, parentWidget(), "ColorPicker: Plugin is Contained in selected Mod", QColorDialog::ShowAlphaChannel); + if (result.isValid()) { + m_ContainedColor = result; + setButtonColor(ui->containedBtn, result); + } +} + +void GeneralTab::on_overwrittenBtn_clicked() +{ + QColor result = QColorDialog::getColor(m_OverwrittenColor, parentWidget(), "ColorPicker: Is overwritten (loose files)", QColorDialog::ShowAlphaChannel); + if (result.isValid()) { + m_OverwrittenColor = result; + setButtonColor(ui->overwrittenBtn, result); + } +} + +void GeneralTab::on_overwritingBtn_clicked() +{ + QColor result = QColorDialog::getColor(m_OverwritingColor, parentWidget(), "ColorPicker: Is overwriting (loose files)", QColorDialog::ShowAlphaChannel); + if (result.isValid()) { + m_OverwritingColor = result; + setButtonColor(ui->overwritingBtn, result); + } +} + +void GeneralTab::on_overwrittenArchiveBtn_clicked() +{ + QColor result = QColorDialog::getColor(m_OverwrittenArchiveColor, parentWidget(), "ColorPicker: Is overwritten (archive files)", QColorDialog::ShowAlphaChannel); + if (result.isValid()) { + m_OverwrittenArchiveColor = result; + setButtonColor(ui->overwrittenArchiveBtn, result); + } +} + +void GeneralTab::on_overwritingArchiveBtn_clicked() +{ + QColor result = QColorDialog::getColor(m_OverwritingArchiveColor, parentWidget(), "ColorPicker: Is overwriting (archive files)", QColorDialog::ShowAlphaChannel); + if (result.isValid()) { + m_OverwritingArchiveColor = result; + setButtonColor(ui->overwritingArchiveBtn, result); + } +} + +void GeneralTab::on_resetColorsBtn_clicked() +{ + m_OverwritingColor = QColor(255, 0, 0, 64); + m_OverwrittenColor = QColor(0, 255, 0, 64); + m_OverwritingArchiveColor = QColor(255, 0, 255, 64); + m_OverwrittenArchiveColor = QColor(0, 255, 255, 64); + m_ContainsColor = QColor(0, 0, 255, 64); + m_ContainedColor = QColor(0, 0, 255, 64); + + setButtonColor(ui->overwritingBtn, m_OverwritingColor); + setButtonColor(ui->overwrittenBtn, m_OverwrittenColor); + setButtonColor(ui->overwritingArchiveBtn, m_OverwritingArchiveColor); + setButtonColor(ui->overwrittenArchiveBtn, m_OverwrittenArchiveColor); + setButtonColor(ui->containsBtn, m_ContainsColor); + setButtonColor(ui->containedBtn, m_ContainedColor); +} + +void GeneralTab::on_resetDialogsButton_clicked() +{ + if (QMessageBox::question(parentWidget(), QObject::tr("Confirm?"), + QObject::tr("This will make all dialogs show up again where you checked the \"Remember selection\"-box. Continue?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + resetDialogs(); + } +} + +void GeneralTab::on_categoriesBtn_clicked() +{ + CategoriesDialog dialog(parentWidget()); + if (dialog.exec() == QDialog::Accepted) { + dialog.commitChanges(); + } +} diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h new file mode 100644 index 00000000..1f1b4637 --- /dev/null +++ b/src/settingsdialoggeneral.h @@ -0,0 +1,53 @@ +#ifndef SETTINGSDIALOGGENERAL_H +#define SETTINGSDIALOGGENERAL_H + +#include "settingsdialog.h" +#include "settings.h" + +class GeneralTab : public SettingsTab +{ +public: + GeneralTab(Settings *m_parent, SettingsDialog &m_dialog); + + void update(); + +private: + QColor m_OverwritingColor; + QColor m_OverwrittenColor; + QColor m_OverwritingArchiveColor; + QColor m_OverwrittenArchiveColor; + QColor m_ContainsColor; + QColor m_ContainedColor; + + void addLanguages(); + void addStyles(); + void resetDialogs(); + void setButtonColor(QPushButton *button, const QColor &color); + + QColor getOverwritingColor() { return m_OverwritingColor; } + QColor getOverwrittenColor() { return m_OverwrittenColor; } + QColor getOverwritingArchiveColor() { return m_OverwritingArchiveColor; } + QColor getOverwrittenArchiveColor() { return m_OverwrittenArchiveColor; } + QColor getContainsColor() { return m_ContainsColor; } + QColor getContainedColor() { return m_ContainedColor; } + + void setOverwritingColor(QColor col) { m_OverwritingColor = col; } + void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; } + void setOverwritingArchiveColor(QColor col) { m_OverwritingArchiveColor = col; } + void setOverwrittenArchiveColor(QColor col) { m_OverwrittenArchiveColor = col; } + void setContainsColor(QColor col) { m_ContainsColor = col; } + void setContainedColor(QColor col) { m_ContainedColor = col; } + + void on_overwritingArchiveBtn_clicked(); + void on_overwritingBtn_clicked(); + void on_overwrittenArchiveBtn_clicked(); + void on_overwrittenBtn_clicked(); + void on_containedBtn_clicked(); + void on_containsBtn_clicked(); + + void on_categoriesBtn_clicked(); + void on_resetColorsBtn_clicked(); + void on_resetDialogsButton_clicked(); +}; + +#endif // SETTINGSDIALOGGENERAL_H -- cgit v1.3.1 From 0a5ce34b1a80694fbfe6a4d6b4f032b9c11a5376 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 09:02:03 -0400 Subject: split paths tab --- src/CMakeLists.txt | 3 + src/settings.cpp | 81 +---------------- src/settings.h | 17 +--- src/settingsdialog.cpp | 113 ------------------------ src/settingsdialog.h | 14 --- src/settingsdialogpaths.cpp | 205 ++++++++++++++++++++++++++++++++++++++++++++ src/settingsdialogpaths.h | 33 +++++++ 7 files changed, 243 insertions(+), 223 deletions(-) create mode 100644 src/settingsdialogpaths.cpp create mode 100644 src/settingsdialogpaths.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 29c419b5..98d59996 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,7 @@ SET(organizer_SRCS singleinstance.cpp settingsdialog.cpp settingsdialoggeneral.cpp + settingsdialogpaths.cpp settings.cpp selfupdater.cpp selectiondialog.cpp @@ -153,6 +154,7 @@ SET(organizer_HDRS singleinstance.h settingsdialog.h settingsdialoggeneral.h + settingsdialogpaths.h settings.h selfupdater.h selectiondialog.h @@ -434,6 +436,7 @@ set(settings settings settingsdialog settingsdialoggeneral + settingsdialogpaths ) set(utilities diff --git a/src/settings.cpp b/src/settings.cpp index 0911b155..bed8e789 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -23,6 +23,7 @@ along with Mod Organizer. If not, see . #include "serverinfo.h" #include "settingsdialog.h" #include "settingsdialoggeneral.h" +#include "settingsdialogpaths.h" #include "versioninfo.h" #include "appconfig.h" #include "organizercore.h" @@ -752,86 +753,6 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) } -Settings::PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) - : SettingsTab(parent, dialog) - , m_baseDirEdit(m_dialog.findChild("baseDirEdit")) - , m_downloadDirEdit(m_dialog.findChild("downloadDirEdit")) - , m_modDirEdit(m_dialog.findChild("modDirEdit")) - , m_cacheDirEdit(m_dialog.findChild("cacheDirEdit")) - , m_profilesDirEdit(m_dialog.findChild("profilesDirEdit")) - , m_overwriteDirEdit(m_dialog.findChild("overwriteDirEdit")) - , m_managedGameDirEdit(m_dialog.findChild("managedGameDirEdit")) -{ - m_baseDirEdit->setText(m_parent->getBaseDirectory()); - m_managedGameDirEdit->setText(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName())); - QString basePath = parent->getBaseDirectory(); - QDir baseDir(basePath); - for (const auto &dir : { - std::make_pair(m_downloadDirEdit, m_parent->getDownloadDirectory(false)), - std::make_pair(m_modDirEdit, m_parent->getModDirectory(false)), - std::make_pair(m_cacheDirEdit, m_parent->getCacheDirectory(false)), - std::make_pair(m_profilesDirEdit, m_parent->getProfileDirectory(false)), - std::make_pair(m_overwriteDirEdit, m_parent->getOverwriteDirectory(false)) - }) { - QString storePath = baseDir.relativeFilePath(dir.second); - storePath = dir.second; - dir.first->setText(storePath); - } -} - -void Settings::PathsTab::update() -{ - typedef std::tuple Directory; - - QString basePath = m_parent->getBaseDirectory(); - - for (const Directory &dir :{ - Directory{m_downloadDirEdit->text(), "download_directory", AppConfig::downloadPath()}, - Directory{m_cacheDirEdit->text(), "cache_directory", AppConfig::cachePath()}, - Directory{m_modDirEdit->text(), "mod_directory", AppConfig::modsPath()}, - Directory{m_overwriteDirEdit->text(), "overwrite_directory", AppConfig::overwritePath()}, - Directory{m_profilesDirEdit->text(), "profiles_directory", AppConfig::profilesPath()} - }) { - QString path, settingsKey; - std::wstring defaultName; - std::tie(path, settingsKey, defaultName) = dir; - - settingsKey = QString("Settings/%1").arg(settingsKey); - - QString realPath = path; - realPath.replace("%BASE_DIR%", m_baseDirEdit->text()); - - if (!QDir(realPath).exists()) { - if (!QDir().mkpath(realPath)) { - QMessageBox::warning(qApp->activeWindow(), tr("Error"), - tr("Failed to create \"%1\", you may not have the " - "necessary permission. path remains unchanged.") - .arg(realPath)); - } - } - - if (QFileInfo(realPath) - != QFileInfo(basePath + "/" + QString::fromStdWString(defaultName))) { - m_Settings.setValue(settingsKey, path); - } else { - m_Settings.remove(settingsKey); - } - } - - if (QFileInfo(m_baseDirEdit->text()) != - QFileInfo(qApp->property("dataPath").toString())) { - m_Settings.setValue("Settings/base_directory", m_baseDirEdit->text()); - } else { - m_Settings.remove("Settings/base_directory"); - } - - QFileInfo oldGameExe(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName())); - QFileInfo newGameExe(m_managedGameDirEdit->text()); - if (oldGameExe != newGameExe) { - m_Settings.setValue("gamePath", newGameExe.absolutePath()); - } -} - Settings::DiagnosticsTab::DiagnosticsTab(Settings *m_parent, SettingsDialog &m_dialog) : SettingsTab(m_parent, m_dialog) , m_logLevelBox(m_dialog.findChild("logLevelBox")) diff --git a/src/settings.h b/src/settings.h index e88080ba..6f75562b 100644 --- a/src/settings.h +++ b/src/settings.h @@ -427,6 +427,7 @@ public: bool colorSeparatorScrollbar() const; QSettings& settingsRef() { return m_Settings; } + MOBase::IPluginGame const *gamePlugin() { return m_GamePlugin; } public slots: @@ -443,22 +444,6 @@ private: QString getConfigurablePath(const QString &key, const QString &def, bool resolve) const; - class PathsTab : public SettingsTab - { - public: - PathsTab(Settings *parent, SettingsDialog &dialog); - - void update(); - - private: - QLineEdit *m_baseDirEdit; - QLineEdit *m_downloadDirEdit; - QLineEdit *m_modDirEdit; - QLineEdit *m_cacheDirEdit; - QLineEdit *m_profilesDirEdit; - QLineEdit *m_overwriteDirEdit; - QLineEdit *m_managedGameDirEdit; - }; class DiagnosticsTab : public SettingsTab { diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index f922cfb9..bfa285bf 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -200,80 +200,6 @@ void SettingsDialog::on_bsaDateBtn_clicked() dir.absolutePath().toStdWString()); } -void SettingsDialog::on_browseBaseDirBtn_clicked() -{ - QString temp = QFileDialog::getExistingDirectory( - this, tr("Select base directory"), ui->baseDirEdit->text()); - if (!temp.isEmpty()) { - ui->baseDirEdit->setText(temp); - } -} - -void SettingsDialog::on_browseDownloadDirBtn_clicked() -{ - QString searchPath = ui->downloadDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); - - QString temp = QFileDialog::getExistingDirectory(this, tr("Select download directory"), searchPath); - if (!temp.isEmpty()) { - ui->downloadDirEdit->setText(temp); - } -} - -void SettingsDialog::on_browseModDirBtn_clicked() -{ - QString searchPath = ui->modDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); - - QString temp = QFileDialog::getExistingDirectory(this, tr("Select mod directory"), searchPath); - if (!temp.isEmpty()) { - ui->modDirEdit->setText(temp); - } -} - -void SettingsDialog::on_browseCacheDirBtn_clicked() -{ - QString searchPath = ui->cacheDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); - - QString temp = QFileDialog::getExistingDirectory(this, tr("Select cache directory"), searchPath); - if (!temp.isEmpty()) { - ui->cacheDirEdit->setText(temp); - } -} - -void SettingsDialog::on_browseProfilesDirBtn_clicked() -{ - QString searchPath = ui->profilesDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); - - QString temp = QFileDialog::getExistingDirectory(this, tr("Select profiles directory"), searchPath); - if (!temp.isEmpty()) { - ui->profilesDirEdit->setText(temp); - } -} - -void SettingsDialog::on_browseOverwriteDirBtn_clicked() -{ - QString searchPath = ui->overwriteDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); - - QString temp = QFileDialog::getExistingDirectory(this, tr("Select overwrite directory"), searchPath); - if (!temp.isEmpty()) { - ui->overwriteDirEdit->setText(temp); - } -} - -void SettingsDialog::on_browseGameDirBtn_clicked() -{ - QFileInfo oldGameExe(ui->managedGameDirEdit->text()); - - QString temp = QFileDialog::getOpenFileName(this, tr("Select game executable"), oldGameExe.absolutePath(), oldGameExe.fileName()); - if (!temp.isEmpty()) { - ui->managedGameDirEdit->setText(temp); - } -} - void SettingsDialog::on_nexusConnect_clicked() { if (m_nexusLogin && m_nexusLogin->isActive()) { @@ -555,45 +481,6 @@ void SettingsDialog::on_clearCacheButton_clicked() NexusInterface::instance(m_PluginContainer)->clearCache(); } -void SettingsDialog::normalizePath(QLineEdit *lineEdit) -{ - QString text = lineEdit->text(); - while (text.endsWith('/') || text.endsWith('\\')) { - text.chop(1); - } - lineEdit->setText(text); -} - -void SettingsDialog::on_baseDirEdit_editingFinished() -{ - normalizePath(ui->baseDirEdit); -} - -void SettingsDialog::on_downloadDirEdit_editingFinished() -{ - normalizePath(ui->downloadDirEdit); -} - -void SettingsDialog::on_modDirEdit_editingFinished() -{ - normalizePath(ui->modDirEdit); -} - -void SettingsDialog::on_cacheDirEdit_editingFinished() -{ - normalizePath(ui->cacheDirEdit); -} - -void SettingsDialog::on_profilesDirEdit_editingFinished() -{ - normalizePath(ui->profilesDirEdit); -} - -void SettingsDialog::on_overwriteDirEdit_editingFinished() -{ - normalizePath(ui->overwriteDirEdit); -} - void SettingsDialog::on_resetGeometryBtn_clicked() { m_GeometriesReset = true; diff --git a/src/settingsdialog.h b/src/settingsdialog.h index 01a0afa2..68e72529 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -67,7 +67,6 @@ signals: private: void storeSettings(QListWidgetItem *pluginItem); - void normalizePath(QLineEdit *lineEdit); public: QString getExecutableBlacklist() { return m_ExecutableBlacklist; } @@ -77,26 +76,13 @@ public: private slots: void on_associateButton_clicked(); - void on_baseDirEdit_editingFinished(); - void on_browseBaseDirBtn_clicked(); - void on_browseCacheDirBtn_clicked(); - void on_browseDownloadDirBtn_clicked(); - void on_browseGameDirBtn_clicked(); - void on_browseModDirBtn_clicked(); - void on_browseOverwriteDirBtn_clicked(); - void on_browseProfilesDirBtn_clicked(); void on_bsaDateBtn_clicked(); - void on_cacheDirEdit_editingFinished(); void on_clearCacheButton_clicked(); - void on_downloadDirEdit_editingFinished(); void on_execBlacklistBtn_clicked(); - void on_modDirEdit_editingFinished(); void on_nexusConnect_clicked(); void on_nexusDisconnect_clicked(); void on_nexusManualKey_clicked(); - void on_overwriteDirEdit_editingFinished(); void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - void on_profilesDirEdit_editingFinished(); void on_resetGeometryBtn_clicked(); void deleteBlacklistItem(); diff --git a/src/settingsdialogpaths.cpp b/src/settingsdialogpaths.cpp new file mode 100644 index 00000000..303d1562 --- /dev/null +++ b/src/settingsdialogpaths.cpp @@ -0,0 +1,205 @@ +#include "settingsdialogpaths.h" +#include "ui_settingsdialog.h" +#include "appconfig.h" +#include + +PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) + : SettingsTab(parent, dialog) +{ + ui->baseDirEdit->setText(m_parent->getBaseDirectory()); + ui->managedGameDirEdit->setText(m_parent->gamePlugin()->gameDirectory().absoluteFilePath(m_parent->gamePlugin()->binaryName())); + QString basePath = parent->getBaseDirectory(); + QDir baseDir(basePath); + for (const auto &dir : { + std::make_pair(ui->downloadDirEdit, m_parent->getDownloadDirectory(false)), + std::make_pair(ui->modDirEdit, m_parent->getModDirectory(false)), + std::make_pair(ui->cacheDirEdit, m_parent->getCacheDirectory(false)), + std::make_pair(ui->profilesDirEdit, m_parent->getProfileDirectory(false)), + std::make_pair(ui->overwriteDirEdit, m_parent->getOverwriteDirectory(false)) + }) { + QString storePath = baseDir.relativeFilePath(dir.second); + storePath = dir.second; + dir.first->setText(storePath); + } + + QObject::connect(ui->browseBaseDirBtn, &QPushButton::clicked, [&]{ on_browseBaseDirBtn_clicked(); }); + QObject::connect(ui->browseCacheDirBtn, &QPushButton::clicked, [&]{ on_browseCacheDirBtn_clicked(); }); + QObject::connect(ui->browseDownloadDirBtn, &QPushButton::clicked, [&]{ on_browseDownloadDirBtn_clicked(); }); + QObject::connect(ui->browseGameDirBtn, &QPushButton::clicked, [&]{ on_browseGameDirBtn_clicked(); }); + QObject::connect(ui->browseModDirBtn, &QPushButton::clicked, [&]{ on_browseModDirBtn_clicked(); }); + QObject::connect(ui->browseOverwriteDirBtn, &QPushButton::clicked, [&]{ on_browseOverwriteDirBtn_clicked(); }); + QObject::connect(ui->browseProfilesDirBtn, &QPushButton::clicked, [&]{ on_browseProfilesDirBtn_clicked(); }); + + QObject::connect(ui->baseDirEdit, &QLineEdit::editingFinished, [&]{ on_baseDirEdit_editingFinished(); }); + QObject::connect(ui->cacheDirEdit, &QLineEdit::editingFinished, [&]{ on_cacheDirEdit_editingFinished(); }); + QObject::connect(ui->downloadDirEdit, &QLineEdit::editingFinished, [&]{ on_downloadDirEdit_editingFinished(); }); + QObject::connect(ui->modDirEdit, &QLineEdit::editingFinished, [&]{ on_modDirEdit_editingFinished(); }); + QObject::connect(ui->overwriteDirEdit, &QLineEdit::editingFinished, [&]{ on_overwriteDirEdit_editingFinished(); }); + QObject::connect(ui->profilesDirEdit, &QLineEdit::editingFinished, [&]{ on_profilesDirEdit_editingFinished(); }); +} + +void PathsTab::update() +{ + typedef std::tuple Directory; + + QString basePath = m_parent->getBaseDirectory(); + + for (const Directory &dir :{ + Directory{ui->downloadDirEdit->text(), "download_directory", AppConfig::downloadPath()}, + Directory{ui->cacheDirEdit->text(), "cache_directory", AppConfig::cachePath()}, + Directory{ui->modDirEdit->text(), "mod_directory", AppConfig::modsPath()}, + Directory{ui->overwriteDirEdit->text(), "overwrite_directory", AppConfig::overwritePath()}, + Directory{ui->profilesDirEdit->text(), "profiles_directory", AppConfig::profilesPath()} + }) { + QString path, settingsKey; + std::wstring defaultName; + std::tie(path, settingsKey, defaultName) = dir; + + settingsKey = QString("Settings/%1").arg(settingsKey); + + QString realPath = path; + realPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + + if (!QDir(realPath).exists()) { + if (!QDir().mkpath(realPath)) { + QMessageBox::warning(qApp->activeWindow(), QObject::tr("Error"), + QObject::tr("Failed to create \"%1\", you may not have the " + "necessary permission. path remains unchanged.") + .arg(realPath)); + } + } + + if (QFileInfo(realPath) + != QFileInfo(basePath + "/" + QString::fromStdWString(defaultName))) { + m_Settings.setValue(settingsKey, path); + } else { + m_Settings.remove(settingsKey); + } + } + + if (QFileInfo(ui->baseDirEdit->text()) != + QFileInfo(qApp->property("dataPath").toString())) { + m_Settings.setValue("Settings/base_directory", ui->baseDirEdit->text()); + } else { + m_Settings.remove("Settings/base_directory"); + } + + QFileInfo oldGameExe(m_parent->gamePlugin()->gameDirectory().absoluteFilePath(m_parent->gamePlugin()->binaryName())); + QFileInfo newGameExe(ui->managedGameDirEdit->text()); + if (oldGameExe != newGameExe) { + m_Settings.setValue("gamePath", newGameExe.absolutePath()); + } +} + +void PathsTab::on_browseBaseDirBtn_clicked() +{ + QString temp = QFileDialog::getExistingDirectory( + parentWidget(), QObject::tr("Select base directory"), ui->baseDirEdit->text()); + if (!temp.isEmpty()) { + ui->baseDirEdit->setText(temp); + } +} + +void PathsTab::on_browseDownloadDirBtn_clicked() +{ + QString searchPath = ui->downloadDirEdit->text(); + searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + + QString temp = QFileDialog::getExistingDirectory(parentWidget(), QObject::tr("Select download directory"), searchPath); + if (!temp.isEmpty()) { + ui->downloadDirEdit->setText(temp); + } +} + +void PathsTab::on_browseModDirBtn_clicked() +{ + QString searchPath = ui->modDirEdit->text(); + searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + + QString temp = QFileDialog::getExistingDirectory(parentWidget(), QObject::tr("Select mod directory"), searchPath); + if (!temp.isEmpty()) { + ui->modDirEdit->setText(temp); + } +} + +void PathsTab::on_browseCacheDirBtn_clicked() +{ + QString searchPath = ui->cacheDirEdit->text(); + searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + + QString temp = QFileDialog::getExistingDirectory(parentWidget(), QObject::tr("Select cache directory"), searchPath); + if (!temp.isEmpty()) { + ui->cacheDirEdit->setText(temp); + } +} + +void PathsTab::on_browseProfilesDirBtn_clicked() +{ + QString searchPath = ui->profilesDirEdit->text(); + searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + + QString temp = QFileDialog::getExistingDirectory(parentWidget(), QObject::tr("Select profiles directory"), searchPath); + if (!temp.isEmpty()) { + ui->profilesDirEdit->setText(temp); + } +} + +void PathsTab::on_browseOverwriteDirBtn_clicked() +{ + QString searchPath = ui->overwriteDirEdit->text(); + searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + + QString temp = QFileDialog::getExistingDirectory(parentWidget(), QObject::tr("Select overwrite directory"), searchPath); + if (!temp.isEmpty()) { + ui->overwriteDirEdit->setText(temp); + } +} + +void PathsTab::on_browseGameDirBtn_clicked() +{ + QFileInfo oldGameExe(ui->managedGameDirEdit->text()); + + QString temp = QFileDialog::getOpenFileName(parentWidget(), QObject::tr("Select game executable"), oldGameExe.absolutePath(), oldGameExe.fileName()); + if (!temp.isEmpty()) { + ui->managedGameDirEdit->setText(temp); + } +} + +void PathsTab::on_baseDirEdit_editingFinished() +{ + normalizePath(ui->baseDirEdit); +} + +void PathsTab::on_downloadDirEdit_editingFinished() +{ + normalizePath(ui->downloadDirEdit); +} + +void PathsTab::on_modDirEdit_editingFinished() +{ + normalizePath(ui->modDirEdit); +} + +void PathsTab::on_cacheDirEdit_editingFinished() +{ + normalizePath(ui->cacheDirEdit); +} + +void PathsTab::on_profilesDirEdit_editingFinished() +{ + normalizePath(ui->profilesDirEdit); +} + +void PathsTab::on_overwriteDirEdit_editingFinished() +{ + normalizePath(ui->overwriteDirEdit); +} + +void PathsTab::normalizePath(QLineEdit *lineEdit) +{ + QString text = lineEdit->text(); + while (text.endsWith('/') || text.endsWith('\\')) { + text.chop(1); + } + lineEdit->setText(text); +} diff --git a/src/settingsdialogpaths.h b/src/settingsdialogpaths.h new file mode 100644 index 00000000..dac402b1 --- /dev/null +++ b/src/settingsdialogpaths.h @@ -0,0 +1,33 @@ +#ifndef SETTINGSDIALOGPATHS_H +#define SETTINGSDIALOGPATHS_H + +#include "settings.h" +#include "settingsdialog.h" + +class PathsTab : public SettingsTab +{ +public: + PathsTab(Settings *parent, SettingsDialog &dialog); + + void update(); + +private: + void on_browseBaseDirBtn_clicked(); + void on_browseCacheDirBtn_clicked(); + void on_browseDownloadDirBtn_clicked(); + void on_browseGameDirBtn_clicked(); + void on_browseModDirBtn_clicked(); + void on_browseOverwriteDirBtn_clicked(); + void on_browseProfilesDirBtn_clicked(); + + void on_baseDirEdit_editingFinished(); + void on_cacheDirEdit_editingFinished(); + void on_downloadDirEdit_editingFinished(); + void on_modDirEdit_editingFinished(); + void on_overwriteDirEdit_editingFinished(); + void on_profilesDirEdit_editingFinished(); + + void normalizePath(QLineEdit *lineEdit); +}; + +#endif // SETTINGSDIALOGPATHS_H -- cgit v1.3.1 From af95b3b8637d28517f69a70f13b901cc7f43d121 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 09:31:38 -0400 Subject: renamed tab classes, clashing with mod info dialog split nexus tab --- src/CMakeLists.txt | 3 + src/settings.cpp | 114 +------------ src/settings.h | 16 -- src/settingsdialog.cpp | 278 +------------------------------ src/settingsdialog.h | 34 +--- src/settingsdialoggeneral.cpp | 30 ++-- src/settingsdialoggeneral.h | 4 +- src/settingsdialognexus.cpp | 374 ++++++++++++++++++++++++++++++++++++++++++ src/settingsdialognexus.h | 40 +++++ src/settingsdialogpaths.cpp | 32 ++-- src/settingsdialogpaths.h | 4 +- 11 files changed, 460 insertions(+), 469 deletions(-) create mode 100644 src/settingsdialognexus.cpp create mode 100644 src/settingsdialognexus.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 98d59996..b2407e17 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,7 @@ SET(organizer_SRCS singleinstance.cpp settingsdialog.cpp settingsdialoggeneral.cpp + settingsdialognexus.cpp settingsdialogpaths.cpp settings.cpp selfupdater.cpp @@ -154,6 +155,7 @@ SET(organizer_HDRS singleinstance.h settingsdialog.h settingsdialoggeneral.h + settingsdialognexus.h settingsdialogpaths.h settings.h selfupdater.h @@ -436,6 +438,7 @@ set(settings settings settingsdialog settingsdialoggeneral + settingsdialognexus settingsdialogpaths ) diff --git a/src/settings.cpp b/src/settings.cpp index bed8e789..bded470c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -23,6 +23,7 @@ along with Mod Organizer. If not, see . #include "serverinfo.h" #include "settingsdialog.h" #include "settingsdialoggeneral.h" +#include "settingsdialognexus.h" #include "settingsdialogpaths.h" #include "versioninfo.h" #include "appconfig.h" @@ -69,19 +70,6 @@ along with Mod Organizer. If not, see . using namespace MOBase; -template -class QListWidgetItemEx : public QListWidgetItem { -public: - QListWidgetItemEx(const QString &text, int sortRole = Qt::DisplayRole, QListWidget *parent = 0, int type = Type) - : QListWidgetItem(text, parent, type), m_SortRole(sortRole) {} - - virtual bool operator< ( const QListWidgetItem & other ) const { - return this->data(m_SortRole).value() < other.data(m_SortRole).value(); - } -private: - int m_SortRole; -}; - SettingsTab::SettingsTab(Settings *m_parent, SettingsDialog &m_dialog) : m_parent(m_parent) @@ -688,10 +676,10 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) std::vector> tabs; - tabs.push_back(std::unique_ptr(new GeneralTab(this, dialog))); - tabs.push_back(std::unique_ptr(new PathsTab(this, dialog))); + tabs.push_back(std::unique_ptr(new GeneralSettingsTab(this, dialog))); + tabs.push_back(std::unique_ptr(new PathsSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new DiagnosticsTab(this, dialog))); - tabs.push_back(std::unique_ptr(new NexusTab(this, dialog))); + tabs.push_back(std::unique_ptr(new NexusSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new SteamTab(this, dialog))); tabs.push_back(std::unique_ptr(new PluginsTab(this, dialog))); tabs.push_back(std::unique_ptr(new WorkaroundsTab(this, dialog))); @@ -781,100 +769,6 @@ void Settings::DiagnosticsTab::update() m_Settings.setValue("Settings/crash_dumps_max", m_dumpsMaxEdit->value()); } -void Settings::DiagnosticsTab::setLevelsBox() -{ - m_logLevelBox->clear(); - - m_logLevelBox->addItem(tr("Debug"), log::Debug); - m_logLevelBox->addItem(tr("Info (recommended)"), log::Info); - m_logLevelBox->addItem(tr("Warning"), log::Warning); - m_logLevelBox->addItem(tr("Error"), log::Error); - - for (int i=0; icount(); ++i) { - if (m_logLevelBox->itemData(i) == m_parent->logLevel()) { - m_logLevelBox->setCurrentIndex(i); - break; - } - } -} - -Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) - : SettingsTab(parent, dialog) - , m_offlineBox(dialog.findChild("offlineBox")) - , m_proxyBox(dialog.findChild("proxyBox")) - , m_knownServersList(dialog.findChild("knownServersList")) - , m_preferredServersList( - dialog.findChild("preferredServersList")) - , m_endorsementBox(dialog.findChild("endorsementBox")) - , m_hideAPICounterBox(dialog.findChild("hideAPICounterBox")) -{ - m_offlineBox->setChecked(parent->offlineMode()); - m_proxyBox->setChecked(parent->useProxy()); - m_endorsementBox->setChecked(parent->endorsementIntegration()); - m_hideAPICounterBox->setChecked(parent->hideAPICounter()); - - // display server preferences - m_Settings.beginGroup("Servers"); - for (const QString &key : m_Settings.childKeys()) { - QVariantMap val = m_Settings.value(key).toMap(); - QString descriptor = key; - if (!descriptor.compare("CDN", Qt::CaseInsensitive)) { - descriptor += QStringLiteral(" (automatic)"); - } - if (val.contains("downloadSpeed") && val.contains("downloadCount") && (val["downloadCount"].toInt() > 0)) { - int bps = static_cast(val["downloadSpeed"].toDouble() / val["downloadCount"].toInt()); - descriptor += QString(" (%1 kbps)").arg(bps / 1024); - } - - QListWidgetItem *newItem = new QListWidgetItemEx(descriptor, Qt::UserRole + 1); - - newItem->setData(Qt::UserRole, key); - newItem->setData(Qt::UserRole + 1, val["preferred"].toInt()); - if (val["preferred"].toInt() > 0) { - m_preferredServersList->addItem(newItem); - } else { - m_knownServersList->addItem(newItem); - } - m_preferredServersList->sortItems(Qt::DescendingOrder); - } - m_Settings.endGroup(); -} - -void Settings::NexusTab::update() -{ - /* - if (m_loginCheckBox->isChecked()) { - m_Settings.setValue("Settings/nexus_login", true); - m_Settings.setValue("Settings/nexus_username", m_usernameEdit->text()); - m_Settings.setValue("Settings/nexus_password", obfuscate(m_passwordEdit->text())); - } else { - m_Settings.setValue("Settings/nexus_login", false); - m_Settings.remove("Settings/nexus_username"); - m_Settings.remove("Settings/nexus_password"); - } - */ - m_Settings.setValue("Settings/offline_mode", m_offlineBox->isChecked()); - m_Settings.setValue("Settings/use_proxy", m_proxyBox->isChecked()); - m_Settings.setValue("Settings/endorsement_integration", m_endorsementBox->isChecked()); - m_Settings.setValue("Settings/hide_api_counter", m_hideAPICounterBox->isChecked()); - - // store server preference - m_Settings.beginGroup("Servers"); - for (int i = 0; i < m_knownServersList->count(); ++i) { - QString key = m_knownServersList->item(i)->data(Qt::UserRole).toString(); - QVariantMap val = m_Settings.value(key).toMap(); - val["preferred"] = 0; - m_Settings.setValue(key, val); - } - int count = m_preferredServersList->count(); - for (int i = 0; i < count; ++i) { - QString key = m_preferredServersList->item(i)->data(Qt::UserRole).toString(); - QVariantMap val = m_Settings.value(key).toMap(); - val["preferred"] = count - i; - m_Settings.setValue(key, val); - } - m_Settings.endGroup(); -} Settings::SteamTab::SteamTab(Settings *m_parent, SettingsDialog &m_dialog) : SettingsTab(m_parent, m_dialog) diff --git a/src/settings.h b/src/settings.h index 6f75562b..b9383ce4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -461,22 +461,6 @@ private: void setLevelsBox(); }; - /** Display/store the configuration in the 'nexus' tab of the settings dialogue */ - class NexusTab : public SettingsTab - { - public: - NexusTab(Settings *m_parent, SettingsDialog &m_dialog); - void update(); - - private: - QCheckBox *m_offlineBox; - QCheckBox *m_proxyBox; - QListWidget *m_knownServersList; - QListWidget *m_preferredServersList; - QCheckBox *m_endorsementBox; - QCheckBox *m_hideAPICounterBox; - }; - /** Display/store the configuration in the 'steam' tab of the settings dialogue */ class SteamTab : public SettingsTab { diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index bfa285bf..6d5a8cc0 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -20,7 +20,6 @@ along with Mod Organizer. If not, see . #include "settingsdialog.h" #include "ui_settingsdialog.h" -#include "ui_nexusmanualkey.h" #include "categoriesdialog.h" #include "helper.h" #include "noeditdelegate.h" @@ -48,62 +47,14 @@ along with Mod Organizer. If not, see . using namespace MOBase; -class NexusManualKeyDialog : public QDialog -{ -public: - NexusManualKeyDialog(QWidget* parent) - : QDialog(parent), ui(new Ui::NexusManualKeyDialog) - { - ui->setupUi(this); - ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - - connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); }); - connect(ui->paste, &QPushButton::clicked, [&]{ paste(); }); - connect(ui->clear, &QPushButton::clicked, [&]{ clear(); }); - } - - void accept() override - { - m_key = ui->key->toPlainText(); - QDialog::accept(); - } - - const QString& key() const - { - return m_key; - } - - void openBrowser() - { - shell::OpenLink(QUrl("https://www.nexusmods.com/users/myaccount?tab=api")); - } - - void paste() - { - const auto text = QApplication::clipboard()->text(); - if (!text.isEmpty()) { - ui->key->setPlainText(text); - } - } - - void clear() - { - ui->key->clear(); - } - -private: - std::unique_ptr ui; - QString m_key; -}; - SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* settings, QWidget *parent) : TutorableDialog("SettingsDialog", parent) , ui(new Ui::SettingsDialog) , m_settings(settings) , m_PluginContainer(pluginContainer) - , m_keyChanged(false) , m_GeometriesReset(false) + , m_keyChanged(false) { ui->setupUi(this); ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}"); @@ -111,8 +62,6 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* setti QShortcut *delShortcut = new QShortcut( QKeySequence(Qt::Key_Delete), ui->pluginBlacklist); connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem())); - - updateNexusState(); } SettingsDialog::~SettingsDialog() @@ -200,220 +149,6 @@ void SettingsDialog::on_bsaDateBtn_clicked() dir.absolutePath().toStdWString()); } -void SettingsDialog::on_nexusConnect_clicked() -{ - if (m_nexusLogin && m_nexusLogin->isActive()) { - m_nexusLogin->cancel(); - return; - } - - if (!m_nexusLogin) { - m_nexusLogin.reset(new NexusSSOLogin); - - m_nexusLogin->keyChanged = [&](auto&& s){ - onSSOKeyChanged(s); - }; - - m_nexusLogin->stateChanged = [&](auto&& s, auto&& e){ - onSSOStateChanged(s, e); - }; - } - - ui->nexusLog->clear(); - m_nexusLogin->start(); - updateNexusState(); -} - -void SettingsDialog::on_nexusManualKey_clicked() -{ - if (m_nexusValidator && m_nexusValidator->isActive()) { - m_nexusValidator->cancel(); - return; - } - - NexusManualKeyDialog dialog(this); - if (dialog.exec() != QDialog::Accepted) { - return; - } - - const auto key = dialog.key(); - if (key.isEmpty()) { - clearKey(); - return; - } - - ui->nexusLog->clear(); - validateKey(key); -} - -void SettingsDialog::on_nexusDisconnect_clicked() -{ - clearKey(); - ui->nexusLog->clear(); - addNexusLog(tr("Disconnected.")); -} - -void SettingsDialog::validateKey(const QString& key) -{ - if (!m_nexusValidator) { - m_nexusValidator.reset(new NexusKeyValidator( - *NexusInterface::instance(m_PluginContainer)->getAccessManager())); - - m_nexusValidator->stateChanged = [&](auto&& s, auto&& e){ - onValidatorStateChanged(s, e); - }; - - m_nexusValidator->finished = [&](auto&& user) { - onValidatorFinished(user); - }; - } - - addNexusLog(tr("Checking API key...")); - m_nexusValidator->start(key); -} - -void SettingsDialog::onSSOKeyChanged(const QString& key) -{ - if (key.isEmpty()) { - clearKey(); - } else { - addNexusLog(tr("Received API key.")); - validateKey(key); - } -} - -void SettingsDialog::onSSOStateChanged(NexusSSOLogin::States s, const QString& e) -{ - if (s != NexusSSOLogin::Finished) { - // finished state is handled in onSSOKeyChanged() - const auto log = NexusSSOLogin::stateToString(s, e); - - for (auto&& line : log.split("\n")) { - addNexusLog(line); - } - } - - updateNexusState(); -} - -void SettingsDialog::onValidatorStateChanged( - NexusKeyValidator::States s, const QString& e) -{ - if (s != NexusKeyValidator::Finished) { - // finished state is handled in onValidatorFinished() - const auto log = NexusKeyValidator::stateToString(s, e); - - for (auto&& line : log.split("\n")) { - addNexusLog(line); - } - } - - updateNexusState(); -} - -void SettingsDialog::onValidatorFinished(const APIUserAccount& user) -{ - NexusInterface::instance(m_PluginContainer)->setUserAccount(user); - - if (!user.apiKey().isEmpty()) { - if (setKey(user.apiKey())) { - addNexusLog(tr("Linked with Nexus successfully.")); - } - } -} - -void SettingsDialog::addNexusLog(const QString& s) -{ - ui->nexusLog->addItem(s); - ui->nexusLog->scrollToBottom(); -} - -bool SettingsDialog::setKey(const QString& key) -{ - m_keyChanged = true; - const bool ret = m_settings->setNexusApiKey(key); - updateNexusState(); - return ret; -} - -bool SettingsDialog::clearKey() -{ - m_keyChanged = true; - const auto ret = m_settings->clearNexusApiKey(); - - NexusInterface::instance(m_PluginContainer)->getAccessManager()->clearApiKey(); - updateNexusState(); - - return ret; -} - -void SettingsDialog::updateNexusState() -{ - updateNexusButtons(); - updateNexusData(); -} - -void SettingsDialog::updateNexusButtons() -{ - if (m_nexusLogin && m_nexusLogin->isActive()) { - // api key is in the process of being retrieved - ui->nexusConnect->setText(tr("Cancel")); - ui->nexusConnect->setEnabled(true); - ui->nexusDisconnect->setEnabled(false); - ui->nexusManualKey->setText(tr("Enter API Key Manually")); - ui->nexusManualKey->setEnabled(false); - } - else if (m_nexusValidator && m_nexusValidator->isActive()) { - // api key is in the process of being tested - ui->nexusConnect->setText(tr("Connect to Nexus")); - ui->nexusConnect->setEnabled(false); - ui->nexusDisconnect->setEnabled(false); - ui->nexusManualKey->setText(tr("Cancel")); - ui->nexusManualKey->setEnabled(true); - } - else if (m_settings->hasNexusApiKey()) { - // api key is present - ui->nexusConnect->setText(tr("Connect to Nexus")); - ui->nexusConnect->setEnabled(false); - ui->nexusDisconnect->setEnabled(true); - ui->nexusManualKey->setText(tr("Enter API Key Manually")); - ui->nexusManualKey->setEnabled(false); - } else { - // api key not present - ui->nexusConnect->setText(tr("Connect to Nexus")); - ui->nexusConnect->setEnabled(true); - ui->nexusDisconnect->setEnabled(false); - ui->nexusManualKey->setText(tr("Enter API Key Manually")); - ui->nexusManualKey->setEnabled(true); - } -} - -void SettingsDialog::updateNexusData() -{ - const auto user = NexusInterface::instance(m_PluginContainer) - ->getAPIUserAccount(); - - if (user.isValid()) { - ui->nexusUserID->setText(user.id()); - ui->nexusName->setText(user.name()); - ui->nexusAccount->setText(localizedUserAccountType(user.type())); - - ui->nexusDailyRequests->setText(QString("%1/%2") - .arg(user.limits().remainingDailyRequests) - .arg(user.limits().maxDailyRequests)); - - ui->nexusHourlyRequests->setText(QString("%1/%2") - .arg(user.limits().remainingHourlyRequests) - .arg(user.limits().maxHourlyRequests)); - } else { - ui->nexusUserID->setText(tr("N/A")); - ui->nexusName->setText(tr("N/A")); - ui->nexusAccount->setText(tr("N/A")); - ui->nexusDailyRequests->setText(tr("N/A")); - ui->nexusHourlyRequests->setText(tr("N/A")); - } -} - void SettingsDialog::storeSettings(QListWidgetItem *pluginItem) { if (pluginItem != nullptr) { @@ -470,17 +205,6 @@ void SettingsDialog::deleteBlacklistItem() ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row()); } -void SettingsDialog::on_associateButton_clicked() -{ - Settings::instance().registerAsNXMHandler(true); -} - -void SettingsDialog::on_clearCacheButton_clicked() -{ - QDir(Settings::instance().getCacheDirectory()).removeRecursively(); - NexusInterface::instance(m_PluginContainer)->clearCache(); -} - void SettingsDialog::on_resetGeometryBtn_clicked() { m_GeometriesReset = true; diff --git a/src/settingsdialog.h b/src/settingsdialog.h index 68e72529..df5d0ad8 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -54,18 +54,15 @@ public: */ QString getColoredButtonStyleSheet() const; + // temp Ui::SettingsDialog *ui; + bool m_keyChanged; + PluginContainer *m_PluginContainer; public slots: - virtual void accept(); -signals: - - void retryApiConnection(); - private: - void storeSettings(QListWidgetItem *pluginItem); public: @@ -75,13 +72,8 @@ public: bool getApiKeyChanged(); private slots: - void on_associateButton_clicked(); void on_bsaDateBtn_clicked(); - void on_clearCacheButton_clicked(); void on_execBlacklistBtn_clicked(); - void on_nexusConnect_clicked(); - void on_nexusDisconnect_clicked(); - void on_nexusManualKey_clicked(); void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); void on_resetGeometryBtn_clicked(); @@ -89,30 +81,10 @@ private slots: private: Settings* m_settings; - PluginContainer *m_PluginContainer; bool m_GeometriesReset; - bool m_keyChanged; QString m_ExecutableBlacklist; - std::unique_ptr m_nexusLogin; - std::unique_ptr m_nexusValidator; - - void validateKey(const QString& key); - bool setKey(const QString& key); - bool clearKey(); - - void updateNexusState(); - void updateNexusButtons(); - void updateNexusData(); - - void onSSOKeyChanged(const QString& key); - void onSSOStateChanged(NexusSSOLogin::States s, const QString& e); - - void onValidatorStateChanged(NexusKeyValidator::States s, const QString& e); - void onValidatorFinished(const APIUserAccount& user); - - void addNexusLog(const QString& s); }; #endif // SETTINGSDIALOG_H diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index b22b04fd..cd98dfdc 100644 --- a/src/settingsdialoggeneral.cpp +++ b/src/settingsdialoggeneral.cpp @@ -6,7 +6,7 @@ using MOBase::QuestionBoxMemory; -GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog) +GeneralSettingsTab::GeneralSettingsTab(Settings *m_parent, SettingsDialog &m_dialog) : SettingsTab(m_parent, m_dialog) { addLanguages(); @@ -87,7 +87,7 @@ GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog) QObject::connect(ui->resetDialogsButton, &QPushButton::clicked, [&]{ on_resetDialogsButton_clicked(); }); } -void GeneralTab::update() +void GeneralSettingsTab::update() { QString oldLanguage = m_parent->language(); QString newLanguage = ui->languageBox->itemData(ui->languageBox->currentIndex()).toString(); @@ -115,7 +115,7 @@ void GeneralTab::update() m_Settings.setValue("Settings/colorSeparatorScrollbars", ui->colorSeparatorsBox->isChecked()); } -void GeneralTab::addLanguages() +void GeneralSettingsTab::addLanguages() { std::vector> languages; @@ -150,7 +150,7 @@ void GeneralTab::addLanguages() } } -void GeneralTab::addStyles() +void GeneralSettingsTab::addStyles() { ui->styleBox->addItem("None", ""); ui->styleBox->addItem("Fusion", "Fusion"); @@ -163,12 +163,12 @@ void GeneralTab::addStyles() } } -void GeneralTab::resetDialogs() +void GeneralSettingsTab::resetDialogs() { QuestionBoxMemory::resetDialogs(); } -void GeneralTab::setButtonColor(QPushButton *button, const QColor &color) +void GeneralSettingsTab::setButtonColor(QPushButton *button, const QColor &color) { button->setStyleSheet( QString("QPushButton {" @@ -185,7 +185,7 @@ void GeneralTab::setButtonColor(QPushButton *button, const QColor &color) ); }; -void GeneralTab::on_containsBtn_clicked() +void GeneralSettingsTab::on_containsBtn_clicked() { QColor result = QColorDialog::getColor(m_ContainsColor, parentWidget(), "Color Picker: Mod contains selected plugin", QColorDialog::ShowAlphaChannel); if (result.isValid()) { @@ -194,7 +194,7 @@ void GeneralTab::on_containsBtn_clicked() } } -void GeneralTab::on_containedBtn_clicked() +void GeneralSettingsTab::on_containedBtn_clicked() { QColor result = QColorDialog::getColor(m_ContainedColor, parentWidget(), "ColorPicker: Plugin is Contained in selected Mod", QColorDialog::ShowAlphaChannel); if (result.isValid()) { @@ -203,7 +203,7 @@ void GeneralTab::on_containedBtn_clicked() } } -void GeneralTab::on_overwrittenBtn_clicked() +void GeneralSettingsTab::on_overwrittenBtn_clicked() { QColor result = QColorDialog::getColor(m_OverwrittenColor, parentWidget(), "ColorPicker: Is overwritten (loose files)", QColorDialog::ShowAlphaChannel); if (result.isValid()) { @@ -212,7 +212,7 @@ void GeneralTab::on_overwrittenBtn_clicked() } } -void GeneralTab::on_overwritingBtn_clicked() +void GeneralSettingsTab::on_overwritingBtn_clicked() { QColor result = QColorDialog::getColor(m_OverwritingColor, parentWidget(), "ColorPicker: Is overwriting (loose files)", QColorDialog::ShowAlphaChannel); if (result.isValid()) { @@ -221,7 +221,7 @@ void GeneralTab::on_overwritingBtn_clicked() } } -void GeneralTab::on_overwrittenArchiveBtn_clicked() +void GeneralSettingsTab::on_overwrittenArchiveBtn_clicked() { QColor result = QColorDialog::getColor(m_OverwrittenArchiveColor, parentWidget(), "ColorPicker: Is overwritten (archive files)", QColorDialog::ShowAlphaChannel); if (result.isValid()) { @@ -230,7 +230,7 @@ void GeneralTab::on_overwrittenArchiveBtn_clicked() } } -void GeneralTab::on_overwritingArchiveBtn_clicked() +void GeneralSettingsTab::on_overwritingArchiveBtn_clicked() { QColor result = QColorDialog::getColor(m_OverwritingArchiveColor, parentWidget(), "ColorPicker: Is overwriting (archive files)", QColorDialog::ShowAlphaChannel); if (result.isValid()) { @@ -239,7 +239,7 @@ void GeneralTab::on_overwritingArchiveBtn_clicked() } } -void GeneralTab::on_resetColorsBtn_clicked() +void GeneralSettingsTab::on_resetColorsBtn_clicked() { m_OverwritingColor = QColor(255, 0, 0, 64); m_OverwrittenColor = QColor(0, 255, 0, 64); @@ -256,7 +256,7 @@ void GeneralTab::on_resetColorsBtn_clicked() setButtonColor(ui->containedBtn, m_ContainedColor); } -void GeneralTab::on_resetDialogsButton_clicked() +void GeneralSettingsTab::on_resetDialogsButton_clicked() { if (QMessageBox::question(parentWidget(), QObject::tr("Confirm?"), QObject::tr("This will make all dialogs show up again where you checked the \"Remember selection\"-box. Continue?"), @@ -265,7 +265,7 @@ void GeneralTab::on_resetDialogsButton_clicked() } } -void GeneralTab::on_categoriesBtn_clicked() +void GeneralSettingsTab::on_categoriesBtn_clicked() { CategoriesDialog dialog(parentWidget()); if (dialog.exec() == QDialog::Accepted) { diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h index 1f1b4637..c7fcae36 100644 --- a/src/settingsdialoggeneral.h +++ b/src/settingsdialoggeneral.h @@ -4,10 +4,10 @@ #include "settingsdialog.h" #include "settings.h" -class GeneralTab : public SettingsTab +class GeneralSettingsTab : public SettingsTab { public: - GeneralTab(Settings *m_parent, SettingsDialog &m_dialog); + GeneralSettingsTab(Settings *m_parent, SettingsDialog &m_dialog); void update(); diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp new file mode 100644 index 00000000..7d4414fd --- /dev/null +++ b/src/settingsdialognexus.cpp @@ -0,0 +1,374 @@ +#include "settingsdialognexus.h" +#include "ui_settingsdialog.h" +#include "ui_nexusmanualkey.h" +#include "nexusinterface.h" +#include + +namespace shell = MOBase::shell; + +template +class ServerItem : public QListWidgetItem { +public: + ServerItem(const QString &text, int sortRole = Qt::DisplayRole, QListWidget *parent = 0, int type = Type) + : QListWidgetItem(text, parent, type), m_SortRole(sortRole) {} + + virtual bool operator< ( const QListWidgetItem & other ) const { + return this->data(m_SortRole).value() < other.data(m_SortRole).value(); + } +private: + int m_SortRole; +}; + + +class NexusManualKeyDialog : public QDialog +{ +public: + NexusManualKeyDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::NexusManualKeyDialog) + { + ui->setupUi(this); + ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + + connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); }); + connect(ui->paste, &QPushButton::clicked, [&]{ paste(); }); + connect(ui->clear, &QPushButton::clicked, [&]{ clear(); }); + } + + void accept() override + { + m_key = ui->key->toPlainText(); + QDialog::accept(); + } + + const QString& key() const + { + return m_key; + } + + void openBrowser() + { + shell::OpenLink(QUrl("https://www.nexusmods.com/users/myaccount?tab=api")); + } + + void paste() + { + const auto text = QApplication::clipboard()->text(); + if (!text.isEmpty()) { + ui->key->setPlainText(text); + } + } + + void clear() + { + ui->key->clear(); + } + +private: + std::unique_ptr ui; + QString m_key; +}; + + +NexusSettingsTab::NexusSettingsTab(Settings *parent, SettingsDialog &dialog) + : SettingsTab(parent, dialog) +{ + ui->offlineBox->setChecked(parent->offlineMode()); + ui->proxyBox->setChecked(parent->useProxy()); + ui->endorsementBox->setChecked(parent->endorsementIntegration()); + ui->hideAPICounterBox->setChecked(parent->hideAPICounter()); + + // display server preferences + m_Settings.beginGroup("Servers"); + for (const QString &key : m_Settings.childKeys()) { + QVariantMap val = m_Settings.value(key).toMap(); + QString descriptor = key; + if (!descriptor.compare("CDN", Qt::CaseInsensitive)) { + descriptor += QStringLiteral(" (automatic)"); + } + if (val.contains("downloadSpeed") && val.contains("downloadCount") && (val["downloadCount"].toInt() > 0)) { + int bps = static_cast(val["downloadSpeed"].toDouble() / val["downloadCount"].toInt()); + descriptor += QString(" (%1 kbps)").arg(bps / 1024); + } + + QListWidgetItem *newItem = new ServerItem(descriptor, Qt::UserRole + 1); + + newItem->setData(Qt::UserRole, key); + newItem->setData(Qt::UserRole + 1, val["preferred"].toInt()); + if (val["preferred"].toInt() > 0) { + ui->preferredServersList->addItem(newItem); + } else { + ui->knownServersList->addItem(newItem); + } + ui->preferredServersList->sortItems(Qt::DescendingOrder); + } + m_Settings.endGroup(); + + QObject::connect(ui->nexusConnect, &QPushButton::clicked, [&]{ on_nexusConnect_clicked(); }); + QObject::connect(ui->nexusManualKey, &QPushButton::clicked, [&]{ on_nexusManualKey_clicked(); }); + QObject::connect(ui->nexusDisconnect, &QPushButton::clicked, [&]{ on_nexusDisconnect_clicked(); }); + QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ on_clearCacheButton_clicked(); }); + QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ on_associateButton_clicked(); }); + + updateNexusState(); +} + +void NexusSettingsTab::update() +{ + /* + if (m_loginCheckBox->isChecked()) { + m_Settings.setValue("Settings/nexus_login", true); + m_Settings.setValue("Settings/nexus_username", m_usernameEdit->text()); + m_Settings.setValue("Settings/nexus_password", obfuscate(m_passwordEdit->text())); + } else { + m_Settings.setValue("Settings/nexus_login", false); + m_Settings.remove("Settings/nexus_username"); + m_Settings.remove("Settings/nexus_password"); + } + */ + m_Settings.setValue("Settings/offline_mode", ui->offlineBox->isChecked()); + m_Settings.setValue("Settings/use_proxy", ui->proxyBox->isChecked()); + m_Settings.setValue("Settings/endorsement_integration", ui->endorsementBox->isChecked()); + m_Settings.setValue("Settings/hide_api_counter", ui->hideAPICounterBox->isChecked()); + + // store server preference + m_Settings.beginGroup("Servers"); + for (int i = 0; i < ui->knownServersList->count(); ++i) { + QString key = ui->knownServersList->item(i)->data(Qt::UserRole).toString(); + QVariantMap val = m_Settings.value(key).toMap(); + val["preferred"] = 0; + m_Settings.setValue(key, val); + } + int count = ui->preferredServersList->count(); + for (int i = 0; i < count; ++i) { + QString key = ui->preferredServersList->item(i)->data(Qt::UserRole).toString(); + QVariantMap val = m_Settings.value(key).toMap(); + val["preferred"] = count - i; + m_Settings.setValue(key, val); + } + m_Settings.endGroup(); +} + +void NexusSettingsTab::on_nexusConnect_clicked() +{ + if (m_nexusLogin && m_nexusLogin->isActive()) { + m_nexusLogin->cancel(); + return; + } + + if (!m_nexusLogin) { + m_nexusLogin.reset(new NexusSSOLogin); + + m_nexusLogin->keyChanged = [&](auto&& s){ + onSSOKeyChanged(s); + }; + + m_nexusLogin->stateChanged = [&](auto&& s, auto&& e){ + onSSOStateChanged(s, e); + }; + } + + ui->nexusLog->clear(); + m_nexusLogin->start(); + updateNexusState(); +} + +void NexusSettingsTab::on_nexusManualKey_clicked() +{ + if (m_nexusValidator && m_nexusValidator->isActive()) { + m_nexusValidator->cancel(); + return; + } + + NexusManualKeyDialog dialog(parentWidget()); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + const auto key = dialog.key(); + if (key.isEmpty()) { + clearKey(); + return; + } + + ui->nexusLog->clear(); + validateKey(key); +} + +void NexusSettingsTab::on_nexusDisconnect_clicked() +{ + clearKey(); + ui->nexusLog->clear(); + addNexusLog(QObject::tr("Disconnected.")); +} + +void NexusSettingsTab::on_clearCacheButton_clicked() +{ + QDir(Settings::instance().getCacheDirectory()).removeRecursively(); + NexusInterface::instance(m_dialog.m_PluginContainer)->clearCache(); +} + +void NexusSettingsTab::on_associateButton_clicked() +{ + Settings::instance().registerAsNXMHandler(true); +} + +void NexusSettingsTab::validateKey(const QString& key) +{ + if (!m_nexusValidator) { + m_nexusValidator.reset(new NexusKeyValidator( + *NexusInterface::instance(m_dialog.m_PluginContainer)->getAccessManager())); + + m_nexusValidator->stateChanged = [&](auto&& s, auto&& e){ + onValidatorStateChanged(s, e); + }; + + m_nexusValidator->finished = [&](auto&& user) { + onValidatorFinished(user); + }; + } + + addNexusLog(QObject::tr("Checking API key...")); + m_nexusValidator->start(key); +} + +void NexusSettingsTab::onSSOKeyChanged(const QString& key) +{ + if (key.isEmpty()) { + clearKey(); + } else { + addNexusLog(QObject::tr("Received API key.")); + validateKey(key); + } +} + +void NexusSettingsTab::onSSOStateChanged(NexusSSOLogin::States s, const QString& e) +{ + if (s != NexusSSOLogin::Finished) { + // finished state is handled in onSSOKeyChanged() + const auto log = NexusSSOLogin::stateToString(s, e); + + for (auto&& line : log.split("\n")) { + addNexusLog(line); + } + } + + updateNexusState(); +} + +void NexusSettingsTab::onValidatorStateChanged( + NexusKeyValidator::States s, const QString& e) +{ + if (s != NexusKeyValidator::Finished) { + // finished state is handled in onValidatorFinished() + const auto log = NexusKeyValidator::stateToString(s, e); + + for (auto&& line : log.split("\n")) { + addNexusLog(line); + } + } + + updateNexusState(); +} + +void NexusSettingsTab::onValidatorFinished(const APIUserAccount& user) +{ + NexusInterface::instance(m_dialog.m_PluginContainer)->setUserAccount(user); + + if (!user.apiKey().isEmpty()) { + if (setKey(user.apiKey())) { + addNexusLog(QObject::tr("Linked with Nexus successfully.")); + } + } +} + +void NexusSettingsTab::addNexusLog(const QString& s) +{ + ui->nexusLog->addItem(s); + ui->nexusLog->scrollToBottom(); +} + +bool NexusSettingsTab::setKey(const QString& key) +{ + m_dialog.m_keyChanged = true; + const bool ret = m_parent->setNexusApiKey(key); + updateNexusState(); + return ret; +} + +bool NexusSettingsTab::clearKey() +{ + m_dialog.m_keyChanged = true; + const auto ret = m_parent->clearNexusApiKey(); + + NexusInterface::instance(m_dialog.m_PluginContainer)->getAccessManager()->clearApiKey(); + updateNexusState(); + + return ret; +} + +void NexusSettingsTab::updateNexusState() +{ + updateNexusButtons(); + updateNexusData(); +} + +void NexusSettingsTab::updateNexusButtons() +{ + if (m_nexusLogin && m_nexusLogin->isActive()) { + // api key is in the process of being retrieved + ui->nexusConnect->setText(QObject::tr("Cancel")); + ui->nexusConnect->setEnabled(true); + ui->nexusDisconnect->setEnabled(false); + ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually")); + ui->nexusManualKey->setEnabled(false); + } + else if (m_nexusValidator && m_nexusValidator->isActive()) { + // api key is in the process of being tested + ui->nexusConnect->setText(QObject::tr("Connect to Nexus")); + ui->nexusConnect->setEnabled(false); + ui->nexusDisconnect->setEnabled(false); + ui->nexusManualKey->setText(QObject::tr("Cancel")); + ui->nexusManualKey->setEnabled(true); + } + else if (m_parent->hasNexusApiKey()) { + // api key is present + ui->nexusConnect->setText(QObject::tr("Connect to Nexus")); + ui->nexusConnect->setEnabled(false); + ui->nexusDisconnect->setEnabled(true); + ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually")); + ui->nexusManualKey->setEnabled(false); + } else { + // api key not present + ui->nexusConnect->setText(QObject::tr("Connect to Nexus")); + ui->nexusConnect->setEnabled(true); + ui->nexusDisconnect->setEnabled(false); + ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually")); + ui->nexusManualKey->setEnabled(true); + } +} + +void NexusSettingsTab::updateNexusData() +{ + const auto user = NexusInterface::instance(m_dialog.m_PluginContainer) + ->getAPIUserAccount(); + + if (user.isValid()) { + ui->nexusUserID->setText(user.id()); + ui->nexusName->setText(user.name()); + ui->nexusAccount->setText(localizedUserAccountType(user.type())); + + ui->nexusDailyRequests->setText(QString("%1/%2") + .arg(user.limits().remainingDailyRequests) + .arg(user.limits().maxDailyRequests)); + + ui->nexusHourlyRequests->setText(QString("%1/%2") + .arg(user.limits().remainingHourlyRequests) + .arg(user.limits().maxHourlyRequests)); + } else { + ui->nexusUserID->setText(QObject::tr("N/A")); + ui->nexusName->setText(QObject::tr("N/A")); + ui->nexusAccount->setText(QObject::tr("N/A")); + ui->nexusDailyRequests->setText(QObject::tr("N/A")); + ui->nexusHourlyRequests->setText(QObject::tr("N/A")); + } +} diff --git a/src/settingsdialognexus.h b/src/settingsdialognexus.h new file mode 100644 index 00000000..5c01f61f --- /dev/null +++ b/src/settingsdialognexus.h @@ -0,0 +1,40 @@ +#ifndef SETTINGSDIALOGNEXUS_H +#define SETTINGSDIALOGNEXUS_H + +#include "settings.h" +#include "settingsdialog.h" + +class NexusSettingsTab : public SettingsTab +{ +public: + NexusSettingsTab(Settings *m_parent, SettingsDialog &m_dialog); + void update(); + +private: + std::unique_ptr m_nexusLogin; + std::unique_ptr m_nexusValidator; + + void on_nexusConnect_clicked(); + void on_nexusManualKey_clicked(); + void on_nexusDisconnect_clicked(); + void on_clearCacheButton_clicked(); + void on_associateButton_clicked(); + + void validateKey(const QString& key); + bool setKey(const QString& key); + bool clearKey(); + + void updateNexusState(); + void updateNexusButtons(); + void updateNexusData(); + + void onSSOKeyChanged(const QString& key); + void onSSOStateChanged(NexusSSOLogin::States s, const QString& e); + + void onValidatorStateChanged(NexusKeyValidator::States s, const QString& e); + void onValidatorFinished(const APIUserAccount& user); + + void addNexusLog(const QString& s); +}; + +#endif // SETTINGSDIALOGNEXUS_H diff --git a/src/settingsdialogpaths.cpp b/src/settingsdialogpaths.cpp index 303d1562..6e8fe994 100644 --- a/src/settingsdialogpaths.cpp +++ b/src/settingsdialogpaths.cpp @@ -3,7 +3,7 @@ #include "appconfig.h" #include -PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) +PathsSettingsTab::PathsSettingsTab(Settings *parent, SettingsDialog &dialog) : SettingsTab(parent, dialog) { ui->baseDirEdit->setText(m_parent->getBaseDirectory()); @@ -38,7 +38,7 @@ PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) QObject::connect(ui->profilesDirEdit, &QLineEdit::editingFinished, [&]{ on_profilesDirEdit_editingFinished(); }); } -void PathsTab::update() +void PathsSettingsTab::update() { typedef std::tuple Directory; @@ -91,7 +91,7 @@ void PathsTab::update() } } -void PathsTab::on_browseBaseDirBtn_clicked() +void PathsSettingsTab::on_browseBaseDirBtn_clicked() { QString temp = QFileDialog::getExistingDirectory( parentWidget(), QObject::tr("Select base directory"), ui->baseDirEdit->text()); @@ -100,7 +100,7 @@ void PathsTab::on_browseBaseDirBtn_clicked() } } -void PathsTab::on_browseDownloadDirBtn_clicked() +void PathsSettingsTab::on_browseDownloadDirBtn_clicked() { QString searchPath = ui->downloadDirEdit->text(); searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); @@ -111,7 +111,7 @@ void PathsTab::on_browseDownloadDirBtn_clicked() } } -void PathsTab::on_browseModDirBtn_clicked() +void PathsSettingsTab::on_browseModDirBtn_clicked() { QString searchPath = ui->modDirEdit->text(); searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); @@ -122,7 +122,7 @@ void PathsTab::on_browseModDirBtn_clicked() } } -void PathsTab::on_browseCacheDirBtn_clicked() +void PathsSettingsTab::on_browseCacheDirBtn_clicked() { QString searchPath = ui->cacheDirEdit->text(); searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); @@ -133,7 +133,7 @@ void PathsTab::on_browseCacheDirBtn_clicked() } } -void PathsTab::on_browseProfilesDirBtn_clicked() +void PathsSettingsTab::on_browseProfilesDirBtn_clicked() { QString searchPath = ui->profilesDirEdit->text(); searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); @@ -144,7 +144,7 @@ void PathsTab::on_browseProfilesDirBtn_clicked() } } -void PathsTab::on_browseOverwriteDirBtn_clicked() +void PathsSettingsTab::on_browseOverwriteDirBtn_clicked() { QString searchPath = ui->overwriteDirEdit->text(); searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); @@ -155,7 +155,7 @@ void PathsTab::on_browseOverwriteDirBtn_clicked() } } -void PathsTab::on_browseGameDirBtn_clicked() +void PathsSettingsTab::on_browseGameDirBtn_clicked() { QFileInfo oldGameExe(ui->managedGameDirEdit->text()); @@ -165,37 +165,37 @@ void PathsTab::on_browseGameDirBtn_clicked() } } -void PathsTab::on_baseDirEdit_editingFinished() +void PathsSettingsTab::on_baseDirEdit_editingFinished() { normalizePath(ui->baseDirEdit); } -void PathsTab::on_downloadDirEdit_editingFinished() +void PathsSettingsTab::on_downloadDirEdit_editingFinished() { normalizePath(ui->downloadDirEdit); } -void PathsTab::on_modDirEdit_editingFinished() +void PathsSettingsTab::on_modDirEdit_editingFinished() { normalizePath(ui->modDirEdit); } -void PathsTab::on_cacheDirEdit_editingFinished() +void PathsSettingsTab::on_cacheDirEdit_editingFinished() { normalizePath(ui->cacheDirEdit); } -void PathsTab::on_profilesDirEdit_editingFinished() +void PathsSettingsTab::on_profilesDirEdit_editingFinished() { normalizePath(ui->profilesDirEdit); } -void PathsTab::on_overwriteDirEdit_editingFinished() +void PathsSettingsTab::on_overwriteDirEdit_editingFinished() { normalizePath(ui->overwriteDirEdit); } -void PathsTab::normalizePath(QLineEdit *lineEdit) +void PathsSettingsTab::normalizePath(QLineEdit *lineEdit) { QString text = lineEdit->text(); while (text.endsWith('/') || text.endsWith('\\')) { diff --git a/src/settingsdialogpaths.h b/src/settingsdialogpaths.h index dac402b1..f661b624 100644 --- a/src/settingsdialogpaths.h +++ b/src/settingsdialogpaths.h @@ -4,10 +4,10 @@ #include "settings.h" #include "settingsdialog.h" -class PathsTab : public SettingsTab +class PathsSettingsTab : public SettingsTab { public: - PathsTab(Settings *parent, SettingsDialog &dialog); + PathsSettingsTab(Settings *parent, SettingsDialog &dialog); void update(); -- cgit v1.3.1 From d91d0caba5fac3b2b27698a5e6ab4a9b60efbf53 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 09:47:09 -0400 Subject: removed incorrect warning that steam password is unencrypted split steam tab --- src/CMakeLists.txt | 3 +++ src/settings.cpp | 36 +++++----------------------- src/settings.h | 13 ---------- src/settingsdialog.ui | 58 ++++++++++++++++----------------------------- src/settingsdialogsteam.cpp | 17 +++++++++++++ src/settingsdialogsteam.h | 17 +++++++++++++ 6 files changed, 64 insertions(+), 80 deletions(-) create mode 100644 src/settingsdialogsteam.cpp create mode 100644 src/settingsdialogsteam.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b2407e17..a1adf2db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ SET(organizer_SRCS settingsdialoggeneral.cpp settingsdialognexus.cpp settingsdialogpaths.cpp + settingsdialogsteam.cpp settings.cpp selfupdater.cpp selectiondialog.cpp @@ -157,6 +158,7 @@ SET(organizer_HDRS settingsdialoggeneral.h settingsdialognexus.h settingsdialogpaths.h + settingsdialogsteam.h settings.h selfupdater.h selectiondialog.h @@ -440,6 +442,7 @@ set(settings settingsdialoggeneral settingsdialognexus settingsdialogpaths + settingsdialogsteam ) set(utilities diff --git a/src/settings.cpp b/src/settings.cpp index bded470c..26c9720a 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -25,6 +25,7 @@ along with Mod Organizer. If not, see . #include "settingsdialoggeneral.h" #include "settingsdialognexus.h" #include "settingsdialogpaths.h" +#include "settingsdialogsteam.h" #include "versioninfo.h" #include "appconfig.h" #include "organizercore.h" @@ -391,15 +392,10 @@ bool Settings::hasNexusApiKey() const bool Settings::getSteamLogin(QString &username, QString &password) const { - if (m_Settings.contains("Settings/steam_username")) { - QString tempPass = deObfuscate("steam_password"); - if (!tempPass.isEmpty()) { - username = m_Settings.value("Settings/steam_username").toString(); - password = tempPass; - return true; - } - } - return false; + username = m_Settings.value("Settings/steam_username", "").toString(); + password = deObfuscate("steam_password"); + + return !username.isEmpty() && !password.isEmpty(); } bool Settings::compactDownloads() const { @@ -680,7 +676,7 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) tabs.push_back(std::unique_ptr(new PathsSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new DiagnosticsTab(this, dialog))); tabs.push_back(std::unique_ptr(new NexusSettingsTab(this, dialog))); - tabs.push_back(std::unique_ptr(new SteamTab(this, dialog))); + tabs.push_back(std::unique_ptr(new SteamSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new PluginsTab(this, dialog))); tabs.push_back(std::unique_ptr(new WorkaroundsTab(this, dialog))); @@ -770,26 +766,6 @@ void Settings::DiagnosticsTab::update() } -Settings::SteamTab::SteamTab(Settings *m_parent, SettingsDialog &m_dialog) - : SettingsTab(m_parent, m_dialog) - , m_steamUserEdit(m_dialog.findChild("steamUserEdit")) - , m_steamPassEdit(m_dialog.findChild("steamPassEdit")) -{ - if (m_Settings.contains("Settings/steam_username")) { - m_steamUserEdit->setText(m_Settings.value("Settings/steam_username", "").toString()); - QString password = deObfuscate("steam_password"); - if (!password.isEmpty()) { - m_steamPassEdit->setText(password); - } - } -} - -void Settings::SteamTab::update() -{ - //FIXME this should be inlined here? - m_parent->setSteamLogin(m_steamUserEdit->text(), m_steamPassEdit->text()); -} - Settings::PluginsTab::PluginsTab(Settings *m_parent, SettingsDialog &m_dialog) : SettingsTab(m_parent, m_dialog) , m_pluginsList(m_dialog.findChild("pluginsList")) diff --git a/src/settings.h b/src/settings.h index b9383ce4..5bf705d1 100644 --- a/src/settings.h +++ b/src/settings.h @@ -461,19 +461,6 @@ private: void setLevelsBox(); }; - /** Display/store the configuration in the 'steam' tab of the settings dialogue */ - class SteamTab : public SettingsTab - { - public: - SteamTab(Settings *m_parent, SettingsDialog &m_dialog); - - void update(); - - private: - QLineEdit *m_steamUserEdit; - QLineEdit *m_steamPassEdit; - }; - /** Display/store the configuration in the 'plugins' tab of the settings dialogue */ class PluginsTab : public SettingsTab { diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 1e94bcde..3ad525e1 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -828,71 +828,55 @@ If you use pre-releases, never contact me directly by e-mail or via private mess Steam - + Username - - - - - - - Password - - - - - - - QLineEdit::Password - - - - - + + Qt::Vertical - QSizePolicy::Minimum + QSizePolicy::Expanding 20 - 40 + 232 - + + + + - If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure. + <html><head/><body><p>If you save your steam user ID and password here, they will be used when logging into steam.</p></body></html> true - - - - Qt::Vertical - - - QSizePolicy::Expanding + + + + Password - - - 20 - 232 - + + + + + + QLineEdit::Password - + diff --git a/src/settingsdialogsteam.cpp b/src/settingsdialogsteam.cpp new file mode 100644 index 00000000..34c2d76b --- /dev/null +++ b/src/settingsdialogsteam.cpp @@ -0,0 +1,17 @@ +#include "settingsdialogsteam.h" +#include "ui_settingsdialog.h" + +SteamSettingsTab::SteamSettingsTab(Settings *m_parent, SettingsDialog &m_dialog) + : SettingsTab(m_parent, m_dialog) +{ + QString username, password; + m_parent->getSteamLogin(username, password); + + ui->steamUserEdit->setText(username); + ui->steamPassEdit->setText(password); +} + +void SteamSettingsTab::update() +{ + m_parent->setSteamLogin(ui->steamUserEdit->text(), ui->steamPassEdit->text()); +} diff --git a/src/settingsdialogsteam.h b/src/settingsdialogsteam.h new file mode 100644 index 00000000..dbd85151 --- /dev/null +++ b/src/settingsdialogsteam.h @@ -0,0 +1,17 @@ +#ifndef SETTINGSDIALOGSTEAM_H +#define SETTINGSDIALOGSTEAM_H + +#include "settings.h" +#include "settingsdialog.h" + +class SteamSettingsTab : public SettingsTab +{ +public: + SteamSettingsTab(Settings *m_parent, SettingsDialog &m_dialog); + + void update(); + +private: +}; + +#endif // SETTINGSDIALOGSTEAM_H -- cgit v1.3.1 From 55eafd62dd3c96f363cde4537061e7f03ae8fd0a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 10:10:49 -0400 Subject: split plugins tab --- src/CMakeLists.txt | 3 ++ src/settings.cpp | 54 +++------------------ src/settings.h | 25 +++------- src/settingsdialog.cpp | 52 -------------------- src/settingsdialog.h | 4 -- src/settingsdialogplugins.cpp | 110 ++++++++++++++++++++++++++++++++++++++++++ src/settingsdialogplugins.h | 20 ++++++++ 7 files changed, 146 insertions(+), 122 deletions(-) create mode 100644 src/settingsdialogplugins.cpp create mode 100644 src/settingsdialogplugins.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a1adf2db..a8ded510 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ SET(organizer_SRCS settingsdialoggeneral.cpp settingsdialognexus.cpp settingsdialogpaths.cpp + settingsdialogplugins.cpp settingsdialogsteam.cpp settings.cpp selfupdater.cpp @@ -158,6 +159,7 @@ SET(organizer_HDRS settingsdialoggeneral.h settingsdialognexus.h settingsdialogpaths.h + settingsdialogplugins.h settingsdialogsteam.h settings.h selfupdater.h @@ -442,6 +444,7 @@ set(settings settingsdialoggeneral settingsdialognexus settingsdialogpaths + settingsdialogplugins settingsdialogsteam ) diff --git a/src/settings.cpp b/src/settings.cpp index 26c9720a..bc45b720 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -25,6 +25,7 @@ along with Mod Organizer. If not, see . #include "settingsdialoggeneral.h" #include "settingsdialognexus.h" #include "settingsdialogpaths.h" +#include "settingsdialogplugins.h" #include "settingsdialogsteam.h" #include "versioninfo.h" #include "appconfig.h" @@ -677,7 +678,7 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) tabs.push_back(std::unique_ptr(new DiagnosticsTab(this, dialog))); tabs.push_back(std::unique_ptr(new NexusSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new SteamSettingsTab(this, dialog))); - tabs.push_back(std::unique_ptr(new PluginsTab(this, dialog))); + tabs.push_back(std::unique_ptr(new PluginsSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new WorkaroundsTab(this, dialog))); @@ -687,6 +688,11 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) } if (dialog.exec() == QDialog::Accepted) { + + for (auto&& tab : tabs) { + tab->closing(); + } + // remember settings before change QMap before; m_Settings.beginGroup("Settings"); @@ -766,52 +772,6 @@ void Settings::DiagnosticsTab::update() } -Settings::PluginsTab::PluginsTab(Settings *m_parent, SettingsDialog &m_dialog) - : SettingsTab(m_parent, m_dialog) - , m_pluginsList(m_dialog.findChild("pluginsList")) - , m_pluginBlacklistList(m_dialog.findChild("pluginBlacklist")) -{ - // display plugin settings - QSet handledNames; - for (IPlugin *plugin : m_parent->m_Plugins) { - if (handledNames.contains(plugin->name())) - continue; - QListWidgetItem *listItem = new QListWidgetItem(plugin->name(), m_pluginsList); - listItem->setData(Qt::UserRole, QVariant::fromValue((void*)plugin)); - listItem->setData(Qt::UserRole + 1, m_parent->m_PluginSettings[plugin->name()]); - listItem->setData(Qt::UserRole + 2, m_parent->m_PluginDescriptions[plugin->name()]); - m_pluginsList->addItem(listItem); - handledNames.insert(plugin->name()); - } - - // display plugin blacklist - for (const QString &pluginName : m_parent->m_PluginBlacklist) { - m_pluginBlacklistList->addItem(pluginName); - } -} - -void Settings::PluginsTab::update() -{ - // transfer plugin settings to in-memory structure - for (int i = 0; i < m_pluginsList->count(); ++i) { - QListWidgetItem *item = m_pluginsList->item(i); - m_parent->m_PluginSettings[item->text()] = item->data(Qt::UserRole + 1).toMap(); - } - // store plugin settings on disc - for (auto iterPlugins = m_parent->m_PluginSettings.begin(); iterPlugins != m_parent->m_PluginSettings.end(); ++iterPlugins) { - for (auto iterSettings = iterPlugins->begin(); iterSettings != iterPlugins->end(); ++iterSettings) { - m_Settings.setValue("Plugins/" + iterPlugins.key() + "/" + iterSettings.key(), iterSettings.value()); - } - } - - // store plugin blacklist - m_parent->m_PluginBlacklist.clear(); - for (QListWidgetItem *item : m_pluginBlacklistList->findItems("*", Qt::MatchWildcard)) { - m_parent->m_PluginBlacklist.insert(item->text()); - } - m_parent->writePluginBlacklist(); -} - Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent, SettingsDialog &m_dialog) : SettingsTab(m_parent, m_dialog) diff --git a/src/settings.h b/src/settings.h index 5bf705d1..5298103a 100644 --- a/src/settings.h +++ b/src/settings.h @@ -70,6 +70,7 @@ public: virtual ~SettingsTab(); virtual void update() = 0; + virtual void closing() {} protected: Settings *m_parent; @@ -426,8 +427,13 @@ public: */ bool colorSeparatorScrollbar() const; + // temp QSettings& settingsRef() { return m_Settings; } MOBase::IPluginGame const *gamePlugin() { return m_GamePlugin; } + QMap m_PluginSettings; + QMap m_PluginDescriptions; + QSet m_PluginBlacklist; + void writePluginBlacklist(); public slots: @@ -440,7 +446,6 @@ private: static QString deObfuscate(const QString key); void readPluginBlacklist(); - void writePluginBlacklist(); QString getConfigurablePath(const QString &key, const QString &def, bool resolve) const; @@ -461,19 +466,6 @@ private: void setLevelsBox(); }; - /** Display/store the configuration in the 'plugins' tab of the settings dialogue */ - class PluginsTab : public SettingsTab - { - public: - PluginsTab(Settings *m_parent, SettingsDialog &m_dialog); - - void update(); - - private: - QListWidget *m_pluginsList; - QListWidget *m_pluginBlacklistList; - }; - /** Display/store the configuration in the 'workarounds' tab of the settings dialogue */ class WorkaroundsTab : public SettingsTab { @@ -512,11 +504,6 @@ private: std::vector m_Plugins; - QMap m_PluginSettings; - QMap m_PluginDescriptions; - - QSet m_PluginBlacklist; - }; #endif // SETTINGS_H diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 6d5a8cc0..f43f7ae8 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -99,7 +99,6 @@ void SettingsDialog::accept() return; } - storeSettings(ui->pluginsList->currentItem()); TutorableDialog::accept(); } @@ -149,57 +148,6 @@ void SettingsDialog::on_bsaDateBtn_clicked() dir.absolutePath().toStdWString()); } -void SettingsDialog::storeSettings(QListWidgetItem *pluginItem) -{ - if (pluginItem != nullptr) { - QVariantMap settings = pluginItem->data(Qt::UserRole + 1).toMap(); - - for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) { - const QTreeWidgetItem *item = ui->pluginSettingsList->topLevelItem(i); - settings[item->text(0)] = item->data(1, Qt::DisplayRole); - } - - pluginItem->setData(Qt::UserRole + 1, settings); - } -} - -void SettingsDialog::on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - storeSettings(previous); - - ui->pluginSettingsList->clear(); - IPlugin *plugin = static_cast(current->data(Qt::UserRole).value()); - ui->authorLabel->setText(plugin->author()); - ui->versionLabel->setText(plugin->version().canonicalString()); - ui->descriptionLabel->setText(plugin->description()); - - QVariantMap settings = current->data(Qt::UserRole + 1).toMap(); - QVariantMap descriptions = current->data(Qt::UserRole + 2).toMap(); - ui->pluginSettingsList->setEnabled(settings.count() != 0); - for (auto iter = settings.begin(); iter != settings.end(); ++iter) { - QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(iter.key())); - QVariant value = *iter; - QString description; - { - auto descriptionIter = descriptions.find(iter.key()); - if (descriptionIter != descriptions.end()) { - description = descriptionIter->toString(); - } - } - - ui->pluginSettingsList->setItemDelegateForColumn(0, new NoEditDelegate()); - newItem->setData(1, Qt::DisplayRole, value); - newItem->setData(1, Qt::EditRole, value); - newItem->setToolTip(1, description); - - newItem->setFlags(newItem->flags() | Qt::ItemIsEditable); - ui->pluginSettingsList->addTopLevelItem(newItem); - } - - ui->pluginSettingsList->resizeColumnToContents(0); - ui->pluginSettingsList->resizeColumnToContents(1); -} - void SettingsDialog::deleteBlacklistItem() { ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row()); diff --git a/src/settingsdialog.h b/src/settingsdialog.h index df5d0ad8..319e6ed8 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -62,9 +62,6 @@ public: public slots: virtual void accept(); -private: - void storeSettings(QListWidgetItem *pluginItem); - public: QString getExecutableBlacklist() { return m_ExecutableBlacklist; } void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } @@ -74,7 +71,6 @@ public: private slots: void on_bsaDateBtn_clicked(); void on_execBlacklistBtn_clicked(); - void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); void on_resetGeometryBtn_clicked(); void deleteBlacklistItem(); diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp new file mode 100644 index 00000000..32269344 --- /dev/null +++ b/src/settingsdialogplugins.cpp @@ -0,0 +1,110 @@ +#include "settingsdialogplugins.h" +#include "ui_settingsdialog.h" +#include "noeditdelegate.h" +#include + +using MOBase::IPlugin; + +PluginsSettingsTab::PluginsSettingsTab(Settings *m_parent, SettingsDialog &m_dialog) + : SettingsTab(m_parent, m_dialog) +{ + // display plugin settings + QSet handledNames; + for (IPlugin *plugin : m_parent->plugins()) { + if (handledNames.contains(plugin->name())) + continue; + QListWidgetItem *listItem = new QListWidgetItem(plugin->name(), ui->pluginsList); + listItem->setData(Qt::UserRole, QVariant::fromValue((void*)plugin)); + listItem->setData(Qt::UserRole + 1, m_parent->m_PluginSettings[plugin->name()]); + listItem->setData(Qt::UserRole + 2, m_parent->m_PluginDescriptions[plugin->name()]); + ui->pluginsList->addItem(listItem); + handledNames.insert(plugin->name()); + } + + // display plugin blacklist + for (const QString &pluginName : m_parent->m_PluginBlacklist) { + ui->pluginBlacklist->addItem(pluginName); + } + + QObject::connect( + ui->pluginsList, &QListWidget::currentItemChanged, + [&](auto* current, auto* previous) { on_pluginsList_currentItemChanged(current, previous); }); +} + +void PluginsSettingsTab::update() +{ + // transfer plugin settings to in-memory structure + for (int i = 0; i < ui->pluginsList->count(); ++i) { + QListWidgetItem *item = ui->pluginsList->item(i); + m_parent->m_PluginSettings[item->text()] = item->data(Qt::UserRole + 1).toMap(); + } + // store plugin settings on disc + for (auto iterPlugins = m_parent->m_PluginSettings.begin(); iterPlugins != m_parent->m_PluginSettings.end(); ++iterPlugins) { + for (auto iterSettings = iterPlugins->begin(); iterSettings != iterPlugins->end(); ++iterSettings) { + m_Settings.setValue("Plugins/" + iterPlugins.key() + "/" + iterSettings.key(), iterSettings.value()); + } + } + + // store plugin blacklist + m_parent->m_PluginBlacklist.clear(); + for (QListWidgetItem *item : ui->pluginBlacklist->findItems("*", Qt::MatchWildcard)) { + m_parent->m_PluginBlacklist.insert(item->text()); + } + m_parent->writePluginBlacklist(); +} + +void PluginsSettingsTab::closing() +{ + storeSettings(ui->pluginsList->currentItem()); +} + +void PluginsSettingsTab::on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + storeSettings(previous); + + ui->pluginSettingsList->clear(); + IPlugin *plugin = static_cast(current->data(Qt::UserRole).value()); + ui->authorLabel->setText(plugin->author()); + ui->versionLabel->setText(plugin->version().canonicalString()); + ui->descriptionLabel->setText(plugin->description()); + + QVariantMap settings = current->data(Qt::UserRole + 1).toMap(); + QVariantMap descriptions = current->data(Qt::UserRole + 2).toMap(); + ui->pluginSettingsList->setEnabled(settings.count() != 0); + for (auto iter = settings.begin(); iter != settings.end(); ++iter) { + QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(iter.key())); + QVariant value = *iter; + QString description; + { + auto descriptionIter = descriptions.find(iter.key()); + if (descriptionIter != descriptions.end()) { + description = descriptionIter->toString(); + } + } + + ui->pluginSettingsList->setItemDelegateForColumn(0, new NoEditDelegate()); + newItem->setData(1, Qt::DisplayRole, value); + newItem->setData(1, Qt::EditRole, value); + newItem->setToolTip(1, description); + + newItem->setFlags(newItem->flags() | Qt::ItemIsEditable); + ui->pluginSettingsList->addTopLevelItem(newItem); + } + + ui->pluginSettingsList->resizeColumnToContents(0); + ui->pluginSettingsList->resizeColumnToContents(1); +} + +void PluginsSettingsTab::storeSettings(QListWidgetItem *pluginItem) +{ + if (pluginItem != nullptr) { + QVariantMap settings = pluginItem->data(Qt::UserRole + 1).toMap(); + + for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) { + const QTreeWidgetItem *item = ui->pluginSettingsList->topLevelItem(i); + settings[item->text(0)] = item->data(1, Qt::DisplayRole); + } + + pluginItem->setData(Qt::UserRole + 1, settings); + } +} diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h new file mode 100644 index 00000000..48d61858 --- /dev/null +++ b/src/settingsdialogplugins.h @@ -0,0 +1,20 @@ +#ifndef SETTINGSDIALOGPLUGINS_H +#define SETTINGSDIALOGPLUGINS_H + +#include "settings.h" +#include "settingsdialog.h" + +class PluginsSettingsTab : public SettingsTab +{ +public: + PluginsSettingsTab(Settings *m_parent, SettingsDialog &m_dialog); + + void update(); + void closing() override; + +private: + void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void storeSettings(QListWidgetItem *pluginItem); +}; + +#endif // SETTINGSDIALOGPLUGINS_H -- cgit v1.3.1 From e4dcdb01ac2e3f99fea76b21e1acfd21d0de89c7 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 10:45:59 -0400 Subject: split workarounds tab --- src/CMakeLists.txt | 3 ++ src/loadmechanism.cpp | 6 +-- src/loadmechanism.h | 6 +-- src/settings.cpp | 73 +------------------------- src/settings.h | 20 +------ src/settingsdialog.cpp | 51 ------------------ src/settingsdialog.h | 14 +---- src/settingsdialogplugins.cpp | 9 ++++ src/settingsdialogplugins.h | 1 + src/settingsdialogworkarounds.cpp | 108 ++++++++++++++++++++++++++++++++++++++ src/settingsdialogworkarounds.h | 25 +++++++++ 11 files changed, 157 insertions(+), 159 deletions(-) create mode 100644 src/settingsdialogworkarounds.cpp create mode 100644 src/settingsdialogworkarounds.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a8ded510..86ef9721 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,6 +42,7 @@ SET(organizer_SRCS settingsdialogpaths.cpp settingsdialogplugins.cpp settingsdialogsteam.cpp + settingsdialogworkarounds.cpp settings.cpp selfupdater.cpp selectiondialog.cpp @@ -161,6 +162,7 @@ SET(organizer_HDRS settingsdialogpaths.h settingsdialogplugins.h settingsdialogsteam.h + settingsdialogworkarounds.h settings.h selfupdater.h selectiondialog.h @@ -446,6 +448,7 @@ set(settings settingsdialogpaths settingsdialogplugins settingsdialogsteam + settingsdialogworkarounds ) set(utilities diff --git a/src/loadmechanism.cpp b/src/loadmechanism.cpp index 4d6cebd4..2d01562d 100644 --- a/src/loadmechanism.cpp +++ b/src/loadmechanism.cpp @@ -63,7 +63,7 @@ void LoadMechanism::removeHintFile(QDir targetDirectory) } -bool LoadMechanism::isDirectLoadingSupported() +bool LoadMechanism::isDirectLoadingSupported() const { //FIXME: Seriously? isn't there a 'do i need steam' thing? IPluginGame const *game = qApp->property("managed_game").value(); @@ -76,7 +76,7 @@ bool LoadMechanism::isDirectLoadingSupported() } } -bool LoadMechanism::isScriptExtenderSupported() +bool LoadMechanism::isScriptExtenderSupported() const { IPluginGame const *game = qApp->property("managed_game").value(); ScriptExtender *extender = game->feature(); @@ -85,7 +85,7 @@ bool LoadMechanism::isScriptExtenderSupported() return extender != nullptr && extender->isInstalled(); } -bool LoadMechanism::isProxyDLLSupported() +bool LoadMechanism::isProxyDLLSupported() const { // using steam_api.dll as the proxy is way too game specific as many games will have different // versions of that dll. diff --git a/src/loadmechanism.h b/src/loadmechanism.h index c04473ab..51fefaf9 100644 --- a/src/loadmechanism.h +++ b/src/loadmechanism.h @@ -68,21 +68,21 @@ public: * * @return true if the load mechanism is supported **/ - bool isDirectLoadingSupported(); + bool isDirectLoadingSupported() const; /** * @brief test whether the "Script Extender" load mechanism is supported for the current game * * @return true if the load mechanism is supported **/ - bool isScriptExtenderSupported(); + bool isScriptExtenderSupported() const; /** * @brief test whether the "Proxy DLL" load mechanism is supported for the current game * * @return true if the load mechanism is supported **/ - bool isProxyDLLSupported(); + bool isProxyDLLSupported() const; private: diff --git a/src/settings.cpp b/src/settings.cpp index bc45b720..515ff907 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -27,6 +27,7 @@ along with Mod Organizer. If not, see . #include "settingsdialogpaths.h" #include "settingsdialogplugins.h" #include "settingsdialogsteam.h" +#include "settingsdialogworkarounds.h" #include "versioninfo.h" #include "appconfig.h" #include "organizercore.h" @@ -679,7 +680,7 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) tabs.push_back(std::unique_ptr(new NexusSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new SteamSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new PluginsSettingsTab(this, dialog))); - tabs.push_back(std::unique_ptr(new WorkaroundsTab(this, dialog))); + tabs.push_back(std::unique_ptr(new WorkaroundsSettingsTab(this, dialog))); QString key = QString("geometry/%1").arg(dialog.objectName()); @@ -770,73 +771,3 @@ void Settings::DiagnosticsTab::update() m_Settings.setValue("Settings/crash_dumps_type", m_dumpsTypeBox->currentIndex()); m_Settings.setValue("Settings/crash_dumps_max", m_dumpsMaxEdit->value()); } - - -Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent, - SettingsDialog &m_dialog) - : SettingsTab(m_parent, m_dialog) - , m_appIDEdit(m_dialog.findChild("appIDEdit")) - , m_mechanismBox(m_dialog.findChild("mechanismBox")) - , m_hideUncheckedBox(m_dialog.findChild("hideUncheckedBox")) - , m_forceEnableBox(m_dialog.findChild("forceEnableBox")) - , m_displayForeignBox(m_dialog.findChild("displayForeignBox")) - , m_lockGUIBox(m_dialog.findChild("lockGUIBox")) - , m_enableArchiveParsingBox(m_dialog.findChild("enableArchiveParsingBox")) - , m_resetGeometriesBtn(m_dialog.findChild("resetGeometryBtn")) -{ - m_appIDEdit->setText(m_parent->getSteamAppID()); - - LoadMechanism::EMechanism mechanismID = m_parent->getLoadMechanism(); - int index = 0; - - if (m_parent->m_LoadMechanism.isDirectLoadingSupported()) { - m_mechanismBox->addItem(QObject::tr("Mod Organizer"), LoadMechanism::LOAD_MODORGANIZER); - if (mechanismID == LoadMechanism::LOAD_MODORGANIZER) { - index = m_mechanismBox->count() - 1; - } - } - - if (m_parent->m_LoadMechanism.isScriptExtenderSupported()) { - m_mechanismBox->addItem(QObject::tr("Script Extender"), LoadMechanism::LOAD_SCRIPTEXTENDER); - if (mechanismID == LoadMechanism::LOAD_SCRIPTEXTENDER) { - index = m_mechanismBox->count() - 1; - } - } - - if (m_parent->m_LoadMechanism.isProxyDLLSupported()) { - m_mechanismBox->addItem(QObject::tr("Proxy DLL"), LoadMechanism::LOAD_PROXYDLL); - if (mechanismID == LoadMechanism::LOAD_PROXYDLL) { - index = m_mechanismBox->count() - 1; - } - } - - m_mechanismBox->setCurrentIndex(index); - - m_hideUncheckedBox->setChecked(m_parent->hideUncheckedPlugins()); - m_forceEnableBox->setChecked(m_parent->forceEnableCoreFiles()); - m_displayForeignBox->setChecked(m_parent->displayForeign()); - m_lockGUIBox->setChecked(m_parent->lockGUI()); - m_enableArchiveParsingBox->setChecked(m_parent->archiveParsing()); - - m_resetGeometriesBtn->setChecked(m_parent->directInterface().value("reset_geometry", false).toBool()); - - m_dialog.setExecutableBlacklist(m_parent->executablesBlacklist()); - -} - -void Settings::WorkaroundsTab::update() -{ - if (m_appIDEdit->text() != m_parent->m_GamePlugin->steamAPPId()) { - m_Settings.setValue("Settings/app_id", m_appIDEdit->text()); - } else { - m_Settings.remove("Settings/app_id"); - } - m_Settings.setValue("Settings/load_mechanism", m_mechanismBox->itemData(m_mechanismBox->currentIndex()).toInt()); - m_Settings.setValue("Settings/hide_unchecked_plugins", m_hideUncheckedBox->isChecked()); - m_Settings.setValue("Settings/force_enable_core_files", m_forceEnableBox->isChecked()); - m_Settings.setValue("Settings/display_foreign", m_displayForeignBox->isChecked()); - m_Settings.setValue("Settings/lock_gui", m_lockGUIBox->isChecked()); - m_Settings.setValue("Settings/archive_parsing_experimental", m_enableArchiveParsingBox->isChecked()); - - m_Settings.setValue("Settings/executable_blacklist", m_dialog.getExecutableBlacklist()); -} diff --git a/src/settings.h b/src/settings.h index 5298103a..64068173 100644 --- a/src/settings.h +++ b/src/settings.h @@ -434,6 +434,7 @@ public: QMap m_PluginDescriptions; QSet m_PluginBlacklist; void writePluginBlacklist(); + const LoadMechanism& loadMechanism() const { return m_LoadMechanism; } public slots: @@ -466,25 +467,6 @@ private: void setLevelsBox(); }; - /** Display/store the configuration in the 'workarounds' tab of the settings dialogue */ - class WorkaroundsTab : public SettingsTab - { - public: - WorkaroundsTab(Settings *m_parent, SettingsDialog &m_dialog); - - void update(); - - private: - QLineEdit *m_appIDEdit; - QComboBox *m_mechanismBox; - QCheckBox *m_hideUncheckedBox; - QCheckBox *m_forceEnableBox; - QCheckBox *m_displayForeignBox; - QCheckBox *m_lockGUIBox; - QCheckBox *m_enableArchiveParsingBox; - QPushButton *m_resetGeometriesBtn; - }; - private slots: signals: diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index f43f7ae8..76b0a146 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -58,10 +58,6 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* setti { ui->setupUi(this); ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}"); - - QShortcut *delShortcut = new QShortcut( - QKeySequence(Qt::Key_Delete), ui->pluginBlacklist); - connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem())); } SettingsDialog::~SettingsDialog() @@ -111,50 +107,3 @@ bool SettingsDialog::getApiKeyChanged() { return m_keyChanged; } - -void SettingsDialog::on_execBlacklistBtn_clicked() -{ - bool ok = false; - QString result = QInputDialog::getMultiLineText( - this, - tr("Executables Blacklist"), - tr("Enter one executable per line to be blacklisted from the virtual file system.\n" - "Mods and other virtualized files will not be visible to these executables and\n" - "any executables launched by them.\n\n" - "Example:\n" - " Chrome.exe\n" - " Firefox.exe"), - m_ExecutableBlacklist.split(";").join("\n"), - &ok - ); - if (ok) { - QStringList blacklist; - for (auto exec : result.split("\n")) { - if (exec.trimmed().endsWith(".exe", Qt::CaseInsensitive)) { - blacklist << exec.trimmed(); - } - } - m_ExecutableBlacklist = blacklist.join(";"); - } -} - -void SettingsDialog::on_bsaDateBtn_clicked() -{ - IPluginGame const *game - = qApp->property("managed_game").value(); - QDir dir = game->dataDirectory(); - - Helper::backdateBSAs(qApp->applicationDirPath().toStdWString(), - dir.absolutePath().toStdWString()); -} - -void SettingsDialog::deleteBlacklistItem() -{ - ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row()); -} - -void SettingsDialog::on_resetGeometryBtn_clicked() -{ - m_GeometriesReset = true; - ui->resetGeometryBtn->setChecked(true); -} diff --git a/src/settingsdialog.h b/src/settingsdialog.h index 319e6ed8..81c17f44 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -57,30 +57,20 @@ public: // temp Ui::SettingsDialog *ui; bool m_keyChanged; + bool m_GeometriesReset; PluginContainer *m_PluginContainer; public slots: virtual void accept(); public: - QString getExecutableBlacklist() { return m_ExecutableBlacklist; } - void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } - bool getResetGeometries(); bool getApiKeyChanged(); - -private slots: - void on_bsaDateBtn_clicked(); - void on_execBlacklistBtn_clicked(); - void on_resetGeometryBtn_clicked(); - - void deleteBlacklistItem(); + bool getResetGeometries(); private: Settings* m_settings; - bool m_GeometriesReset; - QString m_ExecutableBlacklist; }; #endif // SETTINGSDIALOG_H diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 32269344..33bc1563 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -29,6 +29,10 @@ PluginsSettingsTab::PluginsSettingsTab(Settings *m_parent, SettingsDialog &m_dia QObject::connect( ui->pluginsList, &QListWidget::currentItemChanged, [&](auto* current, auto* previous) { on_pluginsList_currentItemChanged(current, previous); }); + + QShortcut *delShortcut = new QShortcut( + QKeySequence(Qt::Key_Delete), ui->pluginBlacklist); + QObject::connect(delShortcut, &QShortcut::activated, parentWidget(), [&]{ deleteBlacklistItem(); }); } void PluginsSettingsTab::update() @@ -95,6 +99,11 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QListWidgetItem *curr ui->pluginSettingsList->resizeColumnToContents(1); } +void PluginsSettingsTab::deleteBlacklistItem() +{ + ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row()); +} + void PluginsSettingsTab::storeSettings(QListWidgetItem *pluginItem) { if (pluginItem != nullptr) { diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h index 48d61858..9d21daa6 100644 --- a/src/settingsdialogplugins.h +++ b/src/settingsdialogplugins.h @@ -14,6 +14,7 @@ public: private: void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void deleteBlacklistItem(); void storeSettings(QListWidgetItem *pluginItem); }; diff --git a/src/settingsdialogworkarounds.cpp b/src/settingsdialogworkarounds.cpp new file mode 100644 index 00000000..4cca5fd4 --- /dev/null +++ b/src/settingsdialogworkarounds.cpp @@ -0,0 +1,108 @@ +#include "settingsdialogworkarounds.h" +#include "ui_settingsdialog.h" +#include "helper.h" +#include + +WorkaroundsSettingsTab::WorkaroundsSettingsTab(Settings *m_parent, SettingsDialog &m_dialog) + : SettingsTab(m_parent, m_dialog) +{ + ui->appIDEdit->setText(m_parent->getSteamAppID()); + + LoadMechanism::EMechanism mechanismID = m_parent->getLoadMechanism(); + int index = 0; + + if (m_parent->loadMechanism().isDirectLoadingSupported()) { + ui->mechanismBox->addItem(QObject::tr("Mod Organizer"), LoadMechanism::LOAD_MODORGANIZER); + if (mechanismID == LoadMechanism::LOAD_MODORGANIZER) { + index = ui->mechanismBox->count() - 1; + } + } + + if (m_parent->loadMechanism().isScriptExtenderSupported()) { + ui->mechanismBox->addItem(QObject::tr("Script Extender"), LoadMechanism::LOAD_SCRIPTEXTENDER); + if (mechanismID == LoadMechanism::LOAD_SCRIPTEXTENDER) { + index = ui->mechanismBox->count() - 1; + } + } + + if (m_parent->loadMechanism().isProxyDLLSupported()) { + ui->mechanismBox->addItem(QObject::tr("Proxy DLL"), LoadMechanism::LOAD_PROXYDLL); + if (mechanismID == LoadMechanism::LOAD_PROXYDLL) { + index = ui->mechanismBox->count() - 1; + } + } + + ui->mechanismBox->setCurrentIndex(index); + + ui->hideUncheckedBox->setChecked(m_parent->hideUncheckedPlugins()); + ui->forceEnableBox->setChecked(m_parent->forceEnableCoreFiles()); + ui->displayForeignBox->setChecked(m_parent->displayForeign()); + ui->lockGUIBox->setChecked(m_parent->lockGUI()); + ui->enableArchiveParsingBox->setChecked(m_parent->archiveParsing()); + + ui->resetGeometryBtn->setChecked(m_parent->directInterface().value("reset_geometry", false).toBool()); + + setExecutableBlacklist(m_parent->executablesBlacklist()); + + QObject::connect(ui->bsaDateBtn, &QPushButton::clicked, [&]{ on_bsaDateBtn_clicked(); }); + QObject::connect(ui->execBlacklistBtn, &QPushButton::clicked, [&]{ on_execBlacklistBtn_clicked(); }); + QObject::connect(ui->resetGeometryBtn, &QPushButton::clicked, [&]{ on_resetGeometryBtn_clicked(); }); +} + +void WorkaroundsSettingsTab::update() +{ + if (ui->appIDEdit->text() != m_parent->gamePlugin()->steamAPPId()) { + m_Settings.setValue("Settings/app_id", ui->appIDEdit->text()); + } else { + m_Settings.remove("Settings/app_id"); + } + m_Settings.setValue("Settings/load_mechanism", ui->mechanismBox->itemData(ui->mechanismBox->currentIndex()).toInt()); + m_Settings.setValue("Settings/hide_unchecked_plugins", ui->hideUncheckedBox->isChecked()); + m_Settings.setValue("Settings/force_enable_core_files", ui->forceEnableBox->isChecked()); + m_Settings.setValue("Settings/display_foreign", ui->displayForeignBox->isChecked()); + m_Settings.setValue("Settings/lock_gui", ui->lockGUIBox->isChecked()); + m_Settings.setValue("Settings/archive_parsing_experimental", ui->enableArchiveParsingBox->isChecked()); + + m_Settings.setValue("Settings/executable_blacklist", getExecutableBlacklist()); +} + +void WorkaroundsSettingsTab::on_execBlacklistBtn_clicked() +{ + bool ok = false; + QString result = QInputDialog::getMultiLineText( + parentWidget(), + QObject::tr("Executables Blacklist"), + QObject::tr("Enter one executable per line to be blacklisted from the virtual file system.\n" + "Mods and other virtualized files will not be visible to these executables and\n" + "any executables launched by them.\n\n" + "Example:\n" + " Chrome.exe\n" + " Firefox.exe"), + m_ExecutableBlacklist.split(";").join("\n"), + &ok + ); + if (ok) { + QStringList blacklist; + for (auto exec : result.split("\n")) { + if (exec.trimmed().endsWith(".exe", Qt::CaseInsensitive)) { + blacklist << exec.trimmed(); + } + } + m_ExecutableBlacklist = blacklist.join(";"); + } +} + +void WorkaroundsSettingsTab::on_bsaDateBtn_clicked() +{ + const auto* game = qApp->property("managed_game").value(); + QDir dir = game->dataDirectory(); + + Helper::backdateBSAs(qApp->applicationDirPath().toStdWString(), + dir.absolutePath().toStdWString()); +} + +void WorkaroundsSettingsTab::on_resetGeometryBtn_clicked() +{ + m_dialog.m_GeometriesReset = true; + ui->resetGeometryBtn->setChecked(true); +} diff --git a/src/settingsdialogworkarounds.h b/src/settingsdialogworkarounds.h new file mode 100644 index 00000000..1687624b --- /dev/null +++ b/src/settingsdialogworkarounds.h @@ -0,0 +1,25 @@ +#ifndef SETTINGSDIALOGWORKAROUNDS_H +#define SETTINGSDIALOGWORKAROUNDS_H + +#include "settings.h" +#include "settingsdialog.h" + +class WorkaroundsSettingsTab : public SettingsTab +{ +public: + WorkaroundsSettingsTab(Settings *m_parent, SettingsDialog &m_dialog); + + void update(); + +private: + QString m_ExecutableBlacklist; + + void on_bsaDateBtn_clicked(); + void on_execBlacklistBtn_clicked(); + void on_resetGeometryBtn_clicked(); + + QString getExecutableBlacklist() { return m_ExecutableBlacklist; } + void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; } +}; + +#endif // SETTINGSDIALOGWORKAROUNDS_H -- cgit v1.3.1 From e8d7930edacdc04a4607ecd59fc402f2f04ea39d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 20 Jul 2019 10:59:07 -0400 Subject: split diagnostics tab fixed log and crash dump directory links in label not working --- src/CMakeLists.txt | 3 +++ src/settings.cpp | 32 ++------------------------------ src/settings.h | 18 ------------------ src/settingsdialog.ui | 3 +++ src/settingsdialogdiagnostics.cpp | 28 ++++++++++++++++++++++++++++ src/settingsdialogdiagnostics.h | 17 +++++++++++++++++ 6 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 src/settingsdialogdiagnostics.cpp create mode 100644 src/settingsdialogdiagnostics.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 86ef9721..d8316e7e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,7 @@ SET(organizer_SRCS spawn.cpp singleinstance.cpp settingsdialog.cpp + settingsdialogdiagnostics.cpp settingsdialoggeneral.cpp settingsdialognexus.cpp settingsdialogpaths.cpp @@ -157,6 +158,7 @@ SET(organizer_HDRS spawn.h singleinstance.h settingsdialog.h + settingsdialogdiagnostics.h settingsdialoggeneral.h settingsdialognexus.h settingsdialogpaths.h @@ -443,6 +445,7 @@ set(profiles set(settings settings settingsdialog + settingsdialogdiagnostics settingsdialoggeneral settingsdialognexus settingsdialogpaths diff --git a/src/settings.cpp b/src/settings.cpp index 515ff907..725b7e06 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -22,6 +22,7 @@ along with Mod Organizer. If not, see . #include "pluginsetting.h" #include "serverinfo.h" #include "settingsdialog.h" +#include "settingsdialogdiagnostics.h" #include "settingsdialoggeneral.h" #include "settingsdialognexus.h" #include "settingsdialogpaths.h" @@ -676,7 +677,7 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) tabs.push_back(std::unique_ptr(new GeneralSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new PathsSettingsTab(this, dialog))); - tabs.push_back(std::unique_ptr(new DiagnosticsTab(this, dialog))); + tabs.push_back(std::unique_ptr(new DiagnosticsSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new NexusSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new SteamSettingsTab(this, dialog))); tabs.push_back(std::unique_ptr(new PluginsSettingsTab(this, dialog))); @@ -742,32 +743,3 @@ void Settings::query(PluginContainer *pluginContainer, QWidget *parent) } } - - -Settings::DiagnosticsTab::DiagnosticsTab(Settings *m_parent, SettingsDialog &m_dialog) - : SettingsTab(m_parent, m_dialog) - , m_logLevelBox(m_dialog.findChild("logLevelBox")) - , m_dumpsTypeBox(m_dialog.findChild("dumpsTypeBox")) - , m_dumpsMaxEdit(m_dialog.findChild("dumpsMaxEdit")) - , m_diagnosticsExplainedLabel(m_dialog.findChild("diagnosticsExplainedLabel")) -{ - setLevelsBox(); - m_dumpsTypeBox->setCurrentIndex(m_parent->crashDumpsType()); - m_dumpsMaxEdit->setValue(m_parent->crashDumpsMax()); - QString logsPath = qApp->property("dataPath").toString() - + "/" + QString::fromStdWString(AppConfig::logPath()); - m_diagnosticsExplainedLabel->setText( - m_diagnosticsExplainedLabel->text() - .replace("LOGS_FULL_PATH", logsPath) - .replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath())) - .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath())) - .replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir())) - ); -} - -void Settings::DiagnosticsTab::update() -{ - m_Settings.setValue("Settings/log_level", m_logLevelBox->currentData().toInt()); - m_Settings.setValue("Settings/crash_dumps_type", m_dumpsTypeBox->currentIndex()); - m_Settings.setValue("Settings/crash_dumps_max", m_dumpsMaxEdit->value()); -} diff --git a/src/settings.h b/src/settings.h index 64068173..71fbcbc1 100644 --- a/src/settings.h +++ b/src/settings.h @@ -449,24 +449,6 @@ private: void readPluginBlacklist(); QString getConfigurablePath(const QString &key, const QString &def, bool resolve) const; - - - class DiagnosticsTab : public SettingsTab - { - public: - DiagnosticsTab(Settings *parent, SettingsDialog &dialog); - - void update(); - - private: - QComboBox *m_logLevelBox; - QComboBox *m_dumpsTypeBox; - QSpinBox *m_dumpsMaxEdit; - QLabel *m_diagnosticsExplainedLabel; - - void setLevelsBox(); - }; - private slots: signals: diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 3ad525e1..1deac400 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1325,6 +1325,9 @@ programs you are intentionally running. true + + true + diff --git a/src/settingsdialogdiagnostics.cpp b/src/settingsdialogdiagnostics.cpp new file mode 100644 index 00000000..2ea4d478 --- /dev/null +++ b/src/settingsdialogdiagnostics.cpp @@ -0,0 +1,28 @@ +#include "settingsdialogdiagnostics.h" +#include "ui_settingsdialog.h" +#include "appconfig.h" +#include "organizercore.h" + +DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings *m_parent, SettingsDialog &m_dialog) + : SettingsTab(m_parent, m_dialog) +{ + ui->logLevelBox->setCurrentIndex(m_parent->logLevel()); + ui->dumpsTypeBox->setCurrentIndex(m_parent->crashDumpsType()); + ui->dumpsMaxEdit->setValue(m_parent->crashDumpsMax()); + QString logsPath = qApp->property("dataPath").toString() + + "/" + QString::fromStdWString(AppConfig::logPath()); + ui->diagnosticsExplainedLabel->setText( + ui->diagnosticsExplainedLabel->text() + .replace("LOGS_FULL_PATH", logsPath) + .replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath())) + .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath())) + .replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir())) + ); +} + +void DiagnosticsSettingsTab::update() +{ + m_Settings.setValue("Settings/log_level", ui->logLevelBox->currentIndex()); + m_Settings.setValue("Settings/crash_dumps_type", ui->dumpsTypeBox->currentIndex()); + m_Settings.setValue("Settings/crash_dumps_max", ui->dumpsMaxEdit->value()); +} diff --git a/src/settingsdialogdiagnostics.h b/src/settingsdialogdiagnostics.h new file mode 100644 index 00000000..2341c253 --- /dev/null +++ b/src/settingsdialogdiagnostics.h @@ -0,0 +1,17 @@ +#ifndef SETTINGSDIALOGDIAGNOSTICS_H +#define SETTINGSDIALOGDIAGNOSTICS_H + +#include "settings.h" +#include "settingsdialog.h" + +class DiagnosticsSettingsTab : public SettingsTab +{ +public: + DiagnosticsSettingsTab(Settings *parent, SettingsDialog &dialog); + + void update(); + +private: +}; + +#endif // SETTINGSDIALOGDIAGNOSTICS_H -- cgit v1.3.1 From 3757aa3f532c943f8a47e997d0a4f250d9dacdd3 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 2 Sep 2019 15:07:35 -0400 Subject: split into settingsutilities --- src/CMakeLists.txt | 9 +- src/settings.cpp | 452 +--------------------------------------------- src/settings.h | 3 - src/settingsutilities.cpp | 247 +++++++++++++++++++++++++ src/settingsutilities.h | 266 +++++++++++++++++++++++++++ 5 files changed, 522 insertions(+), 455 deletions(-) create mode 100644 src/settingsutilities.cpp create mode 100644 src/settingsutilities.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d8316e7e..ea27184b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,7 @@ SET(organizer_SRCS settingsdialogsteam.cpp settingsdialogworkarounds.cpp settings.cpp + settingsutilities.cpp selfupdater.cpp selectiondialog.cpp queryoverwritedialog.cpp @@ -166,6 +167,7 @@ SET(organizer_HDRS settingsdialogsteam.h settingsdialogworkarounds.h settings.h + settingsutilities.h selfupdater.h selectiondialog.h queryoverwritedialog.h @@ -444,6 +446,10 @@ set(profiles set(settings settings + settingsutilities +) + +set(settingsdialog settingsdialog settingsdialogdiagnostics settingsdialoggeneral @@ -490,7 +496,8 @@ set(widgets set(src_filters application core browser dialogs downloads env executables locking modinfo - modinfo\\dialog modlist plugins previews profiles settings utilities widgets + modinfo\\dialog modlist plugins previews profiles settings settingsdialog + utilities widgets ) foreach(filter in list ${src_filters}) diff --git a/src/settings.cpp b/src/settings.cpp index 8b063efb..14189be3 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -18,6 +18,7 @@ along with Mod Organizer. If not, see . */ #include "settings.h" +#include "settingsutilities.h" #include "serverinfo.h" #include "executableslist.h" #include "appconfig.h" @@ -27,310 +28,6 @@ along with Mod Organizer. If not, see . using namespace MOBase; -template -struct ValueConverter -{ - static const T& convert(const T& t) - { - return t; - } -}; - -template -struct ValueConverter>> -{ - static QString convert(const T& t) - { - return QString("%1").arg(static_cast>(t)); - } -}; - - -template -void logChange( - const QString& displayName, std::optional oldValue, const T& newValue) -{ - using VC = ValueConverter; - - if (oldValue) { - log::debug( - "setting '{}' changed from '{}' to '{}'", - displayName, VC::convert(*oldValue), VC::convert(newValue)); - } else { - log::debug( - "setting '{}' set to '{}'", - displayName, VC::convert(newValue)); - } -} - -void logRemoval(const QString& name) -{ - log::debug("setting '{}' removed", name); -} - - -QString settingName(const QString& section, const QString& key) -{ - if (section.isEmpty()) { - return key; - } else if (key.isEmpty()) { - return section; - } else { - if (section.compare("General", Qt::CaseInsensitive) == 0) { - return key; - } else { - return section + "/" + key; - } - } -} - -template -void setImpl( - QSettings& settings, const QString& displayName, - const QString& section, const QString& key, const T& value) -{ - const auto current = getOptional(settings, section, key); - - if (current && *current == value) { - // no change - return; - } - - const auto name = settingName(section, key); - - logChange(displayName, current, value); - - if constexpr (std::is_enum_v) { - settings.setValue( - name, static_cast>(value)); - } else { - settings.setValue(name, value); - } -} - -void removeImpl( - QSettings& settings, const QString& displayName, - const QString& section, const QString& key) -{ - if (key.isEmpty()) { - if (!settings.childGroups().contains(section, Qt::CaseInsensitive)) { - // not there - return; - } - } else { - if (!settings.contains(settingName(section, key))) { - // not there - return; - } - } - - logRemoval(displayName); - settings.remove(settingName(section, key)); -} - - -template -std::optional getOptional( - const QSettings& settings, - const QString& section, const QString& key, std::optional def={}) -{ - if (settings.contains(settingName(section, key))) { - const auto v = settings.value(settingName(section, key)); - - if constexpr (std::is_enum_v) { - return static_cast(v.value>()); - } else { - return v.value(); - } - } - - return def; -} - -template -T get( - const QSettings& settings, - const QString& section, const QString& key, T def) -{ - if (auto v=getOptional(settings, section, key)) { - return *v; - } else { - return def; - } -} - -template -void set( - QSettings& settings, - const QString& section, const QString& key, const T& value) -{ - setImpl(settings, settingName(section, key), section, key, value); -} - -void remove(QSettings& settings, const QString& section, const QString& key) -{ - removeImpl(settings, settingName(section, key), section, key); -} - -void removeSection(QSettings& settings, const QString& section) -{ - removeImpl(settings, section, section, ""); -} - - -class ScopedGroup -{ -public: - ScopedGroup(QSettings& s, const QString& name) - : m_settings(s), m_name(name) - { - m_settings.beginGroup(m_name); - } - - ~ScopedGroup() - { - m_settings.endGroup(); - } - - ScopedGroup(const ScopedGroup&) = delete; - ScopedGroup& operator=(const ScopedGroup&) = delete; - - template - void set(const QString& key, const T& value) - { - setImpl(m_settings, settingName(m_name, key), "", key, value); - } - - void remove(const QString& key) - { - removeImpl(m_settings, settingName(m_name, key), "", key); - } - - QStringList keys() const - { - return m_settings.childKeys(); - } - - template - void for_each(F&& f) const - { - for (const QString& key : keys()) { - f(key); - } - } - - template - std::optional getOptional(const QString& key, std::optional def={}) const - { - return ::getOptional(m_settings, "", key, def); - } - - template - T get(const QString& key, T def={}) const - { - return ::get(m_settings, "", key, def); - } - -private: - QSettings& m_settings; - QString m_name; -}; - - -class ScopedReadArray -{ -public: - ScopedReadArray(QSettings& s, const QString& section) - : m_settings(s), m_count(0) - { - m_count = m_settings.beginReadArray(section); - } - - ~ScopedReadArray() - { - m_settings.endArray(); - } - - ScopedReadArray(const ScopedReadArray&) = delete; - ScopedReadArray& operator=(const ScopedReadArray&) = delete; - - template - void for_each(F&& f) const - { - for (int i=0; i - std::optional getOptional(const QString& key, std::optional def={}) const - { - return ::getOptional(m_settings, "", key, def); - } - - template - T get(const QString& key, T def={}) const - { - return ::get(m_settings, "", key, def); - } - - int count() const - { - return m_count; - } - - QStringList keys() const - { - return m_settings.childKeys(); - } - -private: - QSettings& m_settings; - int m_count; -}; - - -class ScopedWriteArray -{ -public: - ScopedWriteArray(QSettings& s, const QString& section) - : m_settings(s), m_section(section), m_i(0) - { - m_settings.beginWriteArray(section); - } - - ~ScopedWriteArray() - { - m_settings.endArray(); - } - - ScopedWriteArray(const ScopedWriteArray&) = delete; - ScopedWriteArray& operator=(const ScopedWriteArray&) = delete; - - void next() - { - m_settings.setArrayIndex(m_i); - ++m_i; - } - - template - void set(const QString& key, const T& value) - { - const auto displayName = QString("%1/%2\\%3") - .arg(m_section) - .arg(m_i) - .arg(key); - - setImpl(m_settings, displayName, "", key, value); - } - -private: - QSettings& m_settings; - QString m_section; - int m_i; -}; - EndorsementState endorsementStateFromString(const QString& s) { @@ -360,153 +57,6 @@ QString toString(EndorsementState s) } -QString widgetNameWithTopLevel(const QWidget* widget) -{ - QStringList components; - - auto* tl = widget->window(); - - if (tl == widget) { - // this is a top level widget, such as a dialog - components.push_back(widget->objectName()); - } else { - // this is a widget - const auto toplevelName = tl->objectName(); - if (!toplevelName.isEmpty()) { - components.push_back(toplevelName); - } - - const auto widgetName = widget->objectName(); - if (!widgetName.isEmpty()) { - components.push_back(widgetName); - } - } - - if (components.isEmpty()) { - // can't do much - return "unknown_widget"; - } - - return components.join("_"); -} - -QString widgetName(const QMainWindow* w) -{ - return w->objectName(); -} - -QString widgetName(const QHeaderView* w) -{ - return widgetNameWithTopLevel(w->parentWidget()); -} - -QString widgetName(const ExpanderWidget* w) -{ - return widgetNameWithTopLevel(w->button()); -} - -QString widgetName(const QWidget* w) -{ - return widgetNameWithTopLevel(w); -} - -template -QString geoSettingName(const Widget* widget) -{ - return widgetName(widget) + "_geometry"; -} - -template -QString stateSettingName(const Widget* widget) -{ - return widgetName(widget) + "_state"; -} - -template -QString visibilitySettingName(const Widget* widget) -{ - return widgetName(widget) + "_visibility"; -} - -QString dockSettingName(const QDockWidget* dock) -{ - return "MainWindow_docks_" + dock->objectName() + "_size"; -} - -QString indexSettingName(const QWidget* widget) -{ - return widgetNameWithTopLevel(widget) + "_index"; -} - -QString checkedSettingName(const QAbstractButton* b) -{ - return widgetNameWithTopLevel(b) + "_checked"; -} - -void warnIfNotCheckable(const QAbstractButton* b) -{ - if (!b->isCheckable()) { - log::warn( - "button '{}' used in the settings as a checkbox or radio button " - "but is not checkable", b->objectName()); - } -} - - -bool setWindowsCredential(const QString key, const QString data) -{ - QString finalKey("ModOrganizer2_" + key); - wchar_t* keyData = new wchar_t[finalKey.size()+1]; - finalKey.toWCharArray(keyData); - keyData[finalKey.size()] = L'\0'; - bool result = false; - if (data.isEmpty()) { - result = CredDeleteW(keyData, CRED_TYPE_GENERIC, 0); - if (!result) - if (GetLastError() == ERROR_NOT_FOUND) - result = true; - } else { - wchar_t* charData = new wchar_t[data.size()]; - data.toWCharArray(charData); - - CREDENTIALW cred = {}; - cred.Flags = 0; - cred.Type = CRED_TYPE_GENERIC; - cred.TargetName = keyData; - cred.CredentialBlob = (LPBYTE)charData; - cred.CredentialBlobSize = sizeof(wchar_t) * data.size(); - cred.Persist = CRED_PERSIST_LOCAL_MACHINE; - - result = CredWriteW(&cred, 0); - delete[] charData; - } - delete[] keyData; - return result; -} - -QString getWindowsCredential(const QString key) -{ - QString result; - QString finalKey("ModOrganizer2_" + key); - wchar_t* keyData = new wchar_t[finalKey.size()+1]; - finalKey.toWCharArray(keyData); - keyData[finalKey.size()] = L'\0'; - PCREDENTIALW creds; - if (CredReadW(keyData, 1, 0, &creds)) { - wchar_t *charData = (wchar_t *)creds->CredentialBlob; - result = QString::fromWCharArray(charData, creds->CredentialBlobSize / sizeof(wchar_t)); - CredFree(creds); - } else { - const auto e = GetLastError(); - if (e != ERROR_NOT_FOUND) { - log::error("Retrieving encrypted data failed: {}", formatSystemMessage(e)); - } - } - delete[] keyData; - return result; -} - - Settings *Settings::s_Instance = nullptr; Settings::Settings(const QString& path) : diff --git a/src/settings.h b/src/settings.h index 5c0a2542..68cfb9b7 100644 --- a/src/settings.h +++ b/src/settings.h @@ -36,8 +36,6 @@ namespace MOBase { class QSplitter; -class PluginContainer; -class ServerInfo; class ServerList; class Settings; class ExpanderWidget; @@ -505,7 +503,6 @@ private: }; - /** * manages the settings for Mod Organizer. The settings are not cached * inside the class but read/written directly from/to disc diff --git a/src/settingsutilities.cpp b/src/settingsutilities.cpp new file mode 100644 index 00000000..7ac95b5f --- /dev/null +++ b/src/settingsutilities.cpp @@ -0,0 +1,247 @@ +#include "settingsutilities.h" +#include "expanderwidget.h" +#include + +using namespace MOBase; + +void logRemoval(const QString& name) +{ + log::debug("setting '{}' removed", name); +} + + +QString settingName(const QString& section, const QString& key) +{ + if (section.isEmpty()) { + return key; + } else if (key.isEmpty()) { + return section; + } else { + if (section.compare("General", Qt::CaseInsensitive) == 0) { + return key; + } else { + return section + "/" + key; + } + } +} + +void removeImpl( + QSettings& settings, const QString& displayName, + const QString& section, const QString& key) +{ + if (key.isEmpty()) { + if (!settings.childGroups().contains(section, Qt::CaseInsensitive)) { + // not there + return; + } + } else { + if (!settings.contains(settingName(section, key))) { + // not there + return; + } + } + + logRemoval(displayName); + settings.remove(settingName(section, key)); +} + +void remove(QSettings& settings, const QString& section, const QString& key) +{ + removeImpl(settings, settingName(section, key), section, key); +} + +void removeSection(QSettings& settings, const QString& section) +{ + removeImpl(settings, section, section, ""); +} + + +ScopedGroup::ScopedGroup(QSettings& s, const QString& name) + : m_settings(s), m_name(name) +{ + m_settings.beginGroup(m_name); +} + +ScopedGroup::~ScopedGroup() +{ + m_settings.endGroup(); +} + +void ScopedGroup::remove(const QString& key) +{ + removeImpl(m_settings, settingName(m_name, key), "", key); +} + +QStringList ScopedGroup::keys() const +{ + return m_settings.childKeys(); +} + + +ScopedReadArray::ScopedReadArray(QSettings& s, const QString& section) + : m_settings(s), m_count(0) +{ + m_count = m_settings.beginReadArray(section); +} + +ScopedReadArray::~ScopedReadArray() +{ + m_settings.endArray(); +} + +int ScopedReadArray::count() const +{ + return m_count; +} + +QStringList ScopedReadArray::keys() const +{ + return m_settings.childKeys(); +} + + +ScopedWriteArray::ScopedWriteArray(QSettings& s, const QString& section) + : m_settings(s), m_section(section), m_i(0) +{ + m_settings.beginWriteArray(section); +} + +ScopedWriteArray::~ScopedWriteArray() +{ + m_settings.endArray(); +} + +void ScopedWriteArray::next() +{ + m_settings.setArrayIndex(m_i); + ++m_i; +} + + +QString widgetNameWithTopLevel(const QWidget* widget) +{ + QStringList components; + + auto* tl = widget->window(); + + if (tl == widget) { + // this is a top level widget, such as a dialog + components.push_back(widget->objectName()); + } else { + // this is a widget + const auto toplevelName = tl->objectName(); + if (!toplevelName.isEmpty()) { + components.push_back(toplevelName); + } + + const auto widgetName = widget->objectName(); + if (!widgetName.isEmpty()) { + components.push_back(widgetName); + } + } + + if (components.isEmpty()) { + // can't do much + return "unknown_widget"; + } + + return components.join("_"); +} + +QString widgetName(const QMainWindow* w) +{ + return w->objectName(); +} + +QString widgetName(const QHeaderView* w) +{ + return widgetNameWithTopLevel(w->parentWidget()); +} + +QString widgetName(const ExpanderWidget* w) +{ + return widgetNameWithTopLevel(w->button()); +} + +QString widgetName(const QWidget* w) +{ + return widgetNameWithTopLevel(w); +} + +QString dockSettingName(const QDockWidget* dock) +{ + return "MainWindow_docks_" + dock->objectName() + "_size"; +} + +QString indexSettingName(const QWidget* widget) +{ + return widgetNameWithTopLevel(widget) + "_index"; +} + +QString checkedSettingName(const QAbstractButton* b) +{ + return widgetNameWithTopLevel(b) + "_checked"; +} + +void warnIfNotCheckable(const QAbstractButton* b) +{ + if (!b->isCheckable()) { + log::warn( + "button '{}' used in the settings as a checkbox or radio button " + "but is not checkable", b->objectName()); + } +} + + +bool setWindowsCredential(const QString key, const QString data) +{ + QString finalKey("ModOrganizer2_" + key); + wchar_t* keyData = new wchar_t[finalKey.size()+1]; + finalKey.toWCharArray(keyData); + keyData[finalKey.size()] = L'\0'; + bool result = false; + if (data.isEmpty()) { + result = CredDeleteW(keyData, CRED_TYPE_GENERIC, 0); + if (!result) + if (GetLastError() == ERROR_NOT_FOUND) + result = true; + } else { + wchar_t* charData = new wchar_t[data.size()]; + data.toWCharArray(charData); + + CREDENTIALW cred = {}; + cred.Flags = 0; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = keyData; + cred.CredentialBlob = (LPBYTE)charData; + cred.CredentialBlobSize = sizeof(wchar_t) * data.size(); + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + + result = CredWriteW(&cred, 0); + delete[] charData; + } + delete[] keyData; + return result; +} + +QString getWindowsCredential(const QString key) +{ + QString result; + QString finalKey("ModOrganizer2_" + key); + wchar_t* keyData = new wchar_t[finalKey.size()+1]; + finalKey.toWCharArray(keyData); + keyData[finalKey.size()] = L'\0'; + PCREDENTIALW creds; + if (CredReadW(keyData, 1, 0, &creds)) { + wchar_t *charData = (wchar_t *)creds->CredentialBlob; + result = QString::fromWCharArray(charData, creds->CredentialBlobSize / sizeof(wchar_t)); + CredFree(creds); + } else { + const auto e = GetLastError(); + if (e != ERROR_NOT_FOUND) { + log::error("Retrieving encrypted data failed: {}", formatSystemMessage(e)); + } + } + delete[] keyData; + return result; +} diff --git a/src/settingsutilities.h b/src/settingsutilities.h new file mode 100644 index 00000000..b70e55ef --- /dev/null +++ b/src/settingsutilities.h @@ -0,0 +1,266 @@ +#ifndef SETTINGSUTILITIES_H +#define SETTINGSUTILITIES_H + +#include + +class ExpanderWidget; + +template +struct ValueConverter +{ + static const T& convert(const T& t) + { + return t; + } +}; + +template +struct ValueConverter>> +{ + static QString convert(const T& t) + { + return QString("%1").arg(static_cast>(t)); + } +}; + + +template +void logChange( + const QString& displayName, std::optional oldValue, const T& newValue) +{ + using VC = ValueConverter; + + if (oldValue) { + log::debug( + "setting '{}' changed from '{}' to '{}'", + displayName, VC::convert(*oldValue), VC::convert(newValue)); + } else { + log::debug( + "setting '{}' set to '{}'", + displayName, VC::convert(newValue)); + } +} + +void logRemoval(const QString& name); + + +QString settingName(const QString& section, const QString& key); + +template +void setImpl( + QSettings& settings, const QString& displayName, + const QString& section, const QString& key, const T& value) +{ + const auto current = getOptional(settings, section, key); + + if (current && *current == value) { + // no change + return; + } + + const auto name = settingName(section, key); + + logChange(displayName, current, value); + + if constexpr (std::is_enum_v) { + settings.setValue( + name, static_cast>(value)); + } else { + settings.setValue(name, value); + } +} + +void removeImpl( + QSettings& settings, const QString& displayName, + const QString& section, const QString& key); + + +template +std::optional getOptional( + const QSettings& settings, + const QString& section, const QString& key, std::optional def={}) +{ + if (settings.contains(settingName(section, key))) { + const auto v = settings.value(settingName(section, key)); + + if constexpr (std::is_enum_v) { + return static_cast(v.value>()); + } else { + return v.value(); + } + } + + return def; +} + +template +T get( + const QSettings& settings, + const QString& section, const QString& key, T def) +{ + if (auto v=getOptional(settings, section, key)) { + return *v; + } else { + return def; + } +} + +template +void set( + QSettings& settings, + const QString& section, const QString& key, const T& value) +{ + setImpl(settings, settingName(section, key), section, key, value); +} + +void remove(QSettings& settings, const QString& section, const QString& key); +void removeSection(QSettings& settings, const QString& section); + + +class ScopedGroup +{ +public: + ScopedGroup(QSettings& s, const QString& name); + ~ScopedGroup(); + + ScopedGroup(const ScopedGroup&) = delete; + ScopedGroup& operator=(const ScopedGroup&) = delete; + + template + void set(const QString& key, const T& value) + { + setImpl(m_settings, settingName(m_name, key), "", key, value); + } + + void remove(const QString& key); + + QStringList keys() const; + + template + void for_each(F&& f) const + { + for (const QString& key : keys()) { + f(key); + } + } + + template + std::optional getOptional(const QString& key, std::optional def={}) const + { + return ::getOptional(m_settings, "", key, def); + } + + template + T get(const QString& key, T def={}) const + { + return ::get(m_settings, "", key, def); + } + +private: + QSettings& m_settings; + QString m_name; +}; + + +class ScopedReadArray +{ +public: + ScopedReadArray(QSettings& s, const QString& section); + ~ScopedReadArray(); + + ScopedReadArray(const ScopedReadArray&) = delete; + ScopedReadArray& operator=(const ScopedReadArray&) = delete; + + template + void for_each(F&& f) const + { + for (int i=0; i + std::optional getOptional(const QString& key, std::optional def={}) const + { + return ::getOptional(m_settings, "", key, def); + } + + template + T get(const QString& key, T def={}) const + { + return ::get(m_settings, "", key, def); + } + + int count() const; + QStringList keys() const; + +private: + QSettings& m_settings; + int m_count; +}; + + +class ScopedWriteArray +{ +public: + ScopedWriteArray(QSettings& s, const QString& section); + ~ScopedWriteArray(); + + ScopedWriteArray(const ScopedWriteArray&) = delete; + ScopedWriteArray& operator=(const ScopedWriteArray&) = delete; + + void next(); + + template + void set(const QString& key, const T& value) + { + const auto displayName = QString("%1/%2\\%3") + .arg(m_section) + .arg(m_i) + .arg(key); + + setImpl(m_settings, displayName, "", key, value); + } + +private: + QSettings& m_settings; + QString m_section; + int m_i; +}; + + +QString widgetNameWithTopLevel(const QWidget* widget); +QString widgetName(const QMainWindow* w); +QString widgetName(const QHeaderView* w); +QString widgetName(const ExpanderWidget* w); +QString widgetName(const QWidget* w); + +template +QString geoSettingName(const Widget* widget) +{ + return widgetName(widget) + "_geometry"; +} + +template +QString stateSettingName(const Widget* widget) +{ + return widgetName(widget) + "_state"; +} + +template +QString visibilitySettingName(const Widget* widget) +{ + return widgetName(widget) + "_visibility"; +} + +QString dockSettingName(const QDockWidget* dock); +QString indexSettingName(const QWidget* widget); +QString checkedSettingName(const QAbstractButton* b); + +void warnIfNotCheckable(const QAbstractButton* b); + +bool setWindowsCredential(const QString key, const QString data); +QString getWindowsCredential(const QString key); + +#endif // SETTINGSUTILITIES_H -- cgit v1.3.1 From 06218502ed5379555eda1504e6b05f2ef6dfb292 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 11 Sep 2019 23:34:54 -0400 Subject: fixes for ExpanderWidget removed dead code in MainWindow --- src/CMakeLists.txt | 3 --- src/mainwindow.cpp | 11 ----------- src/mainwindow.h | 3 --- src/modinfodialogconflicts.h | 2 +- src/settings.h | 6 +++--- src/settingsutilities.h | 6 ++++-- 6 files changed, 8 insertions(+), 23 deletions(-) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ea27184b..60822834 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -136,7 +136,6 @@ SET(organizer_SRCS apiuseraccount.cpp filerenamer.cpp texteditor.cpp - expanderwidget.cpp env.cpp envmetrics.cpp envmodule.cpp @@ -260,7 +259,6 @@ SET(organizer_HDRS apiuseraccount.h filerenamer.h texteditor.h - expanderwidget.h env.h envmetrics.h envmodule.h @@ -478,7 +476,6 @@ set(utilities ) set(widgets - expanderwidget genericicondelegate filerenamer filterwidget diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 657c1a27..054262e9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2561,17 +2561,6 @@ void MainWindow::modInstalled(const QString &modName) modUpdateCheck(IDs); } -void MainWindow::procError(QProcess::ProcessError error) -{ - reportError(tr("failed to spawn notepad.exe: %1").arg(error)); - this->sender()->deleteLater(); -} - -void MainWindow::procFinished(int, QProcess::ExitStatus) -{ - this->sender()->deleteLater(); -} - void MainWindow::showMessage(const QString &message) { MessageDialog::showMessage(message, this); diff --git a/src/mainwindow.h b/src/mainwindow.h index 6f06b9d5..1f997ab1 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -489,9 +489,6 @@ private slots: void doMoveOverwriteContentToMod(const QString &modAbsolutePath); void clearOverwrite(); - void procError(QProcess::ProcessError error); - void procFinished(int exitCode, QProcess::ExitStatus exitStatus); - // nexus related void checkModsForUpdates(); diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h index a77c2ac9..ad305dfc 100644 --- a/src/modinfodialogconflicts.h +++ b/src/modinfodialogconflicts.h @@ -33,7 +33,7 @@ signals: private: struct Expanders { - ExpanderWidget overwrite, overwritten, nonconflict; + MOBase::ExpanderWidget overwrite, overwritten, nonconflict; }; ConflictsTab* m_tab; diff --git a/src/settings.h b/src/settings.h index 91b87e29..815ed160 100644 --- a/src/settings.h +++ b/src/settings.h @@ -32,13 +32,13 @@ along with Mod Organizer. If not, see . namespace MOBase { class IPlugin; class IPluginGame; + class ExpanderWidget; } class QSplitter; class ServerList; class Settings; -class ExpanderWidget; // helper class that calls restoreGeometry() in the constructor and @@ -153,8 +153,8 @@ public: void saveState(const QSplitter* splitter); bool restoreState(QSplitter* splitter) const; - void saveState(const ExpanderWidget* expander); - bool restoreState(ExpanderWidget* expander) const; + void saveState(const MOBase::ExpanderWidget* expander); + bool restoreState(MOBase::ExpanderWidget* expander) const; void saveVisibility(const QWidget* w); bool restoreVisibility(QWidget* w, std::optional def={}) const; diff --git a/src/settingsutilities.h b/src/settingsutilities.h index d99abb06..c3eef12f 100644 --- a/src/settingsutilities.h +++ b/src/settingsutilities.h @@ -3,7 +3,9 @@ #include -class ExpanderWidget; +namespace MOBase { + class ExpanderWidget; +} template struct ValueConverter @@ -241,7 +243,7 @@ private: QString widgetNameWithTopLevel(const QWidget* widget); QString widgetName(const QMainWindow* w); QString widgetName(const QHeaderView* w); -QString widgetName(const ExpanderWidget* w); +QString widgetName(const MOBase::ExpanderWidget* w); QString widgetName(const QWidget* w); template -- cgit v1.3.1 From c50722100c485d2945082d573158a7083efe2f23 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 19 Sep 2019 10:11:56 -0400 Subject: removed helper.h and helper.cpp, merged into spawn --- src/CMakeLists.txt | 3 --- src/helper.cpp | 20 -------------------- src/helper.h | 26 -------------------------- src/main.cpp | 1 - src/organizercore.cpp | 1 - src/settingsdialogworkarounds.cpp | 2 +- src/spawn.cpp | 1 - 7 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 src/helper.cpp delete mode 100644 src/helper.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 60822834..7c29ff48 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -87,7 +87,6 @@ SET(organizer_SRCS waitingonclosedialog.cpp loadmechanism.cpp installationmanager.cpp - helper.cpp filedialogmemory.cpp executableslist.cpp editexecutablesdialog.cpp @@ -208,7 +207,6 @@ SET(organizer_HDRS waitingonclosedialog.h loadmechanism.h installationmanager.h - helper.h filedialogmemory.h executableslist.h editexecutablesdialog.h @@ -464,7 +462,6 @@ set(utilities csvbuilder shared/error_report eventfilter - helper shared/leaktrace persistentcookiejar serverinfo diff --git a/src/helper.cpp b/src/helper.cpp deleted file mode 100644 index 24446cb8..00000000 --- a/src/helper.cpp +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -// moved to spawn.cpp diff --git a/src/helper.h b/src/helper.h deleted file mode 100644 index 335b8647..00000000 --- a/src/helper.h +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef HELPER_H -#define HELPER_H - -// all helper code moved to spawn.h and spawn.cpp -#include "spawn.h" - -#endif // HELPER_H diff --git a/src/main.cpp b/src/main.cpp index 74b04970..ba988ae3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,7 +38,6 @@ along with Mod Organizer. If not, see . #include "executableslist.h" #include "singleinstance.h" #include "utility.h" -#include "helper.h" #include "loglist.h" #include "selectiondialog.h" #include "moapplication.h" diff --git a/src/organizercore.cpp b/src/organizercore.cpp index fbc9083b..0da5b604 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -33,7 +33,6 @@ #include "lockeddialog.h" #include "instancemanager.h" #include -#include "helper.h" #include "previewdialog.h" #include diff --git a/src/settingsdialogworkarounds.cpp b/src/settingsdialogworkarounds.cpp index f89b021c..ccbfcbfe 100644 --- a/src/settingsdialogworkarounds.cpp +++ b/src/settingsdialogworkarounds.cpp @@ -1,6 +1,6 @@ #include "settingsdialogworkarounds.h" #include "ui_settingsdialog.h" -#include "helper.h" +#include "spawn.h" #include WorkaroundsSettingsTab::WorkaroundsSettingsTab(Settings& s, SettingsDialog& d) diff --git a/src/spawn.cpp b/src/spawn.cpp index 6c524681..64766adf 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -33,7 +33,6 @@ along with Mod Organizer. If not, see . #include #include #include -#include "helper.h" #include #include #include -- cgit v1.3.1 From 51d664d76ce6b611e7a7585b209bad9d68fe65d7 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 27 Sep 2019 15:20:02 -0400 Subject: moved color stuff to ColorTable, now shows sample text and icons --- src/CMakeLists.txt | 3 + src/colortable.cpp | 278 ++++++++++++++++++++++++++++++++++++++++++ src/colortable.h | 28 +++++ src/icondelegate.cpp | 15 ++- src/icondelegate.h | 18 +-- src/modflagicondelegate.cpp | 114 +++++++++-------- src/modflagicondelegate.h | 11 +- src/settingsdialog.ui | 9 +- src/settingsdialoggeneral.cpp | 235 +++-------------------------------- src/settingsdialoggeneral.h | 9 -- 10 files changed, 421 insertions(+), 299 deletions(-) create mode 100644 src/colortable.cpp create mode 100644 src/colortable.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7c29ff48..b21d1a8b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,7 @@ SET(organizer_SRCS envsecurity.cpp envshortcut.cpp envwindows.cpp + colortable.cpp shared/windows_error.cpp shared/error_report.cpp @@ -263,6 +264,7 @@ SET(organizer_HDRS envsecurity.h envshortcut.h envwindows.h + colortable.h shared/windows_error.h shared/error_report.h @@ -473,6 +475,7 @@ set(utilities ) set(widgets + colortable genericicondelegate filerenamer filterwidget diff --git a/src/colortable.cpp b/src/colortable.cpp new file mode 100644 index 00000000..7546abe5 --- /dev/null +++ b/src/colortable.cpp @@ -0,0 +1,278 @@ +#include "colortable.h" +#include "modflagicondelegate.h" +#include "settings.h" + +class ColorItem; +ColorItem* colorItemForRow(QTableWidget* table, int row); + +void paintBackground( + QTableWidget* table, QPainter* p, const QStyleOptionViewItem& option, + const QModelIndex& index); + + +class ColoredBackgroundDelegate : public QStyledItemDelegate +{ +public: + ColoredBackgroundDelegate(QTableWidget* table) + : m_table(table) + { + } + + void paint( + QPainter* p, const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + paintBackground(m_table, p, option, index); + + QStyleOptionViewItem itemOption(option); + initStyleOption(&itemOption, index); + itemOption.state = QStyle::State_Enabled; + + QStyledItemDelegate::paint(p, itemOption, index); + } + +private: + QTableWidget* m_table; +}; + + +class FakeModFlagIconDelegate : public ModFlagIconDelegate +{ +public: + explicit FakeModFlagIconDelegate(QTableWidget* table) + : m_table(table) + { + } + + void paint( + QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + paintBackground(m_table, painter, option, index); + ModFlagIconDelegate::paintIcons(painter, option, index, getIcons(index)); + } + +protected: + QList getIcons(const QModelIndex &index) const override + { + const auto flags = { + ModInfo::FLAG_CONFLICT_MIXED, + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, + ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED, + ModInfo::FLAG_BACKUP, + ModInfo::FLAG_NOTENDORSED, + ModInfo::FLAG_NOTES, + ModInfo::FLAG_ALTERNATE_GAME + }; + + return getIconsForFlags(flags, false); + } + + size_t getNumIcons(const QModelIndex &index) const override + { + return getIcons(index).size(); + } + +private: + QTableWidget* m_table; +}; + + +class ColorItem : public QTableWidgetItem +{ +public: + ColorItem( + QString caption, QColor defaultColor, + std::function get, + std::function commit) : + m_caption(std::move(caption)), m_default(defaultColor), + m_get(get), m_commit(commit) + { + setText(m_caption); + set(get()); + } + + const QString& caption() const + { + return m_caption; + } + + QColor get() const + { + return m_temp; + } + + bool set(const QColor& c) + { + if (m_temp != c) { + m_temp = c; + return true; + } + + return false; + } + + void commit() + { + m_commit(m_temp); + } + + bool reset() + { + return set(m_default); + } + +private: + const QString m_caption; + const QColor m_default; + std::function m_get; + std::function m_commit; + QColor m_temp; +}; + + +ColorItem* colorItemForRow(QTableWidget* table, int row) +{ + return dynamic_cast(table->item(row, 0)); +} + +template +void forEachColorItem(QTableWidget* table, F&& f) +{ + const auto rowCount = table->rowCount(); + + for (int i=0; isave(); + p->fillRect(option.rect, ci->get()); + p->restore(); + } +} + + +ColorTable::ColorTable(QWidget* parent) + : QTableWidget(parent), m_settings(nullptr) +{ + setColumnCount(3); + setHorizontalHeaderLabels({"", "", ""}); + + setItemDelegateForColumn(1, new ColoredBackgroundDelegate(this)); + setItemDelegateForColumn(2, new FakeModFlagIconDelegate(this)); + + connect( + this, &QTableWidget::cellActivated, + [&]{ onColorActivated(); }); +} + +void ColorTable::load(Settings& s) +{ + m_settings = &s; + + addColor( + QObject::tr("Is overwritten (loose files)"), + QColor(0, 255, 0, 64), + [this]{ return m_settings->colors().modlistOverwrittenLoose(); }, + [this](auto&& v){ m_settings->colors().setModlistOverwrittenLoose(v); }); + + addColor( + QObject::tr("Is overwriting (loose files)"), + QColor(255, 0, 0, 64), + [this]{ return m_settings->colors().modlistOverwritingLoose(); }, + [this](auto&& v){ m_settings->colors().setModlistOverwritingLoose(v); }); + + addColor( + QObject::tr("Is overwritten (archives)"), + QColor(0, 255, 255, 64), + [this]{ return m_settings->colors().modlistOverwrittenArchive(); }, + [this](auto&& v){ m_settings->colors().setModlistOverwrittenArchive(v); }); + + addColor( + QObject::tr("Is overwriting (archives)"), + QColor(255, 0, 255, 64), + [this]{ return m_settings->colors().modlistOverwritingArchive(); }, + [this](auto&& v){ m_settings->colors().setModlistOverwritingArchive(v); }); + + addColor( + QObject::tr("Mod contains selected plugin"), + QColor(0, 0, 255, 64), + [this]{ return m_settings->colors().modlistContainsPlugin(); }, + [this](auto&& v){ m_settings->colors().setModlistContainsPlugin(v); }); + + addColor( + QObject::tr("Plugin is contained in selected mod"), + QColor(0, 0, 255, 64), + [this]{ return m_settings->colors().pluginListContained(); }, + [this](auto&& v){ m_settings->colors().setPluginListContained(v); }); +} + +void ColorTable::resetColors() +{ + bool changed = false; + + forEachColorItem(this, [&](auto* item) { + if (item->reset()) { + changed = true; + } + }); + + if (changed) { + update(); + } +} + +void ColorTable::commitColors() +{ + forEachColorItem(this, [](auto* item) { + item->commit(); + }); +} + +void ColorTable::addColor( + const QString& text, const QColor& defaultColor, + std::function get, + std::function commit) +{ + const auto r = rowCount(); + setRowCount(r + 1); + + auto* item = new ColorItem(text, defaultColor, get, commit); + + setItem(r, 0, item); + setItem(r, 1, new QTableWidgetItem("Text")); + setItem(r, 2, new QTableWidgetItem); + + resizeColumnsToContents(); +} + +void ColorTable::onColorActivated() +{ + const auto rows = selectionModel()->selectedRows(); + if (rows.isEmpty()) { + return; + } + + const auto row = rows[0].row(); + auto* ci = colorItemForRow(this, row); + if (!ci) { + return; + } + + const QColor result = QColorDialog::getColor( + ci->get(), topLevelWidget(), ci->caption(), QColorDialog::ShowAlphaChannel); + + if (result.isValid()) { + ci->set(result); + update(model()->index(row, 1)); + } +} diff --git a/src/colortable.h b/src/colortable.h new file mode 100644 index 00000000..c2b64a4d --- /dev/null +++ b/src/colortable.h @@ -0,0 +1,28 @@ +#ifndef COLORTABLE_H +#define COLORTABLE_H + +#include + +class Settings; + +class ColorTable : public QTableWidget +{ +public: + ColorTable(QWidget* parent=nullptr); + + void load(Settings& s); + void resetColors(); + void commitColors(); + +private: + Settings* m_settings; + + void addColor( + const QString& text, const QColor& defaultColor, + std::function get, + std::function commit); + + void onColorActivated(); +}; + +#endif // COLORTABLE_H diff --git a/src/icondelegate.cpp b/src/icondelegate.cpp index 39038f3c..03964263 100644 --- a/src/icondelegate.cpp +++ b/src/icondelegate.cpp @@ -32,13 +32,10 @@ IconDelegate::IconDelegate(QObject *parent) { } - -void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +void IconDelegate::paintIcons( + QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index, const QList& icons) { - QStyledItemDelegate::paint(painter, option, index); - - QList icons = getIcons(index); - int x = 4; painter->save(); @@ -67,3 +64,9 @@ void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, painter->restore(); } +void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyledItemDelegate::paint(painter, option, index); + paintIcons(painter, option, index, getIcons(index)); +} + diff --git a/src/icondelegate.h b/src/icondelegate.h index 39694481..bac71a62 100644 --- a/src/icondelegate.h +++ b/src/icondelegate.h @@ -27,25 +27,19 @@ along with Mod Organizer. If not, see . class IconDelegate : public QStyledItemDelegate { - Q_OBJECT -public: + Q_OBJECT; +public: explicit IconDelegate(QObject *parent = 0); - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; -signals: - -public slots: - -private: + static void paintIcons( + QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index, const QList& icons); +protected: virtual QList getIcons(const QModelIndex &index) const = 0; virtual size_t getNumIcons(const QModelIndex &index) const = 0; - - -private: - }; #endif // ICONDELEGATE_H diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp index 7110a590..a5e9aa22 100644 --- a/src/modflagicondelegate.cpp +++ b/src/modflagicondelegate.cpp @@ -31,72 +31,82 @@ void ModFlagIconDelegate::columnResized(int logicalIndex, int, int newSize) } } -QList ModFlagIconDelegate::getIcons(const QModelIndex &index) const { +QList ModFlagIconDelegate::getIconsForFlags( + std::vector flags, bool compact) +{ QList result; - QVariant modid = index.data(Qt::UserRole + 1); - if (modid.isValid()) { - ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt()); - std::vector flags = info->getFlags(); - // Don't do flags for overwrite - if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE) != flags.end()) - return result; - - // insert conflict icons to provide nicer alignment - { // insert loose file conflicts first - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ConflictFlags, m_ConflictFlags + 4); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!m_Compact) { - result.append(QString()); - } + // Don't do flags for overwrite + if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE) != flags.end()) + return result; + + // insert conflict icons to provide nicer alignment + { // insert loose file conflicts first + auto iter = std::find_first_of(flags.begin(), flags.end(), + m_ConflictFlags, m_ConflictFlags + 4); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); } + } - { // insert loose vs archive overwrite second - auto iter = std::find(flags.begin(), flags.end(), - ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!m_Compact) { - result.append(QString()); - } + { // insert loose vs archive overwrite second + auto iter = std::find(flags.begin(), flags.end(), + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); } + } - { // insert loose vs archive overwritten third - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!m_Compact) { - result.append(QString()); - } + { // insert loose vs archive overwritten third + auto iter = std::find_first_of(flags.begin(), flags.end(), + m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); } + } - { // insert archive conflicts last - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!m_Compact) { - result.append(QString()); - } + { // insert archive conflicts last + auto iter = std::find_first_of(flags.begin(), flags.end(), + m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); } + } - for (auto iter = flags.begin(); iter != flags.end(); ++iter) { - auto iconPath = getFlagIcon(*iter); - if (!iconPath.isEmpty()) - result.append(iconPath); - } + for (auto iter = flags.begin(); iter != flags.end(); ++iter) { + auto iconPath = getFlagIcon(*iter); + if (!iconPath.isEmpty()) + result.append(iconPath); } + return result; } -QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) const +QList ModFlagIconDelegate::getIcons(const QModelIndex &index) const +{ + QVariant modid = index.data(Qt::UserRole + 1); + + if (modid.isValid()) { + ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt()); + return getIconsForFlags(info->getFlags(), m_Compact); + } + + return {}; +} + +QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) { switch (flag) { case ModInfo::FLAG_BACKUP: return QStringLiteral(":/MO/gui/emblem_backup"); diff --git a/src/modflagicondelegate.h b/src/modflagicondelegate.h index eb6a76ab..4f22dd90 100644 --- a/src/modflagicondelegate.h +++ b/src/modflagicondelegate.h @@ -5,21 +5,24 @@ class ModFlagIconDelegate : public IconDelegate { -Q_OBJECT + Q_OBJECT; public: explicit ModFlagIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 120); virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + static QList getIconsForFlags( + std::vector flags, bool compact); + + static QString getFlagIcon(ModInfo::EFlag flag); + public slots: void columnResized(int logicalIndex, int oldSize, int newSize); -private: +protected: virtual QList getIcons(const QModelIndex &index) const; virtual size_t getNumIcons(const QModelIndex &index) const; - QString getFlagIcon(ModInfo::EFlag flag) const; - private: static ModInfo::EFlag m_ConflictFlags[4]; static ModInfo::EFlag m_ArchiveLooseConflictFlags[2]; diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 704e4134..fba65545 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -211,7 +211,7 @@ - + QAbstractItemView::NoEditTriggers @@ -1527,6 +1527,13 @@ programs you are intentionally running. + + + ColorTable + QTableWidget +
colortable.h
+
+
logLevelBox baseDirEdit diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index 0dfd3a08..a9ec5cae 100644 --- a/src/settingsdialoggeneral.cpp +++ b/src/settingsdialoggeneral.cpp @@ -2,114 +2,9 @@ #include "ui_settingsdialog.h" #include "appconfig.h" #include "categoriesdialog.h" +#include "colortable.h" #include -using MOBase::QuestionBoxMemory; - - -class ColorItem : public QTableWidgetItem -{ -public: - ColorItem( - const QColor& defaultColor, - std::function get, - std::function commit) - : m_default(defaultColor), m_get(get), m_commit(commit) - { - set(get()); - } - - QColor get() const - { - return m_temp; - } - - bool set(const QColor& c) - { - if (m_temp != c) { - m_temp = c; - return true; - } - - return false; - } - - void commit() - { - m_commit(m_temp); - } - - bool reset() - { - return set(m_default); - } - -private: - const QColor m_default; - std::function m_get; - std::function m_commit; - QColor m_temp; -}; - - -class ColorDelegate : public QStyledItemDelegate -{ -public: - ColorDelegate(QTableWidget* table) - : m_table(table) - { - } - -protected: - void paint( - QPainter* p, const QStyleOptionViewItem& option, - const QModelIndex& index) const override - { - if (!paintColor(p, option, index)) { - QStyledItemDelegate::paint(p, option, index); - } - } - -private: - QTableWidget* m_table; - - bool paintColor( - QPainter* p, const QStyleOptionViewItem& option, - const QModelIndex& index) const - { - if (index.column() != 1) { - return false; - } - - const auto* item = dynamic_cast( - m_table->item(index.row(), index.column())); - - if (!item) { - return false; - } - - p->save(); - p->fillRect(option.rect, item->get()); - p->restore(); - - return true; - } -}; - - -template -void forEachColorItem(QTableWidget* table, F&& f) -{ - const auto rowCount = table->rowCount(); - - for (int i=0; i(table->item(i, 1))) { - f(item); - } - } -} - - GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d) { @@ -119,17 +14,26 @@ GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) addStyles(); selectStyle(); - setColorTable(); + ui->colorTable->load(s); - QObject::connect(ui->categoriesBtn, &QPushButton::clicked, [&]{ on_categoriesBtn_clicked(); }); - QObject::connect(ui->resetColorsBtn, &QPushButton::clicked, [&]{ on_resetColorsBtn_clicked(); }); - QObject::connect(ui->resetDialogsButton, &QPushButton::clicked, [&]{ on_resetDialogsButton_clicked(); }); + QObject::connect( + ui->categoriesBtn, &QPushButton::clicked, + [&]{ on_categoriesBtn_clicked(); }); + + QObject::connect( + ui->resetColorsBtn, &QPushButton::clicked, + [&]{ on_resetColorsBtn_clicked(); }); + + QObject::connect( + ui->resetDialogsButton, &QPushButton::clicked, + [&]{ on_resetDialogsButton_clicked(); }); } void GeneralSettingsTab::update() { const QString oldLanguage = settings().interface().language(); - const QString newLanguage = ui->languageBox->itemData(ui->languageBox->currentIndex()).toString(); + const QString newLanguage = ui->languageBox->itemData( + ui->languageBox->currentIndex()).toString(); if (newLanguage != oldLanguage) { settings().interface().setLanguage(newLanguage); @@ -137,7 +41,9 @@ void GeneralSettingsTab::update() } const QString oldStyle = settings().interface().styleName().value_or(""); - const QString newStyle = ui->styleBox->itemData(ui->styleBox->currentIndex()).toString(); + const QString newStyle = ui->styleBox->itemData( + ui->styleBox->currentIndex()).toString(); + if (oldStyle != newStyle) { settings().interface().setStyleName(newStyle); emit settings().styleChanged(newStyle); @@ -149,9 +55,7 @@ void GeneralSettingsTab::update() settings().setCheckForUpdates(ui->checkForUpdates->isChecked()); settings().setUsePrereleases(ui->usePrereleaseBox->isChecked()); - forEachColorItem(ui->colorTable, [](auto* item) { - item->commit(); - }); + ui->colorTable->commitColors(); settings().colors().setColorSeparatorScrollbar(ui->colorSeparatorsBox->isChecked()); } @@ -258,113 +162,14 @@ void GeneralSettingsTab::selectStyle() } } -void GeneralSettingsTab::setColorTable() -{ - ui->colorTable->setColumnCount(2); - ui->colorTable->setHorizontalHeaderLabels({ - QObject::tr("Item"), QObject::tr("Color") - }); - - ui->colorTable->setItemDelegate(new ColorDelegate(ui->colorTable)); - - addColor( - QObject::tr("Is overwritten (loose files)"), - QColor(0, 255, 0, 64), - [this]{ return settings().colors().modlistOverwrittenLoose(); }, - [this](auto&& v){ settings().colors().setModlistOverwrittenLoose(v); }); - - addColor( - QObject::tr("Is overwriting (loose files)"), - QColor(255, 0, 0, 64), - [this]{ return settings().colors().modlistOverwritingLoose(); }, - [this](auto&& v){ settings().colors().setModlistOverwritingLoose(v); }); - - addColor( - QObject::tr("Is overwritten (archives)"), - QColor(0, 255, 255, 64), - [this]{ return settings().colors().modlistOverwrittenArchive(); }, - [this](auto&& v){ settings().colors().setModlistOverwrittenArchive(v); }); - - addColor( - QObject::tr("Is overwriting (archives)"), - QColor(255, 0, 255, 64), - [this]{ return settings().colors().modlistOverwritingArchive(); }, - [this](auto&& v){ settings().colors().setModlistOverwritingArchive(v); }); - - addColor( - QObject::tr("Mod contains selected plugin"), - QColor(0, 0, 255, 64), - [this]{ return settings().colors().modlistContainsPlugin(); }, - [this](auto&& v){ settings().colors().setModlistContainsPlugin(v); }); - - addColor( - QObject::tr("Plugin is contained in selected mod"), - QColor(0, 0, 255, 64), - [this]{ return settings().colors().pluginListContained(); }, - [this](auto&& v){ settings().colors().setPluginListContained(v); }); - - QObject::connect( - ui->colorTable, &QTableWidget::cellActivated, - [&]{ onColorActivated(); }); -} - -void GeneralSettingsTab::addColor( - const QString& text, const QColor& defaultColor, - std::function get, - std::function commit) -{ - const auto r = ui->colorTable->rowCount(); - ui->colorTable->setRowCount(r + 1); - - ui->colorTable->setItem(r, 0, new QTableWidgetItem(text)); - ui->colorTable->setItem(r, 1, new ColorItem(defaultColor, get, commit)); - - ui->colorTable->resizeColumnsToContents(); -} - void GeneralSettingsTab::resetDialogs() { settings().widgets().resetQuestionButtons(); } -void GeneralSettingsTab::onColorActivated() -{ - const auto rows = ui->colorTable->selectionModel()->selectedRows(); - if (rows.isEmpty()) { - return; - } - - const auto row = rows[0].row(); - - const auto text = ui->colorTable->item(row, 0)->text(); - auto* item = dynamic_cast(ui->colorTable->item(row, 1)); - - if (!item) { - return; - } - - const QColor result = QColorDialog::getColor( - item->get(), &dialog(), text, QColorDialog::ShowAlphaChannel); - - if (result.isValid()) { - item->set(result); - ui->colorTable->update(ui->colorTable->model()->index(row, 1)); - } -} - void GeneralSettingsTab::on_resetColorsBtn_clicked() { - bool changed = false; - - forEachColorItem(ui->colorTable, [&](auto* item) { - if (item->reset()) { - changed = true; - } - }); - - if (changed) { - ui->colorTable->update(); - } + ui->colorTable->resetColors(); } void GeneralSettingsTab::on_resetDialogsButton_clicked() diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h index 1f7fafff..706ba9ef 100644 --- a/src/settingsdialoggeneral.h +++ b/src/settingsdialoggeneral.h @@ -18,17 +18,8 @@ private: void addStyles(); void selectStyle(); - void setColorTable(); - void resetDialogs(); - void addColor( - const QString& text, const QColor& defaultColor, - std::function get, - std::function commit); - - void onColorActivated(); - void on_categoriesBtn_clicked(); void on_resetColorsBtn_clicked(); void on_resetDialogsButton_clicked(); -- cgit v1.3.1 From 633ef81972139f3c082429ada10ffb27d9f07898 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 27 Sep 2019 17:07:04 -0400 Subject: moved checks to sanitychecks.cpp added check for blocked files, only logs --- src/CMakeLists.txt | 2 + src/main.cpp | 46 +------------- src/sanitychecks.cpp | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 43 deletions(-) create mode 100644 src/sanitychecks.cpp (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b21d1a8b..180422ef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -142,6 +142,7 @@ SET(organizer_SRCS envshortcut.cpp envwindows.cpp colortable.cpp + sanitychecks.cpp shared/windows_error.cpp shared/error_report.cpp @@ -324,6 +325,7 @@ set(application mainwindow moapplication moshortcut + sanitychecks selfupdater singleinstance statusbar diff --git a/src/main.cpp b/src/main.cpp index ba988ae3..fe5fd87a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -91,6 +91,9 @@ along with Mod Organizer. If not, see . using namespace MOBase; using namespace MOShared; + +void sanityChecks(const env::Environment& env); + bool createAndMakeWritable(const std::wstring &subPath) { QString const dataPath = qApp->property("dataPath").toString(); QString fullPath = dataPath + "/" + QString::fromStdWString(subPath); @@ -496,49 +499,6 @@ static QString getVersionDisplayString() return createVersionInfo().displayString(3); } -void checkMissingFiles() -{ - // files that are likely to be eaten - static const QStringList files({ - "helper.exe", "nxmhandler.exe", - "usvfs_proxy_x64.exe", "usvfs_proxy_x86.exe", - "usvfs_x64.dll", "usvfs_x86.dll" - }); - - const auto dir = QCoreApplication::applicationDirPath(); - - for (const auto& name : files) { - const QFileInfo file(dir + QDir::separator() + name); - if (!file.exists()) { - log::warn( - "'{}' seems to be missing, an antivirus may have deleted it", - file.absoluteFilePath()); - } - } -} - -void checkNahimic(const env::Environment& e) -{ - for (auto&& m : e.loadedModules()) { - const QFileInfo file(m.path()); - - if (file.fileName().compare("NahimicOSD.dll", Qt::CaseInsensitive) == 0) { - log::warn( - "NahimicOSD.dll is loaded. Nahimic is known to cause issues with " - "Mod Organizer, such as freezing or blank windows. Consider " - "uninstalling it."); - - break; - } - } -} - -void sanityChecks(const env::Environment& e) -{ - checkMissingFiles(); - checkNahimic(e); -} - int runApplication(MOApplication &application, SingleInstance &instance, const QString &splashPath) diff --git a/src/sanitychecks.cpp b/src/sanitychecks.cpp new file mode 100644 index 00000000..5f9705b8 --- /dev/null +++ b/src/sanitychecks.cpp @@ -0,0 +1,174 @@ +#include "env.h" +#include "envmodule.h" +#include + +using namespace MOBase; + +enum class SecurityZone +{ + NoZone = -1, + MyComputer = 0, + Intranet = 1, + Trusted = 2, + Internet = 3, + Untrusted = 4, +}; + +QString toString(SecurityZone z) +{ + switch (z) + { + case SecurityZone::NoZone: return "NoZone"; + case SecurityZone::MyComputer: return "MyComputer"; + case SecurityZone::Intranet: return "Intranet"; + case SecurityZone::Trusted: return "Trusted"; + case SecurityZone::Internet: return "Internet"; + case SecurityZone::Untrusted: return "Untrusted"; + default: return QString("unknown (%1)").arg(static_cast(z)); + } +} + +bool isZoneBlocked(SecurityZone z) +{ + return (z == SecurityZone::Internet || z == SecurityZone::Untrusted); +} + +bool isFileBlocked(const QFileInfo& fi) +{ + const QString ads = "Zone.Identifier"; + const auto key = "ZoneTransfer/ZoneId"; + + const auto path = fi.absoluteFilePath(); + const auto adsPath = path + ":" + ads; + + QFile f(adsPath); + if (!f.exists()) { + return false; + } + + log::debug("file '{}' has an ADS for {}", path, adsPath); + + QSettings qs(adsPath, QSettings::IniFormat); + + if (!qs.contains(key)) { + log::debug("but key '{}' is not found", key); + return false; + } + + const auto v = qs.value(key); + if (v.isNull()) { + log::debug("but key '{}' is null", key); + return false; + } + + bool ok = false; + const auto z = static_cast(v.toInt(&ok)); + + if (!ok) { + log::debug( + "but key '{}' is not an int (value is '{}')", + key, v); + + return false; + } + + if (!isZoneBlocked(z)) { + log::debug( + "but zone id is {}, {}, which is fine", + static_cast(z), toString(z)); + + return false; + } + + log::warn( + "file '{}' is blocked (zone id is {}, {})", + path, static_cast(z), toString(z)); + + return true; +} + +void checkBlockedFiles(const QDir& dir) +{ + if (!dir.exists()) { + log::error( + "while checking for blocked files, directory '{}' not found", + dir.absolutePath()); + + return; + } + + const auto files = dir.entryInfoList({"*.dll", "*.exe"}, QDir::Files); + if (files.empty()) { + log::error( + "while checking for blocked files, directory '{}' is empty", + dir.absolutePath()); + + return; + } + + for (auto&& fi : files) { + isFileBlocked(fi); + } +} + +void checkBlocked() +{ + const QString appDir = QCoreApplication::applicationDirPath(); + + const QDir dirs[] = { + appDir, + appDir + "/dlls", + appDir + "/loot", + appDir + "/NCC", + appDir + "/platforms", + appDir + "/plugins" + }; + + for (const auto& d : dirs) { + checkBlockedFiles(d); + } +} + +void checkMissingFiles() +{ + // files that are likely to be eaten + static const QStringList files({ + "helper.exe", "nxmhandler.exe", + "usvfs_proxy_x64.exe", "usvfs_proxy_x86.exe", + "usvfs_x64.dll", "usvfs_x86.dll" + }); + + const auto dir = QCoreApplication::applicationDirPath(); + + for (const auto& name : files) { + const QFileInfo file(dir + QDir::separator() + name); + if (!file.exists()) { + log::warn( + "'{}' seems to be missing, an antivirus may have deleted it", + file.absoluteFilePath()); + } + } +} + +void checkNahimic(const env::Environment& e) +{ + for (auto&& m : e.loadedModules()) { + const QFileInfo file(m.path()); + + if (file.fileName().compare("NahimicOSD.dll", Qt::CaseInsensitive) == 0) { + log::warn( + "NahimicOSD.dll is loaded. Nahimic is known to cause issues with " + "Mod Organizer, such as freezing or blank windows. Consider " + "uninstalling it."); + + break; + } + } +} + +void sanityChecks(const env::Environment& e) +{ + checkBlocked(); + checkMissingFiles(); + checkNahimic(e); +} -- cgit v1.3.1 From 4d269c2e1a625e6d50b7e6272b4f474a921c6bfa Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 29 Oct 2019 11:34:06 -0400 Subject: split to processrunner added IUserInterface::qtWidget() put back IUserInterface in OrganizerCore now that there's a way to get the widget --- src/CMakeLists.txt | 3 + src/iuserinterface.h | 2 + src/main.cpp | 4 +- src/mainwindow.cpp | 13 +- src/mainwindow.h | 5 +- src/modinfodialogconflicts.cpp | 2 +- src/organizercore.cpp | 601 ++++++++------------------------------ src/organizercore.h | 60 +--- src/organizerproxy.cpp | 4 +- src/processrunner.cpp | 634 +++++++++++++++++++++++++++++++++++++++++ src/processrunner.h | 104 +++++++ src/spawn.cpp | 198 ------------- src/spawn.h | 48 ---- 13 files changed, 892 insertions(+), 786 deletions(-) create mode 100644 src/processrunner.cpp create mode 100644 src/processrunner.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 180422ef..5e909760 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -143,6 +143,7 @@ SET(organizer_SRCS envwindows.cpp colortable.cpp sanitychecks.cpp + processrunner.cpp shared/windows_error.cpp shared/error_report.cpp @@ -266,6 +267,7 @@ SET(organizer_HDRS envshortcut.h envwindows.h colortable.h + processrunner.h shared/windows_error.h shared/error_report.h @@ -348,6 +350,7 @@ set(core organizercore organizerproxy apiuseraccount + processrunner ) set(dialogs diff --git a/src/iuserinterface.h b/src/iuserinterface.h index a309ed9b..91487aee 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -33,6 +33,8 @@ public: virtual ILockedWaitingForProcess* lock() = 0; virtual void unlock() = 0; + + virtual QWidget* qtWidget() = 0; }; #endif // IUSERINTERFACE_H diff --git a/src/main.cpp b/src/main.cpp index 49ecc084..5ed7da5d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -632,7 +632,7 @@ int runApplication(MOApplication &application, SingleInstance &instance, if (MOShortcut shortcut{ arguments.at(1) }) { if (shortcut.hasExecutable()) { try { - organizer.runShortcut(shortcut); + organizer.processRunner().runShortcut(shortcut); return 0; } catch (const std::exception &e) { @@ -653,7 +653,7 @@ int runApplication(MOApplication &application, SingleInstance &instance, arguments.removeFirst(); // remove binary name // pass the remaining parameters to the binary try { - organizer.runExecutableOrExecutableFile( + organizer.processRunner().runExecutableOrExecutableFile( exeName, arguments, QString(), QString()); return 0; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index bbb63333..ce5280a6 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1312,7 +1312,7 @@ bool MainWindow::canExit() } m_exitAfterWait = true; - m_OrganizerCore.waitForAllUSVFSProcessesWithLock(); + m_OrganizerCore.processRunner().waitForAllUSVFSProcessesWithLock(); if (!m_exitAfterWait) { // if operation cancelled return false; } @@ -1536,7 +1536,7 @@ void MainWindow::startExeAction() } action->setEnabled(false); - m_OrganizerCore.runExecutable(*itor); + m_OrganizerCore.processRunner().runExecutable(*itor); action->setEnabled(true); } @@ -2294,6 +2294,11 @@ void MainWindow::unlock() } } +QWidget* MainWindow::qtWidget() +{ + return this; +} + void MainWindow::on_btnRefreshData_clicked() { m_OrganizerCore.refreshDirectoryStructure(); @@ -2363,7 +2368,7 @@ void MainWindow::on_startButton_clicked() forcedLibraries.clear(); } - m_OrganizerCore.runExecutableFile( + m_OrganizerCore.processRunner().runExecutableFile( selectedExecutable->binaryInfo(), selectedExecutable->arguments(), selectedExecutable->workingDirectory().length() != 0 ? @@ -5453,7 +5458,7 @@ void MainWindow::openDataFile() const QString path = m_ContextItem->data(0, Qt::UserRole).toString(); const QFileInfo targetInfo(path); - m_OrganizerCore.runFile(this, targetInfo); + m_OrganizerCore.processRunner().runFile(this, targetInfo); } void MainWindow::openDataOriginExplorer_clicked() diff --git a/src/mainwindow.h b/src/mainwindow.h index dbbd0bd9..19723480 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -118,8 +118,9 @@ public: void processUpdates(Settings& settings); - virtual ILockedWaitingForProcess* lock() override; - virtual void unlock() override; + ILockedWaitingForProcess* lock() override; + void unlock() override; + QWidget* qtWidget() override; bool addProfile(); void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives); diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 68b1be6b..758112cc 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -527,7 +527,7 @@ void ConflictsTab::openItems(QTreeView* tree) // the menu item is only shown for a single selection, but handle all of them // in case this changes for_each_in_selection(tree, [&](const ConflictItem* item) { - core().runFile(parentWidget(), item->fileName()); + core().processRunner().runFile(parentWidget(), item->fileName()); return true; }); } diff --git a/src/organizercore.cpp b/src/organizercore.cpp index eda64c1d..0f767f46 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1,5 +1,4 @@ #include "organizercore.h" -#include "mainwindow.h" #include "delayedfilewriter.h" #include "guessedvalue.h" #include "imodinterface.h" @@ -86,51 +85,13 @@ QStringList toStringList(InputIterator current, InputIterator end) return result; } -env::Process* getInterestingProcess(std::vector& processes) -{ - if (processes.empty()) { - return nullptr; - } - - // Certain process names we wish to "hide" for aesthetic reason: - const std::vector hiddenList = { - QFileInfo(QCoreApplication::applicationFilePath()).fileName() - }; - - auto isHidden = [&](auto&& p) { - for (auto h : hiddenList) { - if (p.name().contains(h, Qt::CaseInsensitive)) { - return true; - } - } - - return false; - }; - - - for (auto&& root : processes) { - if (!isHidden(root)) { - return &root; - } - - for (auto&& child : root.children()) { - if (!isHidden(child)) { - return &child; - } - } - } - - - // everything is hidden, just pick the first one - return &processes[0]; -} - OrganizerCore::OrganizerCore(Settings &settings) - : m_MainWindow(nullptr) + : m_UserInterface(nullptr) , m_PluginContainer(nullptr) , m_GameName() , m_CurrentProfile(nullptr) + , m_Runner(*this) , m_Settings(settings) , m_Updater(NexusInterface::instance(m_PluginContainer)) , m_AboutToRun() @@ -190,7 +151,7 @@ OrganizerCore::~OrganizerCore() m_RefresherThread.exit(); m_RefresherThread.wait(); - prepareStart(); + saveCurrentProfile(); // profile has to be cleaned up before the modinfo-buffer is cleared delete m_CurrentProfile; @@ -249,43 +210,49 @@ void OrganizerCore::updateExecutablesList() m_PluginContainer, m_Settings.interface().displayForeign(), managedGame()); } -void OrganizerCore::setUserInterface(MainWindow* mainWindow) +void OrganizerCore::setUserInterface(IUserInterface* ui) { storeSettings(); - m_MainWindow = mainWindow; + m_UserInterface = ui; + + QWidget* w = nullptr; + if (m_UserInterface) { + w = m_UserInterface->qtWidget(); + } - if (m_MainWindow != nullptr) { - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), m_MainWindow, + if (w) { + connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), w, SLOT(modlistChanged(QModelIndex, int))); - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), m_MainWindow, + connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), w, SLOT(modlistChanged(QModelIndexList, int))); - connect(&m_ModList, SIGNAL(showMessage(QString)), m_MainWindow, + connect(&m_ModList, SIGNAL(showMessage(QString)), w, SLOT(showMessage(QString))); - connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), m_MainWindow, + connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), w, SLOT(modRenamed(QString, QString))); - connect(&m_ModList, SIGNAL(modUninstalled(QString)), m_MainWindow, + connect(&m_ModList, SIGNAL(modUninstalled(QString)), w, SLOT(modRemoved(QString))); - connect(&m_ModList, SIGNAL(removeSelectedMods()), m_MainWindow, + connect(&m_ModList, SIGNAL(removeSelectedMods()), w, SLOT(removeMod_clicked())); - connect(&m_ModList, SIGNAL(clearOverwrite()), m_MainWindow, + connect(&m_ModList, SIGNAL(clearOverwrite()), w, SLOT(clearOverwrite())); - connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), m_MainWindow, + connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), w, SLOT(displayColumnSelection(QPoint))); - connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), m_MainWindow, + connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), w, SLOT(fileMoved(QString, QString, QString))); - connect(&m_ModList, SIGNAL(modorder_changed()), m_MainWindow, + connect(&m_ModList, SIGNAL(modorder_changed()), w, SLOT(modorder_changed())); - connect(&m_PluginList, SIGNAL(writePluginsList()), m_MainWindow, + connect(&m_PluginList, SIGNAL(writePluginsList()), w, SLOT(esplist_changed())); - connect(&m_PluginList, SIGNAL(esplist_changed()), m_MainWindow, + connect(&m_PluginList, SIGNAL(esplist_changed()), w, SLOT(esplist_changed())); - connect(&m_DownloadManager, SIGNAL(showMessage(QString)), m_MainWindow, + connect(&m_DownloadManager, SIGNAL(showMessage(QString)), w, SLOT(showMessage(QString))); } - m_InstallationManager.setParentWidget(m_MainWindow); - m_Updater.setUserInterface(m_MainWindow); + m_InstallationManager.setParentWidget(w); + m_Updater.setUserInterface(w); + m_Runner.setUserInterface(ui); checkForUpdates(); } @@ -294,7 +261,7 @@ void OrganizerCore::checkForUpdates() { // this currently wouldn't work reliably if the ui isn't initialized yet to // display the result - if (m_MainWindow != nullptr) { + if (m_UserInterface != nullptr) { m_Updater.testForUpdate(m_Settings); } } @@ -390,7 +357,7 @@ void OrganizerCore::externalMessage(const QString &message) { if (MOShortcut moshortcut{ message } ) { if(moshortcut.hasExecutable()) - runShortcut(moshortcut); + m_Runner.runShortcut(moshortcut); } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); @@ -754,13 +721,13 @@ MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName, int modIndex = ModInfo::getIndex(modName); if (modIndex != UINT_MAX) { ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (hasIniTweaks && (m_MainWindow != nullptr) + if (hasIniTweaks && (m_UserInterface != nullptr) && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), tr("This mod contains ini tweaks. Do you " "want to configure them now?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - m_MainWindow->displayModInformation( + m_UserInterface->displayModInformation( modInfo, modIndex, ModInfoTabIDs::IniFiles); } m_ModInstalled(modName); @@ -821,13 +788,13 @@ void OrganizerCore::installDownload(int index) ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); modInfo->addInstalledFile(modID, fileID); - if (hasIniTweaks && m_MainWindow != nullptr + if (hasIniTweaks && m_UserInterface != nullptr && (QMessageBox::question(qApp->activeWindow(), tr("Configure Mod"), tr("This mod contains ini tweaks. Do you " "want to configure them now?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - m_MainWindow->displayModInformation( + m_UserInterface->displayModInformation( modInfo, modIndex, ModInfoTabIDs::IniFiles); } @@ -1104,412 +1071,6 @@ bool OrganizerCore::previewFile( return true; } -bool OrganizerCore::runFile( - QWidget* parent, const QFileInfo& targetInfo) -{ - const auto fec = spawn::getFileExecutionContext(parent, targetInfo); - - switch (fec.type) - { - case spawn::FileExecutionTypes::Executable: - { - runExecutableFile(fec.binary, fec.arguments, targetInfo.absoluteDir()); - return true; - } - - case spawn::FileExecutionTypes::Other: // fall-through - default: - { - auto r = shell::Open(targetInfo.absoluteFilePath()); - if (!r.success()) { - return false; - } - - // not all files will return a valid handle even if opening them was - // successful, such as inproc handlers (like the photo viewer) - if (r.processHandle() != INVALID_HANDLE_VALUE) { - // steal because it gets closed after the wait - return waitForProcessCompletionWithLock(r.stealProcessHandle(), nullptr); - } - - return true; - } - } -} - -bool OrganizerCore::runExecutableFile( - const QFileInfo &binary, const QString &arguments, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - bool refresh) -{ - DWORD processExitCode = 0; - HANDLE processHandle = spawnAndWait( - binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, - customOverwrite, forcedLibraries, &processExitCode); - - if (processHandle == INVALID_HANDLE_VALUE) { - // failed - return false; - } - - if (refresh) { - refreshDirectoryStructure(); - - // need to remove our stored load order because it may be outdated if a foreign tool changed the - // file time. After removing that file, refreshESPList will use the file time as the order - if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { - log::debug("removing loadorder.txt"); - QFile::remove(m_CurrentProfile->getLoadOrderFileName()); - } - - refreshDirectoryStructure(); - - refreshESPList(true); - savePluginList(); - - //These callbacks should not fiddle with directoy structure and ESPs. - m_FinishedRun(binary.absoluteFilePath(), processExitCode); - } - - return true; -} - -bool OrganizerCore::runExecutable(const Executable& exe, bool refresh) -{ - const QString customOverwrite = m_CurrentProfile->setting( - "custom_overwrites", exe.title()).toString(); - - QList forcedLibraries; - - if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); - } - - return runExecutableFile( - exe.binaryInfo(), - exe.arguments(), - exe.workingDirectory().length() != 0 ? exe.workingDirectory() : exe.binaryInfo().absolutePath(), - exe.steamAppID(), - customOverwrite, - forcedLibraries, - refresh); -} - -bool OrganizerCore::runShortcut(const MOShortcut& shortcut) -{ - if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) { - throw std::runtime_error( - QString("Refusing to run executable from different instance %1:%2") - .arg(shortcut.instance(),shortcut.executable()) - .toLocal8Bit().constData()); - } - - const Executable& exe = m_ExecutablesList.get(shortcut.executable()); - return runExecutable(exe, false); -} - -HANDLE OrganizerCore::runExecutableOrExecutableFile( - const QString &executable, const QStringList &args, const QString &cwd, - const QString &profile, const QString &forcedCustomOverwrite, - bool ignoreCustomOverwrite) -{ - QString profileName = profile; - if (profile == "") { - if (m_CurrentProfile != nullptr) { - profileName = m_CurrentProfile->name(); - } else { - throw MyException(tr("No profile set")); - } - } - - QFileInfo binary; - QString arguments = args.join(" "); - QString currentDirectory = cwd; - QString steamAppID; - QString customOverwrite; - QList forcedLibraries; - - if (executable.contains('\\') || executable.contains('/')) { - // file path - - binary = QFileInfo(executable); - if (binary.isRelative()) { - // relative path, should be relative to game directory - binary = managedGame()->gameDirectory().absoluteFilePath(executable); - } - - if (currentDirectory == "") { - currentDirectory = binary.absolutePath(); - } - - try { - const Executable& exe = m_ExecutablesList.getByBinary(binary); - steamAppID = exe.steamAppID(); - customOverwrite = m_CurrentProfile->setting("custom_overwrites", exe.title()).toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); - } - } catch (const std::runtime_error &) { - // nop - } - } else { - // only a file name, search executables list - try { - const Executable &exe = m_ExecutablesList.get(executable); - steamAppID = exe.steamAppID(); - customOverwrite = m_CurrentProfile->setting("custom_overwrites", exe.title()).toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); - } - if (arguments == "") { - arguments = exe.arguments(); - } - binary = exe.binaryInfo(); - if (currentDirectory == "") { - currentDirectory = exe.workingDirectory(); - } - } catch (const std::runtime_error &) { - log::warn("\"{}\" not set up as executable", executable); - binary = QFileInfo(executable); - } - } - - if (!forcedCustomOverwrite.isEmpty()) - customOverwrite = forcedCustomOverwrite; - - if (ignoreCustomOverwrite) - customOverwrite.clear(); - - return spawnAndWait(binary, - arguments, - profileName, - currentDirectory, - steamAppID, - customOverwrite, - forcedLibraries); -} - -HANDLE OrganizerCore::spawnAndWait( - const QFileInfo &binary, const QString &arguments, const QString &profileName, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - LPDWORD exitCode) -{ - spawn::SpawnParameters sp; - sp.binary = binary; - sp.arguments = arguments; - sp.currentDirectory = currentDirectory; - sp.steamAppID = steamAppID; - sp.hooked = true; - - prepareStart(); - - while (m_DirectoryUpdate) { - ::Sleep(100); - QCoreApplication::processEvents(); - } - - // need to make sure all data is saved before we start the application - if (m_CurrentProfile != nullptr) { - m_CurrentProfile->writeModlistNow(true); - } - - // TODO: should also pass arguments - if (!m_AboutToRun(binary.absoluteFilePath())) { - log::debug("start of \"{}\" canceled by plugin", binary.absoluteFilePath()); - return INVALID_HANDLE_VALUE; - } - - try { - m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); - m_USVFS.updateForcedLibraries(forcedLibraries); - - } catch (const UsvfsConnectorException &e) { - log::debug(e.what()); - return INVALID_HANDLE_VALUE; - } catch (const std::exception &e) { - QMessageBox::warning(m_MainWindow, tr("Error"), e.what()); - return INVALID_HANDLE_VALUE; - } - - HANDLE handle = spawn::Spawner() - .spawn(m_MainWindow, m_GamePlugin, sp, m_Settings) - .releaseHandle(); - - if (handle == INVALID_HANDLE_VALUE) { - // failed - return INVALID_HANDLE_VALUE; - } - - waitForProcessCompletionWithLock(handle, exitCode); - return handle; -} - -void OrganizerCore::withLock(std::function f) -{ - std::unique_ptr dlg; - ILockedWaitingForProcess* uilock = nullptr; - - if (m_MainWindow != nullptr) { - uilock = m_MainWindow->lock(); - } - else { - // i.e. when running command line shortcuts there is no user interface - dlg.reset(new LockedDialog); - dlg->show(); - dlg->setEnabled(true); - uilock = dlg.get(); - } - - ON_BLOCK_EXIT([&]() { - if (m_MainWindow != nullptr) { - m_MainWindow->unlock(); - } }); - - f(uilock); -} - -bool OrganizerCore::waitForProcessCompletionWithLock( - HANDLE handle, LPDWORD exitCode) -{ - if (!Settings::instance().interface().lockGUI()) { - return true; - } - - bool r = false; - - withLock([&](auto* uilock) { - DWORD ignoreExitCode; - r = waitForProcessCompletion(handle, exitCode ? exitCode : &ignoreExitCode, uilock); - cycleDiagnostics(); - }); - - return r; -} - -bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode) -{ - if (!Settings::instance().interface().lockGUI()) - return true; - - bool r = false; - - withLock([&](auto* uilock) { - r = waitForProcessCompletion(handle, exitCode, uilock); - }); - - return r; -} - -bool OrganizerCore::waitForProcessCompletion( - HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) -{ - const auto tree = env::getProcessTree(handle); - std::vector processes = {tree}; - - const auto* interesting = getInterestingProcess(processes); - if (!interesting) { - return true; - } - - if (uilock) { - uilock->setProcessInformation(interesting->pid(), interesting->name()); - } - - auto interestingHandle = interesting->openHandleForWait(); - if (!interestingHandle) { - return true; - } - - auto progress = [&]{ return uilock->unlockForced(); }; - const auto r = spawn::waitForProcess( - interestingHandle.get(), exitCode, progress); - - switch (r) - { - case spawn::WaitResults::Completed: // fall-through - case spawn::WaitResults::Cancelled: - return true; - - case spawn::WaitResults::Error: // fall-through - default: - return false; - } -} - -bool OrganizerCore::waitForAllUSVFSProcessesWithLock() -{ - if (!Settings::instance().interface().lockGUI()) - return true; - - bool r = false; - - withLock([&](auto* uilock) { - r = waitForAllUSVFSProcesses(uilock); - }); - - return r; -} - -bool OrganizerCore::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) -{ - for (;;) { - const auto handles = getRunningUSVFSProcesses(); - if (handles.empty()) { - break; - } - - std::vector processes; - for (auto&& h : handles) { - auto p = env::getProcessTree(h); - if (p.isValid()) { - processes.emplace_back(std::move(p)); - } - } - - const auto* interesting = getInterestingProcess(processes); - if (!interesting) { - break; - } - - if (uilock) { - uilock->setProcessInformation(interesting->pid(), interesting->name()); - } - - auto interestingHandle = interesting->openHandleForWait(); - if (!interestingHandle) { - break; - } - - auto progress = [&]{ return uilock->unlockForced(); }; - const auto r = spawn::waitForProcess( - interestingHandle.get(), nullptr, progress); - - switch (r) - { - case spawn::WaitResults::Completed: - // this process is completed, check for others - break; - - case spawn::WaitResults::Cancelled: - // force unlocked - log::debug("waiting for process completion aborted by UI"); - return true; - - case spawn::WaitResults::Error: // fall-through - default: - log::debug("waiting for process completion not successful"); - return false; - } - } - - log::debug("waiting for process completion successful"); - return true; -} - bool OrganizerCore::onAboutToRun( const std::function &func) { @@ -1594,8 +1155,8 @@ void OrganizerCore::refreshBSAList() m_ActiveArchives = m_DefaultArchives; } - if (m_MainWindow != nullptr) { - m_MainWindow->updateBSAList(m_DefaultArchives, m_ActiveArchives); + if (m_UserInterface != nullptr) { + m_UserInterface->updateBSAList(m_DefaultArchives, m_ActiveArchives); } m_ArchivesInit = true; @@ -1711,8 +1272,8 @@ void OrganizerCore::updateModsInDirectoryStructure(QMaparchivesWriter().writeImmediately(false); + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().writeImmediately(false); } std::vector archives = enabledArchives(); @@ -1896,8 +1457,8 @@ void OrganizerCore::modStatusChanged(unsigned int index) = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); origin.enable(false); } - if (m_MainWindow != nullptr) { - m_MainWindow->archivesWriter().write(); + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().write(); } } modInfo->clearCaches(); @@ -1946,8 +1507,8 @@ void OrganizerCore::modStatusChanged(QList index) { origin.enable(false); } } - if (m_MainWindow != nullptr) { - m_MainWindow->archivesWriter().write(); + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().write(); } } @@ -2122,8 +1683,8 @@ bool OrganizerCore::saveCurrentLists() try { savePluginList(); - if (m_MainWindow != nullptr) { - m_MainWindow->archivesWriter().write(); + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().write(); } } catch (const std::exception &e) { reportError(tr("failed to save load order: %1").arg(e.what())); @@ -2147,11 +1708,12 @@ void OrganizerCore::savePluginList() m_PluginList.saveLoadOrder(*m_DirectoryStructure); } -void OrganizerCore::prepareStart() +void OrganizerCore::saveCurrentProfile() { if (m_CurrentProfile == nullptr) { return; } + m_CurrentProfile->writeModlist(); m_CurrentProfile->createTweakedIniFile(); saveCurrentLists(); @@ -2159,6 +1721,79 @@ void OrganizerCore::prepareStart() storeSettings(); } +ProcessRunner& OrganizerCore::processRunner() +{ + return m_Runner; +} + +bool OrganizerCore::beforeRun( + const QFileInfo& binary, const QString& profileName, + const QString& customOverwrite, + const QList& forcedLibraries) +{ + saveCurrentProfile(); + + while (m_DirectoryUpdate) { + ::Sleep(100); + QCoreApplication::processEvents(); + } + + // need to make sure all data is saved before we start the application + if (m_CurrentProfile != nullptr) { + m_CurrentProfile->writeModlistNow(true); + } + + // TODO: should also pass arguments + if (!m_AboutToRun(binary.absoluteFilePath())) { + log::debug("start of \"{}\" cancelled by plugin", binary.absoluteFilePath()); + return false; + } + + try + { + m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); + m_USVFS.updateForcedLibraries(forcedLibraries); + } + catch (const UsvfsConnectorException &e) + { + log::debug(e.what()); + return false; + } + catch (const std::exception &e) + { + QWidget* w = nullptr; + if (m_UserInterface) { + w = m_UserInterface->qtWidget(); + } + QMessageBox::warning(w, tr("Error"), e.what()); + return false; + } + + return true; +} + +void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) +{ + refreshDirectoryStructure(); + + // need to remove our stored load order because it may be outdated if a + // foreign tool changed the file time. After removing that file, + // refreshESPList will use the file time as the order + if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { + log::debug("removing loadorder.txt"); + QFile::remove(m_CurrentProfile->getLoadOrderFileName()); + } + + refreshDirectoryStructure(); + + refreshESPList(true); + savePluginList(); + cycleDiagnostics(); + + //These callbacks should not fiddle with directoy structure and ESPs. + m_FinishedRun(binary.absoluteFilePath(), exitCode); +} + std::vector OrganizerCore::fileMapping(const QString &profileName, const QString &customOverwrite) { diff --git a/src/organizercore.h b/src/organizercore.h index ffdb6830..2252c118 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -3,7 +3,6 @@ #include "selfupdater.h" -#include "ilockedwaitingforprocess.h" #include "settings.h" #include "modlist.h" #include "modinfo.h" @@ -14,6 +13,7 @@ #include "executableslist.h" #include "usvfsconnector.h" #include "moshortcut.h" +#include "processrunner.h" #include #include #include @@ -26,7 +26,7 @@ class ModListSortProxy; class PluginListSortProxy; class Profile; -class MainWindow; +class IUserInterface; namespace MOBase { template class GuessedValue; @@ -97,7 +97,7 @@ public: ~OrganizerCore(); - void setUserInterface(MainWindow* mainWindow); + void setUserInterface(IUserInterface* ui); void connectPlugins(PluginContainer *container); void disconnectPlugins(); @@ -134,7 +134,14 @@ public: bool saveCurrentLists(); - void prepareStart(); + ProcessRunner& processRunner(); + + bool beforeRun( + const QFileInfo& binary, const QString& profileName, + const QString& customOverwrite, + const QList& forcedLibraries); + + void afterRun(const QFileInfo& binary, DWORD exitCode); void refreshESPList(bool force = false); void refreshBSAList(); @@ -149,27 +156,6 @@ public: bool previewFileWithAlternatives(QWidget* parent, QString filename, int selectedOrigin=-1); bool previewFile(QWidget* parent, const QString& originName, const QString& path); - bool runFile(QWidget* parent, const QFileInfo& targetInfo); - - bool runExecutableFile( - const QFileInfo &binary, const QString &arguments, - const QDir ¤tDirectory, const QString &steamAppID={}, - const QString &customOverwrite={}, - const QList &forcedLibraries={}, - bool refresh=true); - - bool runExecutable(const Executable& exe, bool refresh=true); - - bool runShortcut(const MOShortcut& shortcut); - - HANDLE runExecutableOrExecutableFile( - const QString &executable, const QStringList &args, const QString &cwd, - const QString &profile, const QString &forcedCustomOverwrite = "", - bool ignoreCustomOverwrite = false); - - bool waitForApplication(HANDLE processHandle, LPDWORD exitCode = nullptr); - bool waitForAllUSVFSProcessesWithLock(); - void loginSuccessfulUpdate(bool necessary); void loginFailedUpdate(const QString &message); @@ -273,6 +259,7 @@ signals: private: + void saveCurrentProfile(); void storeSettings(); bool queryApi(QString &apiKey); @@ -298,26 +285,6 @@ private: const MOShared::DirectoryEntry *directoryEntry, int createDestination); - HANDLE spawnAndWait(const QFileInfo &binary, const QString &arguments, - const QString &profileName, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries = QList(), - LPDWORD exitCode = nullptr); - - bool waitForProcessCompletionWithLock(HANDLE handle, LPDWORD exitCode); - - bool waitForProcessCompletion( - HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock); - - bool waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock); - - void withLock(std::function f); - - HANDLE findAndOpenAUSVFSProcess( - const std::vector& hiddenList, DWORD preferedParentPid); - private slots: void directory_refreshed(); @@ -331,13 +298,14 @@ private: static const unsigned int PROBLEM_MO1SCRIPTEXTENDERWORKAROUND = 1; private: - MainWindow* m_MainWindow; + IUserInterface* m_UserInterface; PluginContainer *m_PluginContainer; QString m_GameName; MOBase::IPluginGame *m_GamePlugin; Profile *m_CurrentProfile; + ProcessRunner m_Runner; Settings& m_Settings; SelfUpdater m_Updater; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 2ea1761a..75b3ea41 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -112,14 +112,14 @@ HANDLE OrganizerProxy::startApplication( const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite) { - return m_Proxied->runExecutableOrExecutableFile( + return m_Proxied->processRunner().runExecutableOrExecutableFile( executable, args, cwd, profile, forcedCustomOverwrite, ignoreCustomOverwrite); } bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const { - return m_Proxied->waitForApplication(handle, exitCode); + return m_Proxied->processRunner().waitForApplication(handle, exitCode); } bool OrganizerProxy::onAboutToRun(const std::function &func) diff --git a/src/processrunner.cpp b/src/processrunner.cpp new file mode 100644 index 00000000..81c4b99e --- /dev/null +++ b/src/processrunner.cpp @@ -0,0 +1,634 @@ +#include "processrunner.h" +#include "organizercore.h" +#include "instancemanager.h" +#include "lockeddialog.h" +#include "iuserinterface.h" +#include "envmodule.h" +#include + +using namespace MOBase; + +void adjustForVirtualized( + const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) +{ + const QString modsPath = settings.paths().mods(); + + // Check if this a request with either an executable or a working directory + // under our mods folder then will start the process in a virtualized + // "environment" with the appropriate paths fixed: + // (i.e. mods\FNIS\path\exe => game\data\path\exe) + QString cwdPath = sp.currentDirectory.absolutePath(); + bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); + if (virtualizedCwd || virtualizedBin) { + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = game->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', modsPath.length() + 1); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = game->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + QString cmdline + = QString("launch \"%1\" \"%2\" %3") + .arg(QDir::toNativeSeparators(cwdPath), + QDir::toNativeSeparators(binPath), sp.arguments); + + sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); + sp.arguments = cmdline; + sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + } +} + +env::Process* getInterestingProcess(std::vector& processes) +{ + if (processes.empty()) { + return nullptr; + } + + // Certain process names we wish to "hide" for aesthetic reason: + const std::vector hiddenList = { + QFileInfo(QCoreApplication::applicationFilePath()).fileName() + }; + + auto isHidden = [&](auto&& p) { + for (auto h : hiddenList) { + if (p.name().contains(h, Qt::CaseInsensitive)) { + return true; + } + } + + return false; + }; + + + for (auto&& root : processes) { + if (!isHidden(root)) { + return &root; + } + + for (auto&& child : root.children()) { + if (!isHidden(child)) { + return &child; + } + } + } + + + // everything is hidden, just pick the first one + return &processes[0]; +} + + +SpawnedProcess::SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp) + : m_handle(handle), m_parameters(std::move(sp)) +{ +} + +SpawnedProcess::SpawnedProcess(SpawnedProcess&& other) + : m_handle(other.m_handle), m_parameters(std::move(other.m_parameters)) +{ + other.m_handle = INVALID_HANDLE_VALUE; +} + +SpawnedProcess& SpawnedProcess::operator=(SpawnedProcess&& other) +{ + if (this != &other) { + destroy(); + + m_handle = other.m_handle; + other.m_handle = INVALID_HANDLE_VALUE; + + m_parameters = std::move(other.m_parameters); + } + + return *this; +} + +SpawnedProcess::~SpawnedProcess() +{ + destroy(); +} + +HANDLE SpawnedProcess::releaseHandle() +{ + const auto h = m_handle; + m_handle = INVALID_HANDLE_VALUE; + return h; +} + +void SpawnedProcess::destroy() +{ + if (m_handle != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } +} + + +ProcessRunner::ProcessRunner(OrganizerCore& core) + : m_core(core), m_ui(nullptr) +{ +} + +void ProcessRunner::setUserInterface(IUserInterface* ui) +{ + m_ui = ui; +} + +bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) +{ + if (!parent && m_ui) { + parent = m_ui->qtWidget(); + } + + const auto fec = spawn::getFileExecutionContext(parent, targetInfo); + + switch (fec.type) + { + case spawn::FileExecutionTypes::Executable: + { + runExecutableFile(fec.binary, fec.arguments, targetInfo.absoluteDir()); + return true; + } + + case spawn::FileExecutionTypes::Other: // fall-through + default: + { + auto r = shell::Open(targetInfo.absoluteFilePath()); + if (!r.success()) { + return false; + } + + // not all files will return a valid handle even if opening them was + // successful, such as inproc handlers (like the photo viewer) + if (r.processHandle() != INVALID_HANDLE_VALUE) { + // steal because it gets closed after the wait + return waitForProcessCompletionWithLock(r.stealProcessHandle(), nullptr); + } + + return true; + } + } +} + +bool ProcessRunner::runExecutableFile( + const QFileInfo &binary, const QString &arguments, + const QDir ¤tDirectory, const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + bool refresh) +{ + DWORD processExitCode = 0; + HANDLE processHandle = spawnAndWait( + binary, arguments, m_core.currentProfile()->name(), + currentDirectory, steamAppID, customOverwrite, forcedLibraries, + &processExitCode); + + if (processHandle == INVALID_HANDLE_VALUE) { + // failed + return false; + } + + if (refresh) { + m_core.afterRun(binary, processExitCode); + } + + return true; +} + +bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) +{ + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } + + const QString customOverwrite = profile->setting( + "custom_overwrites", exe.title()).toString(); + + QList forcedLibraries; + + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + + return runExecutableFile( + exe.binaryInfo(), + exe.arguments(), + exe.workingDirectory().length() != 0 ? exe.workingDirectory() : exe.binaryInfo().absolutePath(), + exe.steamAppID(), + customOverwrite, + forcedLibraries, + refresh); +} + +bool ProcessRunner::runShortcut(const MOShortcut& shortcut) +{ + if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) { + throw std::runtime_error( + QString("Refusing to run executable from different instance %1:%2") + .arg(shortcut.instance(),shortcut.executable()) + .toLocal8Bit().constData()); + } + + const Executable& exe = m_core.executablesList()->get(shortcut.executable()); + return runExecutable(exe, false); +} + +HANDLE ProcessRunner::runExecutableOrExecutableFile( + const QString& executable, const QStringList &args, const QString &cwd, + const QString& profileOverride, const QString &forcedCustomOverwrite, + bool ignoreCustomOverwrite) +{ + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } + + QString profileName = profileOverride; + if (profileName == "") { + profileName = profile->name(); + } + + QFileInfo binary; + QString arguments = args.join(" "); + QString currentDirectory = cwd; + QString steamAppID; + QString customOverwrite; + QList forcedLibraries; + + if (executable.contains('\\') || executable.contains('/')) { + // file path + + binary = QFileInfo(executable); + if (binary.isRelative()) { + // relative path, should be relative to game directory + binary = m_core.managedGame()->gameDirectory().absoluteFilePath(executable); + } + + if (currentDirectory == "") { + currentDirectory = binary.absolutePath(); + } + + try { + const Executable& exe = m_core.executablesList()->getByBinary(binary); + steamAppID = exe.steamAppID(); + customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + } catch (const std::runtime_error &) { + // nop + } + } else { + // only a file name, search executables list + try { + const Executable &exe = m_core.executablesList()->get(executable); + steamAppID = exe.steamAppID(); + customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + if (arguments == "") { + arguments = exe.arguments(); + } + binary = exe.binaryInfo(); + if (currentDirectory == "") { + currentDirectory = exe.workingDirectory(); + } + } catch (const std::runtime_error &) { + log::warn("\"{}\" not set up as executable", executable); + binary = QFileInfo(executable); + } + } + + if (!forcedCustomOverwrite.isEmpty()) + customOverwrite = forcedCustomOverwrite; + + if (ignoreCustomOverwrite) + customOverwrite.clear(); + + return spawnAndWait( + binary, + arguments, + profileName, + currentDirectory, + steamAppID, + customOverwrite, + forcedLibraries); +} + +HANDLE ProcessRunner::spawnAndWait( + const QFileInfo &binary, const QString &arguments, const QString &profileName, + const QDir ¤tDirectory, const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + LPDWORD exitCode) +{ + spawn::SpawnParameters sp; + sp.binary = binary; + sp.arguments = arguments; + sp.currentDirectory = currentDirectory; + sp.steamAppID = steamAppID; + sp.hooked = true; + + if (!m_core.beforeRun(binary, profileName, customOverwrite, forcedLibraries)) { + return INVALID_HANDLE_VALUE; + } + + HANDLE handle = spawn(sp).releaseHandle(); + + if (handle == INVALID_HANDLE_VALUE) { + // failed + return INVALID_HANDLE_VALUE; + } + + waitForProcessCompletionWithLock(handle, exitCode); + return handle; +} + +SpawnedProcess ProcessRunner::spawn(spawn::SpawnParameters sp) +{ + QWidget* parent = nullptr; + if (m_ui) { + parent = m_ui->qtWidget(); + } + + if (!checkBinary(parent, sp)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + const auto* game = m_core.managedGame(); + auto& settings = m_core.settings(); + + if (!checkSteam(parent, sp, game->gameDirectory(), sp.steamAppID, settings)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + if (!checkEnvironment(parent, sp)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + if (!checkBlacklist(parent, sp, settings)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + adjustForVirtualized(game, sp, settings); + + return {startBinary(parent, sp), sp}; +} + +void ProcessRunner::withLock(std::function f) +{ + std::unique_ptr dlg; + ILockedWaitingForProcess* uilock = nullptr; + + if (m_ui != nullptr) { + uilock = m_ui->lock(); + } + else { + // i.e. when running command line shortcuts there is no user interface + dlg.reset(new LockedDialog); + dlg->show(); + dlg->setEnabled(true); + uilock = dlg.get(); + } + + Guard g([&]() { + if (m_ui != nullptr) { + m_ui->unlock(); + } + }); + + f(uilock); +} + +bool ProcessRunner::waitForProcessCompletionWithLock( + HANDLE handle, LPDWORD exitCode) +{ + if (!Settings::instance().interface().lockGUI()) { + return true; + } + + bool r = false; + + withLock([&](auto* uilock) { + DWORD ignoreExitCode; + r = waitForProcessCompletion(handle, exitCode ? exitCode : &ignoreExitCode, uilock); + }); + + return r; +} + +bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) +{ + if (!Settings::instance().interface().lockGUI()) + return true; + + bool r = false; + + withLock([&](auto* uilock) { + r = waitForProcessCompletion(handle, exitCode, uilock); + }); + + return r; +} + +bool ProcessRunner::waitForProcessCompletion( + HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) +{ + const auto tree = env::getProcessTree(handle); + std::vector processes = {tree}; + + const auto* interesting = getInterestingProcess(processes); + if (!interesting) { + return true; + } + + if (uilock) { + uilock->setProcessInformation(interesting->pid(), interesting->name()); + } + + auto interestingHandle = interesting->openHandleForWait(); + if (!interestingHandle) { + return true; + } + + auto progress = [&]{ return uilock->unlockForced(); }; + const auto r = waitForProcess( + interestingHandle.get(), exitCode, progress); + + switch (r) + { + case WaitResults::Completed: // fall-through + case WaitResults::Cancelled: + return true; + + case WaitResults::Error: // fall-through + default: + return false; + } +} + +bool ProcessRunner::waitForAllUSVFSProcessesWithLock() +{ + if (!Settings::instance().interface().lockGUI()) + return true; + + bool r = false; + + withLock([&](auto* uilock) { + r = waitForAllUSVFSProcesses(uilock); + }); + + return r; +} + +bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) +{ + for (;;) { + const auto handles = getRunningUSVFSProcesses(); + if (handles.empty()) { + break; + } + + std::vector processes; + for (auto&& h : handles) { + auto p = env::getProcessTree(h); + if (p.isValid()) { + processes.emplace_back(std::move(p)); + } + } + + const auto* interesting = getInterestingProcess(processes); + if (!interesting) { + break; + } + + if (uilock) { + uilock->setProcessInformation(interesting->pid(), interesting->name()); + } + + auto interestingHandle = interesting->openHandleForWait(); + if (!interestingHandle) { + break; + } + + auto progress = [&]{ return uilock->unlockForced(); }; + const auto r = waitForProcess( + interestingHandle.get(), nullptr, progress); + + switch (r) + { + case WaitResults::Completed: + // this process is completed, check for others + break; + + case WaitResults::Cancelled: + // force unlocked + log::debug("waiting for process completion aborted by UI"); + return true; + + case WaitResults::Error: // fall-through + default: + log::debug("waiting for process completion not successful"); + return false; + } + } + + log::debug("waiting for process completion successful"); + return true; +} + + + +WaitResults waitForProcess( + HANDLE handle, DWORD* exitCode, std::function progress) +{ + if (handle == INVALID_HANDLE_VALUE) { + return WaitResults::Error; + } + + log::debug("waiting for completion on pid {}", ::GetProcessId(handle)); + + std::vector handles; + handles.push_back(handle); + + std::vector exitCodes; + + const auto r = waitForProcesses(handles, exitCodes, progress); + + if (r == WaitResults::Completed) { + if (exitCode && !exitCodes.empty()) { + *exitCode = exitCodes[0]; + } + } + + return r; +} + +WaitResults waitForProcesses( + const std::vector& handles, std::vector& exitCodes, + std::function progress) +{ + if (handles.empty()) { + return WaitResults::Completed; + } + + const auto WAIT_OBJECT_N = static_cast(WAIT_OBJECT_0 + handles.size()); + + for (;;) { + // Wait for a an event on the handle, a key press, mouse click or timeout + const auto res = MsgWaitForMultipleObjects( + static_cast(handles.size()), &handles[0], + TRUE, 50, QS_KEY | QS_MOUSEBUTTON); + + if (res == WAIT_FAILED) { + // error + const auto e = ::GetLastError(); + + log::error( + "failed waiting for process completion, {}", formatSystemMessage(e)); + + return WaitResults::Error; + } else if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_N) { + // completed + exitCodes.resize(handles.size()); + std::fill(exitCodes.begin(), exitCodes.end(), 0); + + for (std::size_t i=0; i + +class OrganizerCore; +class ILockedWaitingForProcess; +class IUserInterface; +class Executable; +class MOShortcut; + +class SpawnedProcess +{ +public: + SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp); + + SpawnedProcess(const SpawnedProcess&) = delete; + SpawnedProcess& operator=(const SpawnedProcess&) = delete; + SpawnedProcess(SpawnedProcess&& other); + SpawnedProcess& operator=(SpawnedProcess&& other); + ~SpawnedProcess(); + + HANDLE releaseHandle(); + void wait(); + +private: + HANDLE m_handle; + spawn::SpawnParameters m_parameters; + + void destroy(); +}; + + +class ProcessRunner +{ +public: + ProcessRunner(OrganizerCore& core); + + void setUserInterface(IUserInterface* ui); + + bool runFile(QWidget* parent, const QFileInfo& targetInfo); + + bool runExecutableFile( + const QFileInfo &binary, const QString &arguments, + const QDir ¤tDirectory, const QString &steamAppID={}, + const QString &customOverwrite={}, + const QList &forcedLibraries={}, + bool refresh=true); + + bool runExecutable(const Executable& exe, bool refresh=true); + + bool runShortcut(const MOShortcut& shortcut); + + HANDLE runExecutableOrExecutableFile( + const QString &executable, const QStringList &args, const QString &cwd, + const QString &profile, const QString &forcedCustomOverwrite = "", + bool ignoreCustomOverwrite = false); + + bool waitForApplication(HANDLE processHandle, LPDWORD exitCode = nullptr); + + bool waitForAllUSVFSProcessesWithLock(); + +private: + OrganizerCore& m_core; + IUserInterface* m_ui; + + HANDLE spawnAndWait( + const QFileInfo &binary, const QString &arguments, + const QString &profileName, + const QDir ¤tDirectory, + const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries={}, + LPDWORD exitCode = nullptr); + + SpawnedProcess spawn(spawn::SpawnParameters sp); + + void withLock(std::function f); + + bool waitForProcessCompletionWithLock(HANDLE handle, LPDWORD exitCode); + + bool waitForProcessCompletion( + HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock); + + bool waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock); +}; + + +enum class WaitResults +{ + Completed = 1, + Error, + Cancelled +}; + +WaitResults waitForProcess( + HANDLE handle, DWORD* exitCode, std::function progress); + +WaitResults waitForProcesses( + const std::vector& handles, std::vector& exitCodes, + std::function progress); + +#endif // PROCESSRUNNER_H diff --git a/src/spawn.cpp b/src/spawn.cpp index 0ea60641..c8d7c76a 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -772,50 +772,6 @@ bool checkBlacklist( } } - -void adjustForVirtualized( - const IPluginGame* game, SpawnParameters& sp, const Settings& settings) -{ - const QString modsPath = settings.paths().mods(); - - // Check if this a request with either an executable or a working directory - // under our mods folder then will start the process in a virtualized - // "environment" with the appropriate paths fixed: - // (i.e. mods\FNIS\path\exe => game\data\path\exe) - QString cwdPath = sp.currentDirectory.absolutePath(); - bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); - QString binPath = sp.binary.absoluteFilePath(); - bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); - if (virtualizedCwd || virtualizedBin) { - if (virtualizedCwd) { - int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); - QString adjustedCwd = cwdPath.mid(cwdOffset, -1); - cwdPath = game->dataDirectory().absolutePath(); - if (cwdOffset >= 0) - cwdPath += adjustedCwd; - - } - - if (virtualizedBin) { - int binOffset = binPath.indexOf('/', modsPath.length() + 1); - QString adjustedBin = binPath.mid(binOffset, -1); - binPath = game->dataDirectory().absolutePath(); - if (binOffset >= 0) - binPath += adjustedBin; - } - - QString cmdline - = QString("launch \"%1\" \"%2\" %3") - .arg(QDir::toNativeSeparators(cwdPath), - QDir::toNativeSeparators(binPath), sp.arguments); - - sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); - sp.arguments = cmdline; - sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); - } -} - - HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) { HANDLE handle = INVALID_HANDLE_VALUE; @@ -842,80 +798,6 @@ HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) } } - - -SpawnedProcess::SpawnedProcess(HANDLE handle, SpawnParameters sp) - : m_handle(handle), m_parameters(std::move(sp)) -{ -} - -SpawnedProcess::SpawnedProcess(SpawnedProcess&& other) - : m_handle(other.m_handle), m_parameters(std::move(other.m_parameters)) -{ - other.m_handle = INVALID_HANDLE_VALUE; -} - -SpawnedProcess& SpawnedProcess::operator=(SpawnedProcess&& other) -{ - if (this != &other) { - destroy(); - - m_handle = other.m_handle; - other.m_handle = INVALID_HANDLE_VALUE; - - m_parameters = std::move(other.m_parameters); - } - - return *this; -} - -SpawnedProcess::~SpawnedProcess() -{ - destroy(); -} - -HANDLE SpawnedProcess::releaseHandle() -{ - const auto h = m_handle; - m_handle = INVALID_HANDLE_VALUE; - return h; -} - -void SpawnedProcess::destroy() -{ - if (m_handle != INVALID_HANDLE_VALUE) { - ::CloseHandle(m_handle); - m_handle = INVALID_HANDLE_VALUE; - } -} - - -SpawnedProcess Spawner::spawn( - QWidget* parent, const IPluginGame* game, - SpawnParameters sp, Settings& settings) -{ - if (!checkBinary(parent, sp)) { - return {INVALID_HANDLE_VALUE, sp}; - } - - if (!checkSteam(parent, sp, game->gameDirectory(), sp.steamAppID, settings)) { - return {INVALID_HANDLE_VALUE, sp}; - } - - if (!checkEnvironment(parent, sp)) { - return {INVALID_HANDLE_VALUE, sp}; - } - - if (!checkBlacklist(parent, sp, settings)) { - return {INVALID_HANDLE_VALUE, sp}; - } - - adjustForVirtualized(game, sp, settings); - - return {startBinary(parent, sp), sp}; -} - - QString getExecutableForJarFile(const QString& jarFile) { const std::wstring jarFileW = jarFile.toStdWString(); @@ -1094,86 +976,6 @@ FileExecutionContext getFileExecutionContext( return {{}, {}, FileExecutionTypes::Other}; } -WaitResults waitForProcess( - HANDLE handle, DWORD* exitCode, std::function progress) -{ - if (handle == INVALID_HANDLE_VALUE) { - return WaitResults::Error; - } - - log::debug("waiting for completion on pid {}", ::GetProcessId(handle)); - - std::vector handles; - handles.push_back(handle); - - std::vector exitCodes; - - const auto r = waitForProcesses(handles, exitCodes, progress); - - if (r == WaitResults::Completed) { - if (exitCode && !exitCodes.empty()) { - *exitCode = exitCodes[0]; - } - } - - return r; -} - -WaitResults waitForProcesses( - const std::vector& handles, std::vector& exitCodes, - std::function progress) -{ - if (handles.empty()) { - return WaitResults::Completed; - } - - const auto WAIT_OBJECT_N = static_cast(WAIT_OBJECT_0 + handles.size()); - - for (;;) { - // Wait for a an event on the handle, a key press, mouse click or timeout - const auto res = MsgWaitForMultipleObjects( - static_cast(handles.size()), &handles[0], - TRUE, 50, QS_KEY | QS_MOUSEBUTTON); - - if (res == WAIT_FAILED) { - // error - const auto e = ::GetLastError(); - - log::error( - "failed waiting for process completion, {}", formatSystemMessage(e)); - - return WaitResults::Error; - } else if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_N) { - // completed - exitCodes.resize(handles.size()); - std::fill(exitCodes.begin(), exitCodes.end(), 0); - - for (std::size_t i=0; i progress); - -WaitResults waitForProcesses( - const std::vector& handles, std::vector& exitCodes, - std::function progress); - } // namespace -- cgit v1.3.1 From db0b92776b5c9a34ebb1a5ce5c3f0b105844ee16 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 30 Oct 2019 23:42:59 -0400 Subject: added lockwidget to replace all the other dialogs rewrote ProcessRunner to have a bunch of setters and then a run() fixed bad exit code when waiting on a process that's already completed removed lock()/unlock() from main window, ProcessRunner is in charge of that now --- src/CMakeLists.txt | 3 + src/envmodule.cpp | 1 - src/iuserinterface.h | 6 +- src/lockwidget.cpp | 224 ++++++++++++++++ src/lockwidget.h | 68 +++++ src/mainwindow.cpp | 44 +--- src/mainwindow.h | 7 - src/organizercore.cpp | 8 +- src/organizercore.h | 3 +- src/organizerproxy.cpp | 19 +- src/pch.h | 47 ++-- src/processrunner.cpp | 687 ++++++++++++++++++++++++++++--------------------- src/processrunner.h | 70 ++++- 13 files changed, 806 insertions(+), 381 deletions(-) create mode 100644 src/lockwidget.cpp create mode 100644 src/lockwidget.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e909760..168b79dc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,6 +144,7 @@ SET(organizer_SRCS colortable.cpp sanitychecks.cpp processrunner.cpp + lockwidget.cpp shared/windows_error.cpp shared/error_report.cpp @@ -268,6 +269,7 @@ SET(organizer_HDRS envwindows.h colortable.h processrunner.h + lockwidget.h shared/windows_error.h shared/error_report.h @@ -486,6 +488,7 @@ set(widgets filterwidget icondelegate lcdnumber + lockwidget loglist loghighlighter modflagicondelegate diff --git a/src/envmodule.cpp b/src/envmodule.cpp index 0e2e8ec7..09593e61 100644 --- a/src/envmodule.cpp +++ b/src/envmodule.cpp @@ -545,7 +545,6 @@ Process getProcessTree(HANDLE parent) } if (root.pid() == 0) { - log::error("process {} is not running", parentPID); return {}; } diff --git a/src/iuserinterface.h b/src/iuserinterface.h index 91487aee..e5755f03 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -4,12 +4,13 @@ #include "modinfodialogfwd.h" #include "ilockedwaitingforprocess.h" +#include "lockwidget.h" #include #include #include - #include + class IUserInterface { public: @@ -31,9 +32,6 @@ public: virtual MOBase::DelayedFileWriterBase &archivesWriter() = 0; - virtual ILockedWaitingForProcess* lock() = 0; - virtual void unlock() = 0; - virtual QWidget* qtWidget() = 0; }; diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp new file mode 100644 index 00000000..720cac36 --- /dev/null +++ b/src/lockwidget.cpp @@ -0,0 +1,224 @@ +#include "lockwidget.h" +#include "mainwindow.h" +#include +#include +#include + +QWidget* createTransparentWidget(QWidget* parent=nullptr) +{ + auto* w = new QWidget(parent); + + w->setWindowOpacity(0); + w->setAttribute(Qt::WA_NoSystemBackground); + w->setAttribute(Qt::WA_TranslucentBackground); + + return w; +} + + +LockWidget::LockWidget(QWidget* parent, Reasons reason) : + m_parent(parent), m_overlay(nullptr), m_info(nullptr), m_result(NoResult), + m_filter(nullptr) +{ + if (reason != NoReason) { + lock(reason); + } +} + +LockWidget::~LockWidget() +{ + unlock(); +} + +void LockWidget::lock(Reasons reason) +{ + m_result = StillLocked; + createUi(reason); +} + +void LockWidget::unlock() +{ + m_overlay.reset(); + + if (m_filter && m_parent) { + m_parent->removeEventFilter(m_filter.get()); + } + + enableAll(); +} + +void LockWidget::setInfo(DWORD pid, const QString& name) +{ + m_info->setText(QString("%1 (%2)").arg(name).arg(pid)); +} + +LockWidget::Results LockWidget::result() const +{ + return m_result; +} + +void LockWidget::createUi(Reasons reason) +{ + if (m_parent) { + m_overlay.reset(createTransparentWidget(m_parent)); + m_overlay->setWindowFlags(m_overlay->windowFlags() & Qt::FramelessWindowHint); + m_overlay->setGeometry(m_parent->rect()); + } else { + m_overlay.reset(new QDialog); + } + + auto* center = new QFrame; + + if (m_parent) { + center->setFrameStyle(QFrame::StyledPanel); + center->setLineWidth(1); + center->setAutoFillBackground(true); + + auto* shadow = new QGraphicsDropShadowEffect; + shadow->setBlurRadius(50); + shadow->setOffset(0); + shadow->setColor(QColor(0, 0, 0, 100)); + center->setGraphicsEffect(shadow); + } + + m_info = new QLabel(" "); + m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + + auto* ly = new QVBoxLayout(center); + + if (!m_parent) { + ly->setContentsMargins(0, 0, 0, 0); + } + + auto* message = new QLabel; + ly->addWidget(message); + ly->addWidget(m_info); + + auto* buttons = new QWidget; + auto* buttonsLayout = new QHBoxLayout(buttons); + ly->addWidget(buttons); + + switch (reason) + { + case LockUI: + { + message->setText(QObject::tr( + "Mod Organizer is locked while the executable is running.")); + + auto* unlockButton = new QPushButton(QObject::tr("Unlock")); + QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); + buttonsLayout->addWidget(unlockButton); + + break; + } + + case OutputRequired: + { + message->setText(QObject::tr( + "The executable must run to completion because a its output is " + "required.")); + + auto* unlockButton = new QPushButton(QObject::tr("Unlock")); + QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); + buttonsLayout->addWidget(unlockButton); + + break; + } + + case PreventExit: + { + message->setText(QObject::tr( + "Mod Organizer is waiting on processes to finish before exiting.")); + + auto* exit = new QPushButton(QObject::tr("Exit Now")); + QObject::connect(exit, &QPushButton::clicked, [&]{ onForceUnlock(); }); + buttonsLayout->addWidget(exit); + + auto* cancel = new QPushButton(QObject::tr("Cancel")); + QObject::connect(cancel, &QPushButton::clicked, [&]{ onCancel(); }); + buttonsLayout->addWidget(cancel); + + break; + } + } + + auto* grid = new QGridLayout(m_overlay.get()); + grid->addWidget(createTransparentWidget(), 0, 1); + grid->addWidget(createTransparentWidget(), 2, 1); + grid->addWidget(createTransparentWidget(), 1, 0); + grid->addWidget(createTransparentWidget(), 1, 2); + grid->addWidget(center, 1, 1); + + if (!m_parent) { + grid->setContentsMargins(0, 0, 0, 0); + } + + grid->setRowStretch(0, 1); + grid->setRowStretch(2, 1); + grid->setColumnStretch(0, 1); + grid->setColumnStretch(2, 1); + + disableAll(); + + if (m_parent) { + m_filter.reset(new Filter); + m_filter->resized = [=]{ m_overlay->setGeometry(m_parent->rect()); }; + m_parent->installEventFilter(m_filter.get()); + } + + m_overlay->setFocusPolicy(Qt::TabFocus); + m_overlay->setFocus(); + m_overlay->show(); + m_overlay->setEnabled(true); +} + +void LockWidget::onForceUnlock() +{ + m_result = ForceUnlocked; + unlock(); +} + +void LockWidget::onCancel() +{ + m_result = Cancelled; + unlock(); +} + +void LockWidget::disableAll() +{ + if (!m_parent) { + // nothing to disable without a main window + return; + } + + if (auto* mw=dynamic_cast(m_parent)) { + disable(mw->centralWidget()); + disable(mw->menuBar()); + disable(mw->statusBar()); + } + + for (auto* tb : m_parent->findChildren()) { + disable(tb); + } + + for (auto* d : m_parent->findChildren()) { + disable(d); + } +} + +void LockWidget::enableAll() +{ + for (auto* w : m_disabled) { + w->setEnabled(true); + } + + m_disabled.clear(); +} + +void LockWidget::disable(QWidget* w) +{ + if (w->isEnabled()) { + w->setEnabled(false); + m_disabled.push_back(w); + } +} diff --git a/src/lockwidget.h b/src/lockwidget.h new file mode 100644 index 00000000..bfb0b30f --- /dev/null +++ b/src/lockwidget.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +class LockWidget +{ +public: + enum Reasons + { + NoReason = 0, + LockUI, + OutputRequired, + PreventExit + }; + + enum Results + { + NoResult = 0, + StillLocked, + ForceUnlocked, + Cancelled + }; + + LockWidget(QWidget* parent, Reasons reason=NoReason); + ~LockWidget(); + + void lock(Reasons reason); + void unlock(); + + void setInfo(DWORD pid, const QString& name); + Results result() const; + +private: + class Filter : public QObject + { + public: + std::function resized; + + protected: + bool eventFilter(QObject* o, QEvent* e) override + { + if (e->type() == QEvent::Resize) { + if (resized) { + resized(); + } + } + + return QObject::eventFilter(o, e); + } + }; + + + QWidget* m_parent; + std::unique_ptr m_overlay; + QLabel* m_info; + Results m_result; + std::unique_ptr m_filter; + std::vector m_disabled; + + void createUi(Reasons reason); + + void onForceUnlock(); + void onCancel(); + + void disableAll(); + void enableAll(); + void disable(QWidget* w); +}; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ce5280a6..7d3d20b3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -57,7 +57,6 @@ along with Mod Organizer. If not, see . #include "downloadlistwidget.h" #include "messagedialog.h" #include "installationmanager.h" -#include "lockeddialog.h" #include "waitingonclosedialog.h" #include "downloadlistsortproxy.h" #include "motddialog.h" @@ -1311,9 +1310,10 @@ bool MainWindow::canExit() } } - m_exitAfterWait = true; - m_OrganizerCore.processRunner().waitForAllUSVFSProcessesWithLock(); - if (!m_exitAfterWait) { // if operation cancelled + const auto r = m_OrganizerCore.processRunner() + .waitForAllUSVFSProcessesWithLock(LockWidget::PreventExit); + + if (r == ProcessRunner::Cancelled) { return false; } @@ -2258,42 +2258,6 @@ void MainWindow::storeSettings() s.widgets().saveIndex(ui->executablesListBox); } -ILockedWaitingForProcess* MainWindow::lock() -{ - if (m_LockDialog != nullptr) { - ++m_LockCount; - return m_LockDialog; - } - if (m_exitAfterWait) - m_LockDialog = new WaitingOnCloseDialog(this); - else - m_LockDialog = new LockedDialog(this, true); - m_LockDialog->setModal(true); - m_LockDialog->show(); - setEnabled(false); - m_LockDialog->setEnabled(true); //What's the point otherwise? - ++m_LockCount; - return m_LockDialog; -} - -void MainWindow::unlock() -{ - //If you come through here with a null lock pointer, it's a bug! - if (m_LockDialog == nullptr) { - log::debug("Unlocking main window when already unlocked"); - return; - } - --m_LockCount; - if (m_LockCount == 0) { - if (m_exitAfterWait && m_LockDialog->canceled()) - m_exitAfterWait = false; - m_LockDialog->hide(); - m_LockDialog->deleteLater(); - m_LockDialog = nullptr; - setEnabled(true); - } -} - QWidget* MainWindow::qtWidget() { return this; diff --git a/src/mainwindow.h b/src/mainwindow.h index 19723480..c80287b2 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -38,7 +38,6 @@ along with Mod Organizer. If not, see . //when I get round to cleaning up main.cpp class Executable; class CategoryFactory; -class LockedDialogBase; class OrganizerCore; class PluginListSortProxy; @@ -118,8 +117,6 @@ public: void processUpdates(Settings& settings); - ILockedWaitingForProcess* lock() override; - void unlock() override; QWidget* qtWidget() override; bool addProfile(); @@ -381,11 +378,7 @@ private: bool m_DidUpdateMasterList; - LockedDialogBase *m_LockDialog { nullptr }; - uint64_t m_LockCount { 0 }; - bool m_showArchiveData{ true }; - bool m_exitAfterWait{ false }; MOBase::DelayedFileWriter m_ArchiveListWriter; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 0f767f46..89e8bd9e 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -91,7 +91,6 @@ OrganizerCore::OrganizerCore(Settings &settings) , m_PluginContainer(nullptr) , m_GameName() , m_CurrentProfile(nullptr) - , m_Runner(*this) , m_Settings(settings) , m_Updater(NexusInterface::instance(m_PluginContainer)) , m_AboutToRun() @@ -252,7 +251,6 @@ void OrganizerCore::setUserInterface(IUserInterface* ui) m_InstallationManager.setParentWidget(w); m_Updater.setUserInterface(w); - m_Runner.setUserInterface(ui); checkForUpdates(); } @@ -357,7 +355,7 @@ void OrganizerCore::externalMessage(const QString &message) { if (MOShortcut moshortcut{ message } ) { if(moshortcut.hasExecutable()) - m_Runner.runShortcut(moshortcut); + processRunner().runShortcut(moshortcut); } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); @@ -1721,9 +1719,9 @@ void OrganizerCore::saveCurrentProfile() storeSettings(); } -ProcessRunner& OrganizerCore::processRunner() +ProcessRunner OrganizerCore::processRunner() { - return m_Runner; + return ProcessRunner(*this, m_UserInterface); } bool OrganizerCore::beforeRun( diff --git a/src/organizercore.h b/src/organizercore.h index 2252c118..d4882b92 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -134,7 +134,7 @@ public: bool saveCurrentLists(); - ProcessRunner& processRunner(); + ProcessRunner processRunner(); bool beforeRun( const QFileInfo& binary, const QString& profileName, @@ -305,7 +305,6 @@ private: Profile *m_CurrentProfile; - ProcessRunner m_Runner; Settings& m_Settings; SelfUpdater m_Updater; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 75b3ea41..3ee35fe2 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -119,7 +119,24 @@ HANDLE OrganizerProxy::startApplication( bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const { - return m_Proxied->processRunner().waitForApplication(handle, exitCode); + const auto r = m_Proxied->processRunner().waitForApplication( + handle, exitCode, LockWidget::OutputRequired); + + switch (r) + { + case ProcessRunner::Completed: + return true; + + case ProcessRunner::Cancelled: // fall-through + case ProcessRunner::ForceUnlocked: + // this is always an error because the application should have run to + // completion + return false; + + case ProcessRunner::Error: // fall-through + default: + return false; + } } bool OrganizerProxy::onAboutToRun(const std::function &func) diff --git a/src/pch.h b/src/pch.h index 01a97357..b7c5d695 100644 --- a/src/pch.h +++ b/src/pch.h @@ -100,9 +100,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -111,13 +111,14 @@ #include #include #include -#include +#include #include +#include #include +#include #include #include #include -#include #include #include #include @@ -126,20 +127,21 @@ #include #include #include -#include #include +#include #include #include #include #include #include #include -#include #include #include +#include #include #include #include +#include #include #include #include @@ -190,53 +192,42 @@ #include #include #include -#include #include #include +#include #include #include -#include #include +#include +#include #include #include #include #include -#include #include #include #include +#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include +#include #include #include #include #include #include #include -#include -#include #include #include #include #include #include #include -#include -#include #include #include #include #include #include -#include #include #include #include @@ -255,4 +246,16 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 5d4a9fde..62c77efc 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -1,26 +1,58 @@ #include "processrunner.h" #include "organizercore.h" #include "instancemanager.h" -#include "lockeddialog.h" #include "iuserinterface.h" #include "envmodule.h" #include using namespace MOBase; -enum class WaitResults +void adjustForVirtualized( + const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) { - Completed = 1, - Error, - Cancelled, - StillRunning -}; + const QString modsPath = settings.paths().mods(); + + // Check if this a request with either an executable or a working directory + // under our mods folder then will start the process in a virtualized + // "environment" with the appropriate paths fixed: + // (i.e. mods\FNIS\path\exe => game\data\path\exe) + QString cwdPath = sp.currentDirectory.absolutePath(); + bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); + if (virtualizedCwd || virtualizedBin) { + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = game->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', modsPath.length() + 1); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = game->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + QString cmdline + = QString("launch \"%1\" \"%2\" %3") + .arg(QDir::toNativeSeparators(cwdPath), + QDir::toNativeSeparators(binPath), sp.arguments); + sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); + sp.arguments = cmdline; + sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + } +} -WaitResults singleWait(HANDLE handle, DWORD* exitCode) +std::optional singleWait(HANDLE handle, DWORD pid) { if (handle == INVALID_HANDLE_VALUE) { - return WaitResults::Error; + return ProcessRunner::Error; } const DWORD WAIT_EVENT = WAIT_OBJECT_0 + 1; @@ -33,23 +65,15 @@ WaitResults singleWait(HANDLE handle, DWORD* exitCode) { case WAIT_OBJECT_0: { - // completed - if (exitCode) { - if (!::GetExitCodeProcess(handle, exitCode)) { - const auto e = ::GetLastError(); - log::warn( - "failed to get exit code of process, {}", - formatSystemMessage(e)); - } - } - - return WaitResults::Completed; + log::debug("process {} completed", pid); + return ProcessRunner::Completed; } case WAIT_TIMEOUT: case WAIT_EVENT: { - return WaitResults::StillRunning; + // still running + return {}; } case WAIT_FAILED: // fall-through @@ -57,11 +81,8 @@ WaitResults singleWait(HANDLE handle, DWORD* exitCode) { // error const auto e = ::GetLastError(); - - log::error( - "failed waiting for process completion, {}", formatSystemMessage(e)); - - return WaitResults::Error; + log::error("failed waiting for {}, {}", pid, formatSystemMessage(e)); + return ProcessRunner::Error; } } } @@ -132,6 +153,11 @@ std::pair findInterestingProcessInTrees( std::pair getInterestingProcess( const std::vector& initialProcesses) { + if (initialProcesses.empty()) { + log::debug("nothing to wait for"); + return {{}, Interest::None}; + } + std::vector processes; log::debug("getting process tree for {} processes", initialProcesses.size()); @@ -143,7 +169,7 @@ std::pair getInterestingProcess( } if (processes.empty()) { - log::debug("nothing to wait for"); + log::debug("processes are already completed"); return {{}, Interest::None}; } @@ -158,9 +184,8 @@ std::pair getInterestingProcess( const std::chrono::milliseconds Infinite(-1); -WaitResults timedWait( - HANDLE handle, DWORD* exitCode, ILockedWaitingForProcess* uilock, - std::chrono::milliseconds wait) +std::optional timedWait( + HANDLE handle, DWORD pid, LockWidget& lock, std::chrono::milliseconds wait) { using namespace std::chrono; @@ -170,37 +195,66 @@ WaitResults timedWait( } for (;;) { - const auto r = singleWait(handle, exitCode); + const auto r = singleWait(handle, pid); - if (r != WaitResults::StillRunning) { - return r; + if (r) { + return *r; } + // still running + // keep processing events so the app doesn't appear dead QCoreApplication::sendPostedEvents(); QCoreApplication::processEvents(); - if (uilock && uilock->unlockForced()) { - return WaitResults::Cancelled; + switch (lock.result()) + { + case LockWidget::StillLocked: + { + break; + } + + case LockWidget::ForceUnlocked: + { + log::debug("waiting for {} force unlocked by user", pid); + return ProcessRunner::ForceUnlocked; + } + + case LockWidget::Cancelled: + { + log::debug("waiting for {} cancelled by user", pid); + return ProcessRunner::Cancelled; + } + + case LockWidget::NoResult: // fall-through + default: + { + // shouldn't happen + log::debug( + "unexpected result {} while waiting for {}", + static_cast(lock.result()), pid); + + return ProcessRunner::Error; + } } if (wait != Infinite) { const auto now = high_resolution_clock::now(); if (duration_cast(now - start) >= wait) { - return WaitResults::StillRunning; + return {}; } } } } -WaitResults waitForProcesses( - const std::vector& initialProcesses, - LPDWORD exitCode, ILockedWaitingForProcess* uilock) +ProcessRunner::Results waitForProcesses( + const std::vector& initialProcesses, LockWidget& lock) { using namespace std::chrono; if (initialProcesses.empty()) { - return WaitResults::Completed; + // shouldn't happen + return ProcessRunner::Completed; } DWORD currentPID = 0; @@ -208,14 +262,16 @@ WaitResults waitForProcesses( for (;;) { auto [p, interest] = getInterestingProcess(initialProcesses); - - if (uilock) { - uilock->setProcessInformation(p.pid(), p.name()); + if (!p.isValid()) { + // nothing to wait on + return ProcessRunner::Completed; } + lock.setInfo(p.pid(), p.name()); + auto interestingHandle = p.openHandleForWait(); if (!interestingHandle) { - return WaitResults::Error; + return ProcessRunner::Error; } if (p.pid() != currentPID) { @@ -230,9 +286,9 @@ WaitResults waitForProcesses( wait = Infinite; } - const auto r = timedWait(interestingHandle.get(), exitCode, uilock, wait); - if (r != WaitResults::StillRunning) { - return r; + const auto r = timedWait(interestingHandle.get(), p.pid(), lock, wait); + if (r) { + return *r; } wait = std::min(wait * 2, milliseconds(2000)); @@ -243,11 +299,24 @@ WaitResults waitForProcesses( } } -WaitResults waitForProcess( - HANDLE initialProcess, LPDWORD exitCode, ILockedWaitingForProcess* uilock) +ProcessRunner::Results waitForProcess( + HANDLE initialProcess, LPDWORD exitCode, LockWidget& lock) { std::vector processes = {initialProcess}; - return waitForProcesses(processes, exitCode, uilock); + + const auto r = waitForProcesses(processes, lock); + + // as long as it's not running anymore, try to get the exit code + if (exitCode && r != ProcessRunner::Running) { + if (!::GetExitCodeProcess(initialProcess, exitCode)) { + const auto e = ::GetLastError(); + log::warn( + "failed to get exit code of process, {}", + formatSystemMessage(e)); + } + } + + return r; } @@ -298,17 +367,64 @@ void SpawnedProcess::destroy() } -ProcessRunner::ProcessRunner(OrganizerCore& core) - : m_core(core), m_ui(nullptr) +ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : + m_core(core), m_ui(ui), m_lock(LockWidget::NoReason), m_refresh(false), + m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { + m_sp.hooked = true; } -void ProcessRunner::setUserInterface(IUserInterface* ui) +ProcessRunner& ProcessRunner::setBinary(const QFileInfo &binary) { - m_ui = ui; + m_sp.binary = binary; + return *this; } -bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) +ProcessRunner& ProcessRunner::setArguments(const QString& arguments) +{ + m_sp.arguments = arguments; + return *this; +} + +ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory) +{ + m_sp.currentDirectory = directory; + return *this; +} + +ProcessRunner& ProcessRunner::setSteamID(const QString& steamID) +{ + m_sp.steamAppID = steamID; + return *this; +} + +ProcessRunner& ProcessRunner::setCustomOverwrite(const QString& customOverwrite) +{ + m_customOverwrite = customOverwrite; + return *this; +} + +ProcessRunner& ProcessRunner::setForcedLibraries(const ForcedLibraries& forcedLibraries) +{ + m_forcedLibraries = forcedLibraries; + return *this; +} + +ProcessRunner& ProcessRunner::setProfileName(const QString& profileName) +{ + m_profileName = profileName; + return *this; +} + +ProcessRunner& ProcessRunner::setWaitForCompletion( + LockWidget::Reasons reason, bool refresh) +{ + m_lock = reason; + m_refresh = refresh; + return *this; +} + +ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) { if (!parent && m_ui) { parent = m_ui->qtWidget(); @@ -320,56 +436,24 @@ bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) { case spawn::FileExecutionTypes::Executable: { - runExecutableFile(fec.binary, fec.arguments, targetInfo.absoluteDir()); - return true; + setBinary(fec.binary); + setArguments(fec.arguments); + setCurrentDirectory(targetInfo.absoluteDir()); + break; } case spawn::FileExecutionTypes::Other: // fall-through default: { - auto r = shell::Open(targetInfo.absoluteFilePath()); - if (!r.success()) { - return false; - } - - // not all files will return a valid handle even if opening them was - // successful, such as inproc handlers (like the photo viewer) - if (r.processHandle() != INVALID_HANDLE_VALUE) { - // steal because it gets closed after the wait - return waitForProcessCompletionWithLock(r.stealProcessHandle(), nullptr); - } - - return true; + m_shellOpen = targetInfo.absoluteFilePath(); + break; } } -} -bool ProcessRunner::runExecutableFile( - const QFileInfo &binary, const QString &arguments, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - bool refresh) -{ - DWORD processExitCode = 0; - HANDLE processHandle = spawnAndWait( - binary, arguments, m_core.currentProfile()->name(), - currentDirectory, steamAppID, customOverwrite, forcedLibraries, - &processExitCode); - - if (processHandle == INVALID_HANDLE_VALUE) { - // failed - return false; - } - - if (refresh) { - m_core.afterRun(binary, processExitCode); - } - - return true; + return *this; } -bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) +ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) { const auto* profile = m_core.currentProfile(); if (!profile) { @@ -379,25 +463,31 @@ bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) const QString customOverwrite = profile->setting( "custom_overwrites", exe.title()).toString(); - QList forcedLibraries; - + ForcedLibraries forcedLibraries; if (profile->forcedLibrariesEnabled(exe.title())) { forcedLibraries = profile->determineForcedLibraries(exe.title()); } - return runExecutableFile( - exe.binaryInfo(), - exe.arguments(), - exe.workingDirectory().length() != 0 ? exe.workingDirectory() : exe.binaryInfo().absolutePath(), - exe.steamAppID(), - customOverwrite, - forcedLibraries, - refresh); + QDir currentDirectory = exe.workingDirectory(); + if (currentDirectory.isEmpty()) { + currentDirectory.setPath(exe.binaryInfo().absolutePath()); + } + + setBinary(exe.binaryInfo()); + setArguments(exe.arguments()); + setCurrentDirectory(currentDirectory); + setSteamID(exe.steamAppID()); + setCustomOverwrite(customOverwrite); + setForcedLibraries(forcedLibraries); + + return *this; } -bool ProcessRunner::runShortcut(const MOShortcut& shortcut) +ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) { - if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) { + const auto currentInstance = InstanceManager::instance().currentInstance(); + + if (shortcut.hasInstance() && shortcut.instance() != currentInstance) { throw std::runtime_error( QString("Refusing to run executable from different instance %1:%2") .arg(shortcut.instance(),shortcut.executable()) @@ -405,12 +495,17 @@ bool ProcessRunner::runShortcut(const MOShortcut& shortcut) } const Executable& exe = m_core.executablesList()->get(shortcut.executable()); - return runExecutable(exe, false); + setFromExecutable(exe); + + return *this; } -HANDLE ProcessRunner::runExecutableOrExecutableFile( - const QString& executable, const QStringList &args, const QString &cwd, - const QString& profileOverride, const QString &forcedCustomOverwrite, +ProcessRunner& ProcessRunner::setFromFileOrExecutable( + const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profileOverride, + const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite) { const auto* profile = m_core.currentProfile(); @@ -418,37 +513,41 @@ HANDLE ProcessRunner::runExecutableOrExecutableFile( throw MyException(QObject::tr("No profile set")); } - QString profileName = profileOverride; - if (profileName == "") { - profileName = profile->name(); - } + setProfileName(profileOverride); - QFileInfo binary; - QString arguments = args.join(" "); - QString currentDirectory = cwd; - QString steamAppID; - QString customOverwrite; - QList forcedLibraries; + //QFileInfo binary; + //QString arguments = args.join(" "); + //QString currentDirectory = cwd; + //QString steamAppID; + //QString customOverwrite; + //QList forcedLibraries; if (executable.contains('\\') || executable.contains('/')) { // file path - binary = QFileInfo(executable); + auto binary = QFileInfo(executable); + if (binary.isRelative()) { // relative path, should be relative to game directory binary = m_core.managedGame()->gameDirectory().absoluteFilePath(executable); } - if (currentDirectory == "") { - currentDirectory = binary.absolutePath(); + setBinary(binary); + + if (cwd == "") { + setCurrentDirectory(binary.absolutePath()); + } else { + setCurrentDirectory(cwd); } try { const Executable& exe = m_core.executablesList()->getByBinary(binary); - steamAppID = exe.steamAppID(); - customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + + setSteamID(exe.steamAppID()); + setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + if (profile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = profile->determineForcedLibraries(exe.title()); + setForcedLibraries(profile->determineForcedLibraries(exe.title())); } } catch (const std::runtime_error &) { // nop @@ -457,223 +556,244 @@ HANDLE ProcessRunner::runExecutableOrExecutableFile( // only a file name, search executables list try { const Executable &exe = m_core.executablesList()->get(executable); - steamAppID = exe.steamAppID(); - customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + + setSteamID(exe.steamAppID()); + setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + if (profile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = profile->determineForcedLibraries(exe.title()); + setForcedLibraries(profile->determineForcedLibraries(exe.title())); } - if (arguments == "") { - arguments = exe.arguments(); + + if (args.isEmpty()) { + setArguments(exe.arguments()); + } else { + setArguments(args.join(" ")); } - binary = exe.binaryInfo(); - if (currentDirectory == "") { - currentDirectory = exe.workingDirectory(); + + setBinary(exe.binaryInfo()); + + if (cwd == "") { + setCurrentDirectory(exe.workingDirectory()); + } else { + setCurrentDirectory(cwd); } } catch (const std::runtime_error &) { log::warn("\"{}\" not set up as executable", executable); - binary = QFileInfo(executable); + setBinary(QFileInfo(executable)); } } - if (!forcedCustomOverwrite.isEmpty()) - customOverwrite = forcedCustomOverwrite; - - if (ignoreCustomOverwrite) - customOverwrite.clear(); + if (ignoreCustomOverwrite) { + setCustomOverwrite(""); + } else if (!forcedCustomOverwrite.isEmpty()) { + setCustomOverwrite(forcedCustomOverwrite); + } - return spawnAndWait( - binary, - arguments, - profileName, - currentDirectory, - steamAppID, - customOverwrite, - forcedLibraries); + return *this; } -HANDLE ProcessRunner::spawnAndWait( - const QFileInfo &binary, const QString &arguments, const QString &profileName, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - LPDWORD exitCode) +ProcessRunner::Results ProcessRunner::run() { - spawn::SpawnParameters sp; - sp.binary = binary; - sp.arguments = arguments; - sp.currentDirectory = currentDirectory; - sp.steamAppID = steamAppID; - sp.hooked = true; + if (!m_shellOpen.isEmpty()) { + auto r = shell::Open(m_shellOpen); + if (!r.success()) { + return Error; + } - if (!m_core.beforeRun(binary, profileName, customOverwrite, forcedLibraries)) { - return INVALID_HANDLE_VALUE; - } + // not all files will return a valid handle even if opening them was + // successful, such as inproc handlers (like the photo viewer) + m_handle = r.stealProcessHandle(); + } else { + if (m_profileName.isEmpty()) { + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } - HANDLE handle = spawn(sp).releaseHandle(); + m_profileName = profile->name(); + } - if (handle == INVALID_HANDLE_VALUE) { - // failed - return INVALID_HANDLE_VALUE; - } + if (!m_core.beforeRun(m_sp.binary, m_profileName, m_customOverwrite, m_forcedLibraries)) { + return Error; + } - waitForProcessCompletionWithLock(handle, exitCode); - return handle; -} + QWidget* parent = nullptr; + if (m_ui) { + parent = m_ui->qtWidget(); + } -void adjustForVirtualized( - const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) -{ - const QString modsPath = settings.paths().mods(); + if (!checkBinary(parent, m_sp)) { + return Error; + } - // Check if this a request with either an executable or a working directory - // under our mods folder then will start the process in a virtualized - // "environment" with the appropriate paths fixed: - // (i.e. mods\FNIS\path\exe => game\data\path\exe) - QString cwdPath = sp.currentDirectory.absolutePath(); - bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); - QString binPath = sp.binary.absoluteFilePath(); - bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); - if (virtualizedCwd || virtualizedBin) { - if (virtualizedCwd) { - int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); - QString adjustedCwd = cwdPath.mid(cwdOffset, -1); - cwdPath = game->dataDirectory().absolutePath(); - if (cwdOffset >= 0) - cwdPath += adjustedCwd; + const auto* game = m_core.managedGame(); + auto& settings = m_core.settings(); + if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { + return Error; } - if (virtualizedBin) { - int binOffset = binPath.indexOf('/', modsPath.length() + 1); - QString adjustedBin = binPath.mid(binOffset, -1); - binPath = game->dataDirectory().absolutePath(); - if (binOffset >= 0) - binPath += adjustedBin; + if (!checkEnvironment(parent, m_sp)) { + return Error; } - QString cmdline - = QString("launch \"%1\" \"%2\" %3") - .arg(QDir::toNativeSeparators(cwdPath), - QDir::toNativeSeparators(binPath), sp.arguments); + if (!checkBlacklist(parent, m_sp, settings)) { + return Error; + } - sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); - sp.arguments = cmdline; - sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + adjustForVirtualized(game, m_sp, settings); + + m_handle = startBinary(parent, m_sp); + if (m_handle == INVALID_HANDLE_VALUE) { + return Error; + } + } + + if (m_handle == INVALID_HANDLE_VALUE || m_lock == LockWidget::NoReason) { + return Running; + } else { + const auto r = waitForProcessCompletionWithLock( + m_handle, &m_exitCode, m_lock); + + if (r == Completed && m_refresh) { + m_core.afterRun(m_sp.binary, m_exitCode); + } + + return r; } } -SpawnedProcess ProcessRunner::spawn(spawn::SpawnParameters sp) +DWORD ProcessRunner::exitCode() { - QWidget* parent = nullptr; - if (m_ui) { - parent = m_ui->qtWidget(); - } + return m_exitCode; +} - if (!checkBinary(parent, sp)) { - return {INVALID_HANDLE_VALUE, sp}; - } - const auto* game = m_core.managedGame(); - auto& settings = m_core.settings(); +bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) +{ + setFromFile(parent, targetInfo); + setWaitForCompletion(LockWidget::LockUI, true); - if (!checkSteam(parent, sp, game->gameDirectory(), sp.steamAppID, settings)) { - return {INVALID_HANDLE_VALUE, sp}; - } + const auto r = run(); + return (r != Error); +} - if (!checkEnvironment(parent, sp)) { - return {INVALID_HANDLE_VALUE, sp}; - } +bool ProcessRunner::runExecutableFile( + const QFileInfo &binary, const QString &arguments, + const QDir ¤tDirectory, const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + bool refresh) +{ + setBinary(binary); + setArguments(arguments); + setCurrentDirectory(currentDirectory); + setSteamID(steamAppID); + setCustomOverwrite(customOverwrite); + setForcedLibraries(forcedLibraries); + setWaitForCompletion(LockWidget::LockUI, refresh); + + const auto r = run(); + return (r != Error); +} - if (!checkBlacklist(parent, sp, settings)) { - return {INVALID_HANDLE_VALUE, sp}; - } +bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) +{ + setFromExecutable(exe); + setWaitForCompletion(LockWidget::LockUI, refresh); + + const auto r = run(); + return (r != Error); +} - adjustForVirtualized(game, sp, settings); +bool ProcessRunner::runShortcut(const MOShortcut& shortcut) +{ + setFromShortcut(shortcut); + setWaitForCompletion(LockWidget::LockUI, false); - return {startBinary(parent, sp), sp}; + const auto r = run(); + return (r != Error); } -void ProcessRunner::withLock(std::function f) +HANDLE ProcessRunner::runExecutableOrExecutableFile( + const QString& executable, const QStringList &args, const QString &cwd, + const QString& profileOverride, const QString &forcedCustomOverwrite, + bool ignoreCustomOverwrite) { - std::unique_ptr dlg; - ILockedWaitingForProcess* uilock = nullptr; + setFromFileOrExecutable( + executable, args, cwd, profileOverride, forcedCustomOverwrite, + ignoreCustomOverwrite); - if (m_ui != nullptr) { - uilock = m_ui->lock(); - } else { - // i.e. when running command line shortcuts there is no user interface - dlg.reset(new LockedDialog); - dlg->show(); - dlg->setEnabled(true); - uilock = dlg.get(); - } + setWaitForCompletion(LockWidget::LockUI, true); - Guard g([&]() { - if (m_ui != nullptr) { - m_ui->unlock(); - } - }); + run(); + return m_handle; +} - f(uilock); +void ProcessRunner::withLock( + LockWidget::Reasons reason, std::function f) +{ + auto lock = std::make_unique( + m_ui ? m_ui->qtWidget() : nullptr, reason); + + f(*lock); } -bool ProcessRunner::waitForProcessCompletionWithLock( - HANDLE handle, LPDWORD exitCode) +ProcessRunner::Results ProcessRunner::waitForProcessCompletionWithLock( + HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason) { if (!Settings::instance().interface().lockGUI()) { log::debug("not waiting for process because user has disabled locking"); - return true; + return ForceUnlocked; } - auto r = WaitResults::Error; - - withLock([&](auto* uilock) { - r = waitForProcess(handle, exitCode, uilock); - }); - - // completed/unlocked is fine - return (r != WaitResults::Error); + return waitForApplication(handle, exitCode, reason); } -bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) +ProcessRunner::Results ProcessRunner::waitForApplication( + HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason) { // don't check for lockGUI() setting; this _always_ locks the ui and waits // for completion // - // this is typically called only from OrganizerProxy, which allows plugins - // to wait on applications until they're finished + // this is typically called only from: + // 1) OrganizerProxy, which allows plugins to wait on applications until + // they're finished + // + // the check_fnis plugin for example will start FNIS, wait for it to + // complete, and then check the exit code; this has to work regardless of + // the locking setting; // - // the check_fnis plugin for example will start FNIS, wait for it to complete, - // and then check the exit code; this has to work regardless of the locking - // setting + // 2) waitForProcessCompletionWithLock() above, which has already checked the + // lock setting - auto r = WaitResults::Error; + auto r = Error; - withLock([&](auto* uilock) { - r = waitForProcess(handle, exitCode, uilock); + withLock(reason, [&](auto& lock) { + r = waitForProcess(handle, exitCode, lock); }); - // treat unlocked as an error since this should always wait for completion - return (r == WaitResults::Completed); + return r; } -bool ProcessRunner::waitForAllUSVFSProcessesWithLock() +ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( + LockWidget::Reasons reason) { if (!Settings::instance().interface().lockGUI()) { log::debug("not waiting for usvfs processes because user has disabled locking"); - return true; + return ForceUnlocked; } - bool r = false; + auto r = Error; - withLock([&](auto* uilock) { - r = waitForAllUSVFSProcesses(uilock); + withLock(reason, [&](auto& lock) { + r = waitForAllUSVFSProcesses(lock); }); return r; } -bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) +ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcesses(LockWidget& lock) { for (;;) { const auto processes = getRunningUSVFSProcesses(); @@ -681,26 +801,15 @@ bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) break; } - const auto r = waitForProcesses(processes, nullptr, uilock); + const auto r = waitForProcesses(processes, lock); - switch (r) - { - case WaitResults::Completed: - // this process is completed, check for others - break; - - case WaitResults::Cancelled: - // force unlocked - log::debug("waiting for process completion aborted by UI"); - return true; - - case WaitResults::Error: // fall-through - default: - log::debug("waiting for process completion not successful"); - return false; + if (r != Completed) { + // error, cancelled, or unlocked + return r; } + + // this process is completed, check for others } - log::debug("waiting for process completion successful"); - return true; + return Completed; } diff --git a/src/processrunner.h b/src/processrunner.h index 28f4da75..b7895903 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -2,10 +2,10 @@ #define PROCESSRUNNER_H #include "spawn.h" +#include "lockwidget.h" #include class OrganizerCore; -class ILockedWaitingForProcess; class IUserInterface; class Executable; class MOShortcut; @@ -35,9 +35,43 @@ private: class ProcessRunner { public: - ProcessRunner(OrganizerCore& core); + enum Results + { + Running = 1, + Completed, + Error, + Cancelled, + ForceUnlocked + }; + + using ForcedLibraries = QList; + + ProcessRunner(OrganizerCore& core, IUserInterface* ui); + + ProcessRunner& setBinary(const QFileInfo &binary); + ProcessRunner& setArguments(const QString& arguments); + ProcessRunner& setCurrentDirectory(const QDir& directory); + ProcessRunner& setSteamID(const QString& steamID); + ProcessRunner& setCustomOverwrite(const QString& customOverwrite); + ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries); + ProcessRunner& setProfileName(const QString& profileName); + ProcessRunner& setWaitForCompletion(LockWidget::Reasons reason, bool refresh); + + ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); + ProcessRunner& setFromExecutable(const Executable& exe); + ProcessRunner& setFromShortcut(const MOShortcut& shortcut); + + ProcessRunner& setFromFileOrExecutable( + const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profile, + const QString &forcedCustomOverwrite = "", + bool ignoreCustomOverwrite = false); + + Results run(); + DWORD exitCode(); - void setUserInterface(IUserInterface* ui); bool runFile(QWidget* parent, const QFileInfo& targetInfo); @@ -53,17 +87,31 @@ public: bool runShortcut(const MOShortcut& shortcut); HANDLE runExecutableOrExecutableFile( - const QString &executable, const QStringList &args, const QString &cwd, - const QString &profile, const QString &forcedCustomOverwrite = "", + const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profile, + const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false); - bool waitForApplication(HANDLE processHandle, LPDWORD exitCode = nullptr); - bool waitForAllUSVFSProcessesWithLock(); + Results waitForApplication( + HANDLE processHandle, LPDWORD exitCode, LockWidget::Reasons reason); + + Results waitForAllUSVFSProcessesWithLock(LockWidget::Reasons reason); private: OrganizerCore& m_core; IUserInterface* m_ui; + spawn::SpawnParameters m_sp; + QString m_customOverwrite; + ForcedLibraries m_forcedLibraries; + QString m_profileName; + LockWidget::Reasons m_lock; + bool m_refresh; + QString m_shellOpen; + HANDLE m_handle; + DWORD m_exitCode; HANDLE spawnAndWait( const QFileInfo &binary, const QString &arguments, @@ -76,11 +124,13 @@ private: SpawnedProcess spawn(spawn::SpawnParameters sp); - void withLock(std::function f); + void withLock( + LockWidget::Reasons reason, std::function f); - bool waitForProcessCompletionWithLock(HANDLE handle, LPDWORD exitCode); + Results waitForProcessCompletionWithLock( + HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason); - bool waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock); + Results waitForAllUSVFSProcesses(LockWidget& lock); }; #endif // PROCESSRUNNER_H -- cgit v1.3.1 From 72dd230cdc60e74446caceb5cfb4c6d32e4f6f68 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 05:58:41 -0400 Subject: removed unused files fixed handle leak when starting steam --- src/CMakeLists.txt | 17 +----- src/ilockedwaitingforprocess.h | 14 ----- src/iuserinterface.h | 1 - src/lockeddialog.cpp | 71 ------------------------ src/lockeddialog.h | 57 -------------------- src/lockeddialog.ui | 98 --------------------------------- src/lockeddialogbase.cpp | 78 --------------------------- src/lockeddialogbase.h | 64 ---------------------- src/mainwindow.cpp | 1 - src/organizercore.cpp | 1 - src/spawn.cpp | 3 +- src/spawn.h | 2 - src/waitingonclosedialog.cpp | 71 ------------------------ src/waitingonclosedialog.h | 56 ------------------- src/waitingonclosedialog.ui | 119 ----------------------------------------- 15 files changed, 2 insertions(+), 651 deletions(-) delete mode 100644 src/ilockedwaitingforprocess.h delete mode 100644 src/lockeddialog.cpp delete mode 100644 src/lockeddialog.h delete mode 100644 src/lockeddialog.ui delete mode 100644 src/lockeddialogbase.cpp delete mode 100644 src/lockeddialogbase.h delete mode 100644 src/waitingonclosedialog.cpp delete mode 100644 src/waitingonclosedialog.h delete mode 100644 src/waitingonclosedialog.ui (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 168b79dc..06f2cd1e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,9 +82,6 @@ SET(organizer_SRCS main.cpp loghighlighter.cpp loglist.cpp - lockeddialogbase.cpp - lockeddialog.cpp - waitingonclosedialog.cpp loadmechanism.cpp installationmanager.cpp filedialogmemory.cpp @@ -206,9 +203,6 @@ SET(organizer_HDRS mainwindow.h loghighlighter.h loglist.h - lockeddialogbase.h - lockeddialog.h - waitingonclosedialog.h loadmechanism.h installationmanager.h filedialogmemory.h @@ -246,7 +240,6 @@ SET(organizer_HDRS viewmarkingscrollbar.h plugincontainer.h organizercore.h - ilockedwaitingforprocess.h iuserinterface.h instancemanager.h usvfsconnector.h @@ -293,8 +286,6 @@ SET(organizer_UIS modinfodialog.ui messagedialog.ui mainwindow.ui - lockeddialog.ui - waitingonclosedialog.ui editexecutablesdialog.ui credentialsdialog.ui categoriesdialog.ui @@ -397,12 +388,6 @@ set(executables editexecutablesdialog ) -set(locking - ilockedwaitingforprocess - lockeddialog - lockeddialogbase -) - set(modinfo modinfo modinfobackup @@ -500,7 +485,7 @@ set(widgets ) set(src_filters - application core browser dialogs downloads env executables locking modinfo + application core browser dialogs downloads env executables modinfo modinfo\\dialog modlist plugins previews profiles settings settingsdialog utilities widgets ) diff --git a/src/ilockedwaitingforprocess.h b/src/ilockedwaitingforprocess.h deleted file mode 100644 index 4d1e786f..00000000 --- a/src/ilockedwaitingforprocess.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef ILOCKEDWAITINGFORPROCESS_H -#define ILOCKEDWAITINGFORPROCESS_H - -class QString; - -class ILockedWaitingForProcess -{ -public: - virtual bool unlockForced() const = 0; - virtual void setProcessName(QString const &) = 0; - virtual void setProcessInformation(DWORD pid, const QString& name) = 0; -}; - -#endif // ILOCKEDWAITINGFORPROCESS_H diff --git a/src/iuserinterface.h b/src/iuserinterface.h index e5755f03..99caceb1 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -3,7 +3,6 @@ #include "modinfodialogfwd.h" -#include "ilockedwaitingforprocess.h" #include "lockwidget.h" #include #include diff --git a/src/lockeddialog.cpp b/src/lockeddialog.cpp deleted file mode 100644 index 143d5838..00000000 --- a/src/lockeddialog.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "lockeddialog.h" -#include "ui_lockeddialog.h" - -#include -#include -#include -#include // for Qt::FramelessWindowHint, etc - -LockedDialog::LockedDialog(QWidget *parent, bool unlockByButton) - : LockedDialogBase(parent, !unlockByButton) - , ui(new Ui::LockedDialog) -{ - ui->setupUi(this); - - // Supposedly the Qt::CustomizeWindowHint should use a customized window - // allowing us to select if there is a close button. In practice this doesn't - // seem to work. We will ignore pressing the close button if unlockByButton == true - Qt::WindowFlags flags = - this->windowFlags() | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint; - if (m_allowClose) - flags |= Qt::WindowCloseButtonHint; - this->setWindowFlags(flags); - - if (!unlockByButton) - { - ui->unlockButton->hide(); - ui->verticalLayout->addItem( - new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding)); - } -} - -LockedDialog::~LockedDialog() -{ - delete ui; -} - - -void LockedDialog::setProcessName(const QString &name) -{ - ui->processLabel->setText(name); -} - -void LockedDialog::on_unlockButton_clicked() -{ - unlock(); -} - -void LockedDialog::unlock() { - LockedDialogBase::unlock(); - ui->label->setText("unlocking may take a few seconds"); - ui->unlockButton->setEnabled(false); -} diff --git a/src/lockeddialog.h b/src/lockeddialog.h deleted file mode 100644 index 36c16429..00000000 --- a/src/lockeddialog.h +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#pragma once - -#include "lockeddialogbase.h" - -namespace Ui { - class LockedDialog; -} - -/** - * a small borderless dialog displayed while the Mod Organizer UI is locked - * The dialog contains only a label and a button to force the UI to be unlocked - * - * The UI gets locked while running external applications since they may modify the - * data on which Mod Organizer works. After the UI is unlocked (manually or after the - * external application closed) MO will refresh all of its data sources - **/ -class LockedDialog : public LockedDialogBase -{ - Q_OBJECT - -public: - explicit LockedDialog(QWidget *parent = 0, bool unlockByButton = false); - ~LockedDialog(); - - void setProcessName(const QString &name) override; - -protected: - - void unlock() override; - -private slots: - - void on_unlockButton_clicked(); - -private: - - Ui::LockedDialog *ui; -}; diff --git a/src/lockeddialog.ui b/src/lockeddialog.ui deleted file mode 100644 index 0ec2e467..00000000 --- a/src/lockeddialog.ui +++ /dev/null @@ -1,98 +0,0 @@ - - - LockedDialog - - - - 0 - 0 - 317 - 151 - - - - Running virtualized processes - - - - - - This dialog should disappear automatically if the application/game is done. Click unlock if it didn't. - - - MO is locked while the executable is running. - - - Qt::AlignCenter - - - true - - - - - - - Qt::Vertical - - - - 10 - - - - - - - - - true - - - - color: grey; - - - - - - Qt::AlignCenter - - - - - - - Qt::Vertical - - - - 10 - - - - - - - - Unlock - - - - - - - Qt::Vertical - - - - 10 - - - - - - - - - diff --git a/src/lockeddialogbase.cpp b/src/lockeddialogbase.cpp deleted file mode 100644 index 0876a511..00000000 --- a/src/lockeddialogbase.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "lockeddialogbase.h" -#include "envmodule.h" -#include -#include -#include -#include // for Qt::FramelessWindowHint, etc - -LockedDialogBase::LockedDialogBase(QWidget *parent, bool allowClose) - : QDialog(parent) - , m_Unlocked(false) - , m_Canceled(false) - , m_allowClose(allowClose) -{ - if (parent != nullptr) { - QPoint position = parent->mapToGlobal(QPoint(parent->width() / 2, parent->height() / 2)); - position.rx() -= this->width() / 2; - position.ry() -= this->height() / 2; - move(position); - } -} - -void LockedDialogBase::resizeEvent(QResizeEvent *event) -{ - QWidget *par = parentWidget(); - if (par != nullptr) { - QPoint position = par->mapToGlobal(QPoint(par->width() / 2, par->height() / 2)); - position.rx() -= event->size().width() / 2; - position.ry() -= event->size().height() / 2; - move(position); - } -} - -void LockedDialogBase::reject() -{ - if (m_allowClose) - unlock(); -} - -bool LockedDialogBase::unlockForced() const { - return m_Unlocked; -} - -bool LockedDialogBase::canceled() const { - return m_Canceled; -} - -void LockedDialogBase::setProcessInformation(DWORD pid, const QString& name) -{ - setProcessName(QString("%1 (%2)").arg(name).arg(pid)); -} - -void LockedDialogBase::unlock() { - m_Unlocked = true; -} - -void LockedDialogBase::cancel() { - m_Canceled = true; -} - diff --git a/src/lockeddialogbase.h b/src/lockeddialogbase.h deleted file mode 100644 index 4ebad4c7..00000000 --- a/src/lockeddialogbase.h +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#pragma once - -#include "ilockedwaitingforprocess.h" -#include // for QDialog -#include // for Q_OBJECT, slots -#include // for QString - -class QResizeEvent; -class QWidget; - -/** - * a small borderless dialog displayed while the Mod Organizer UI is locked - * The dialog contains only a label and a button to force the UI to be unlocked - * - * The UI gets locked while running external applications since they may modify the - * data on which Mod Organizer works. After the UI is unlocked (manually or after the - * external application closed) MO will refresh all of its data sources - **/ -class LockedDialogBase : public QDialog, public ILockedWaitingForProcess -{ - Q_OBJECT - -public: - explicit LockedDialogBase(QWidget *parent, bool allowClose); - - bool unlockForced() const override; - - virtual bool canceled() const; - - void setProcessInformation(DWORD pid, const QString& name) override; - -protected: - - virtual void resizeEvent(QResizeEvent *event); - - virtual void reject(); - - virtual void unlock(); - - virtual void cancel(); - - bool m_Unlocked; - bool m_Canceled; - bool m_allowClose; -}; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 631a31d8..9453f07c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -57,7 +57,6 @@ along with Mod Organizer. If not, see . #include "downloadlistwidget.h" #include "messagedialog.h" #include "installationmanager.h" -#include "waitingonclosedialog.h" #include "downloadlistsortproxy.h" #include "motddialog.h" #include "filedialogmemory.h" diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 3c535868..dda60f76 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -29,7 +29,6 @@ #include "appconfig.h" #include #include -#include "lockeddialog.h" #include "instancemanager.h" #include #include "previewdialog.h" diff --git a/src/spawn.cpp b/src/spawn.cpp index b331db01..62745542 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -27,8 +27,6 @@ along with Mod Organizer. If not, see . #include "envmodule.h" #include "settings.h" #include "settingsdialogworkarounds.h" -#include -#include #include #include #include @@ -645,6 +643,7 @@ bool startSteam(QWidget* parent) HANDLE ph = INVALID_HANDLE_VALUE; const auto e = spawn(sp, ph); + ::CloseHandle(ph); if (e != ERROR_SUCCESS) { // make sure username and passwords are not shown diff --git a/src/spawn.h b/src/spawn.h index 0464ffd6..9fb346b0 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -28,8 +28,6 @@ along with Mod Organizer. If not, see . class Settings; -namespace MOBase { class IPluginGame; } - namespace spawn { diff --git a/src/waitingonclosedialog.cpp b/src/waitingonclosedialog.cpp deleted file mode 100644 index 565d0a36..00000000 --- a/src/waitingonclosedialog.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "waitingonclosedialog.h" -#include "ui_waitingonclosedialog.h" - -#include -#include -#include -#include // for Qt::FramelessWindowHint, etc - -WaitingOnCloseDialog::WaitingOnCloseDialog(QWidget *parent) - : LockedDialogBase(parent,true) - , ui(new Ui::WaitingOnCloseDialog) -{ - ui->setupUi(this); - - // Supposedly the Qt::CustomizeWindowHint should use a customized window - // allowing us to select if there is a close button. In practice this doesn't - // seem to work. We will ignore pressing the close button if unlockByButton == true - Qt::WindowFlags flags = - this->windowFlags() | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint; - if (m_allowClose) - flags |= Qt::WindowCloseButtonHint; - this->setWindowFlags(flags); -} - -WaitingOnCloseDialog::~WaitingOnCloseDialog() -{ - delete ui; -} - - -void WaitingOnCloseDialog::setProcessName(const QString &name) -{ - ui->processLabel->setText(name); -} - -void WaitingOnCloseDialog::on_closeButton_clicked() -{ - unlock(); -} - -void WaitingOnCloseDialog::on_cancelButton_clicked() -{ - cancel(); - unlock(); -} - -void WaitingOnCloseDialog::unlock() { - LockedDialogBase::unlock(); - ui->label->setText("unlocking may take a few seconds"); - ui->closeButton->setEnabled(false); - ui->cancelButton->setEnabled(false); -} diff --git a/src/waitingonclosedialog.h b/src/waitingonclosedialog.h deleted file mode 100644 index 6650c390..00000000 --- a/src/waitingonclosedialog.h +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#pragma once - -#include "lockeddialogbase.h" - -namespace Ui { - class WaitingOnCloseDialog; -} - -/** - * Similar to the LockedDialog but used for waiting on running process during - * a process close request which requries a slightly different dialog. - **/ -class WaitingOnCloseDialog : public LockedDialogBase -{ - Q_OBJECT - -public: - explicit WaitingOnCloseDialog(QWidget *parent = 0); - ~WaitingOnCloseDialog(); - - bool canceled() const { return m_Canceled; } - - void setProcessName(const QString &name) override; - -protected: - - void unlock() override; - -private slots: - - void on_closeButton_clicked(); - void on_cancelButton_clicked(); - -private: - - Ui::WaitingOnCloseDialog *ui; -}; diff --git a/src/waitingonclosedialog.ui b/src/waitingonclosedialog.ui deleted file mode 100644 index 9c7818e0..00000000 --- a/src/waitingonclosedialog.ui +++ /dev/null @@ -1,119 +0,0 @@ - - - WaitingOnCloseDialog - - - - 0 - 0 - 317 - 151 - - - - Waiting for virtualized processes - - - - - - This dialog should disappear automatically if the application/game is done. - - - Virtualized processes are still running, it is prefered to keep MO running until they are finished. - - - true - - - - - - - Qt::Vertical - - - - 10 - - - - - - - - - true - - - - color: grey; - - - - - - Qt::AlignCenter - - - - - - - Qt::Vertical - - - - 10 - - - - - - - - - - Close Now - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Cancel - - - - - - - - - Qt::Vertical - - - - 10 - - - - - - - - - -- cgit v1.3.1 From decd5c1828f495be4e230c9fc6fb79dd9bfdfb81 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:28:11 -0500 Subject: renamed lockwidget files to uilocker --- src/CMakeLists.txt | 6 +- src/iuserinterface.h | 1 - src/lockwidget.cpp | 548 --------------------------------------------------- src/lockwidget.h | 95 --------- src/organizercore.h | 2 +- src/processrunner.h | 2 +- src/uilocker.cpp | 548 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/uilocker.h | 95 +++++++++ 8 files changed, 648 insertions(+), 649 deletions(-) delete mode 100644 src/lockwidget.cpp delete mode 100644 src/lockwidget.h create mode 100644 src/uilocker.cpp create mode 100644 src/uilocker.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 06f2cd1e..d935981d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,7 +141,7 @@ SET(organizer_SRCS colortable.cpp sanitychecks.cpp processrunner.cpp - lockwidget.cpp + uilocker.cpp shared/windows_error.cpp shared/error_report.cpp @@ -262,7 +262,7 @@ SET(organizer_HDRS envwindows.h colortable.h processrunner.h - lockwidget.h + uilocker.h shared/windows_error.h shared/error_report.h @@ -344,6 +344,7 @@ set(core organizerproxy apiuseraccount processrunner + uilocker ) set(dialogs @@ -473,7 +474,6 @@ set(widgets filterwidget icondelegate lcdnumber - lockwidget loglist loghighlighter modflagicondelegate diff --git a/src/iuserinterface.h b/src/iuserinterface.h index 99caceb1..aa48194f 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -3,7 +3,6 @@ #include "modinfodialogfwd.h" -#include "lockwidget.h" #include #include #include diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp deleted file mode 100644 index 150d2847..00000000 --- a/src/lockwidget.cpp +++ /dev/null @@ -1,548 +0,0 @@ -#include "lockwidget.h" -#include "mainwindow.h" -#include -#include -#include - -class UILockerInterface -{ -public: - UILockerInterface(QWidget* mainUI) : - m_mainUI(mainUI), m_target(nullptr), m_message(nullptr), m_info(nullptr), - m_buttons(nullptr), m_reason(UILocker::NoReason) - { - m_timer.reset(new QTimer); - QObject::connect(m_timer.get(), &QTimer::timeout, [&]{ checkTarget(); }); - m_timer->start(200); - - set(); - } - - ~UILockerInterface() - { - } - - void checkTarget() - { - if (set()) { - update(m_reason); - } - } - - bool set() - { - QWidget* newTarget = nullptr; - - newTarget = m_mainUI; - if (auto* w = QApplication::activeModalWidget()) { - newTarget = w; - } - - if (newTarget == m_target) { - return false; - } - - m_target = newTarget; - - QFrame* center = nullptr; - - if (m_target) { - center = createOverlay(m_target); - } else { - center = createDialog(); - } - - createMessageLabel(); - createInfoLabel(); - createButtonsPanel(); - - center->layout()->addWidget(m_message); - center->layout()->addWidget(m_info); - center->layout()->addWidget(m_buttons); - - m_topLevel->setFocusPolicy(Qt::TabFocus); - m_topLevel->setFocus(); - m_topLevel->show(); - m_topLevel->setEnabled(true); - - m_topLevel->raise(); - m_topLevel->activateWindow(); - - return true; - } - - void update(UILocker::Reasons reason) - { - m_reason = reason; - updateMessage(reason); - updateButtons(reason); - setInfo(m_labels); - } - - void setInfo(const QStringList& labels) - { - const int MaxLabels = 2; - - m_labels = labels; - - QString s; - - if (labels.size() > MaxLabels) { - s = labels.mid(0, MaxLabels).join(", ") + "..."; - } else { - s = labels.join(", "); - } - - m_info->setText(s); - } - - QWidget* topLevel() - { - return m_topLevel.get(); - } - -private: - class Filter : public QObject - { - public: - std::function resized; - std::function closed; - - protected: - bool eventFilter(QObject* o, QEvent* e) override - { - if (e->type() == QEvent::Resize) { - if (resized) { - resized(); - } - } else if (e->type() == QEvent::Close) { - if (closed) { - closed(); - } - } - - return QObject::eventFilter(o, e); - } - }; - - - std::unique_ptr m_timer; - QWidget* m_mainUI; - QWidget* m_target; - std::unique_ptr m_topLevel; - QLabel* m_message; - QLabel* m_info; - QStringList m_labels; - QWidget* m_buttons; - std::unique_ptr m_filter; - UILocker::Reasons m_reason; - - - bool hasMainUI() const - { - return (m_target != nullptr); - } - - QWidget* createTransparentWidget(QWidget* parent=nullptr) - { - auto* w = new QWidget(parent); - - w->setWindowOpacity(0); - w->setAttribute(Qt::WA_NoSystemBackground); - w->setAttribute(Qt::WA_TranslucentBackground); - - return w; - } - - QFrame* createOverlay(QWidget* mainUI) - { - m_topLevel.reset(createTransparentWidget(mainUI)); - m_topLevel->setWindowFlags(m_topLevel->windowFlags() & Qt::FramelessWindowHint); - m_topLevel->setGeometry(mainUI->rect()); - - m_filter.reset(new Filter); - m_filter->resized = [=]{ m_topLevel->setGeometry(mainUI->rect()); }; - m_filter->closed = [=]{ checkTarget(); }; - - mainUI->installEventFilter(m_filter.get()); - - return createFrame(); - } - - QFrame* createDialog() - { - m_topLevel.reset(new QDialog); - - return createFrame(); - } - - QFrame* createFrame() - { - auto* frame = new QFrame; - auto* ly = new QVBoxLayout(frame); - - if (hasMainUI()) { - frame->setFrameStyle(QFrame::StyledPanel); - frame->setLineWidth(1); - frame->setAutoFillBackground(true); - - auto* shadow = new QGraphicsDropShadowEffect; - shadow->setBlurRadius(50); - shadow->setOffset(0); - shadow->setColor(QColor(0, 0, 0, 100)); - frame->setGraphicsEffect(shadow); - } else { - ly->setContentsMargins(0, 0, 0, 0); - } - - auto* grid = new QGridLayout(m_topLevel.get()); - grid->addWidget(createTransparentWidget(), 0, 1); - grid->addWidget(createTransparentWidget(), 2, 1); - grid->addWidget(createTransparentWidget(), 1, 0); - grid->addWidget(createTransparentWidget(), 1, 2); - grid->addWidget(frame, 1, 1); - - if (!hasMainUI()) { - grid->setContentsMargins(0, 0, 0, 0); - } - - grid->setRowStretch(0, 1); - grid->setRowStretch(2, 1); - grid->setColumnStretch(0, 1); - grid->setColumnStretch(2, 1); - - return frame; - } - - void createMessageLabel() - { - m_message = new QLabel; - m_message->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); - } - - void createInfoLabel() - { - m_info = new QLabel(" "); - m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); - } - - void createButtonsPanel() - { - m_buttons = new QWidget; - m_buttons->setLayout(new QHBoxLayout); - } - - - void updateMessage(UILocker::Reasons reason) - { - switch (reason) - { - case UILocker::LockUI: - { - QString s; - - if (hasMainUI()) { - s = QObject::tr( - "Mod Organizer is locked while the application is running."); - } else { - s = QObject::tr("Mod Organizer is currently running an application."); - } - - m_message->setText(s); - - break; - } - - case UILocker::OutputRequired: - { - m_message->setText(QObject::tr( - "The application must run to completion because its output is " - "required.")); - - break; - } - - case UILocker::PreventExit: - { - m_message->setText(QObject::tr( - "Mod Organizer is waiting on application to close before exiting.")); - - break; - } - } - } - - void updateButtons(UILocker::Reasons reason) - { - MOBase::deleteChildWidgets(m_buttons); - auto* ly = m_buttons->layout(); - - switch (reason) - { - case UILocker::LockUI: // fall-through - case UILocker::OutputRequired: - { - auto* unlock = new QPushButton(QObject::tr("Unlock")); - - QObject::connect(unlock, &QPushButton::clicked, [&]{ - UILocker::instance().onForceUnlock(); - }); - - ly->addWidget(unlock); - - break; - } - - case UILocker::PreventExit: - { - auto* exit = new QPushButton(QObject::tr("Exit Now")); - QObject::connect(exit, &QPushButton::clicked, [&]{ - UILocker::instance().onForceUnlock(); - }); - - ly->addWidget(exit); - - auto* cancel = new QPushButton(QObject::tr("Cancel")); - QObject::connect(cancel, &QPushButton::clicked, [&]{ - UILocker::instance().onCancel(); - }); - - ly->addWidget(cancel); - - break; - } - } - } -}; - -UILocker::Session::~Session() -{ - unlock(); -} - -void UILocker::Session::unlock() -{ - QMetaObject::invokeMethod(qApp, [this]{ - UILocker::instance().unlock(this); - }); -} - -void UILocker::Session::setInfo(DWORD pid, const QString& name) -{ - { - std::scoped_lock lock(m_mutex); - m_pid = pid; - m_name = name; - } - - QMetaObject::invokeMethod(qApp, [this]{ - UILocker::instance().updateLabel(); - }); -} - -DWORD UILocker::Session::pid() const -{ - std::scoped_lock lock(m_mutex); - return m_pid; -} - -const QString& UILocker::Session::name() const -{ - std::scoped_lock lock(m_mutex); - return m_name; -} - -UILocker::Results UILocker::Session::result() const -{ - return UILocker::instance().result(); -} - - -static UILocker* g_instance = nullptr; - - -UILocker::UILocker() - : m_parent(nullptr), m_result(NoResult) -{ - Q_ASSERT(!g_instance); - g_instance = this; -} - -UILocker::~UILocker() -{ - const auto v = m_sessions; - - for (auto& wp : v) { - if (auto s=wp.lock()) { - unlock(s.get()); - } - } -} - -UILocker& UILocker::instance() -{ - Q_ASSERT(g_instance); - return *g_instance; -} - -void UILocker::setUserInterface(QWidget* parent) -{ - m_parent = parent; -} - -std::shared_ptr UILocker::lock(Reasons reason) -{ - m_result = StillLocked; - createUi(reason); - - auto ls = std::make_shared(); - m_sessions.push_back(ls); - - updateLabel(); - - return ls; -} - -void UILocker::unlock(Session* s) -{ - auto itor = m_sessions.begin(); - for (;;) { - if (itor == m_sessions.end()) { - break; - } - - if (auto ss=itor->lock()) { - if (ss.get() == s) { - itor = m_sessions.erase(itor); - continue; - } - } else { - itor = m_sessions.erase(itor); - continue; - } - - ++itor; - } - - if (m_sessions.empty()) { - m_ui.reset(); - enableAll(); - } else { - updateLabel(); - } -} - -void UILocker::unlockCurrent() -{ - if (m_sessions.empty()) { - return; - } - - auto s = m_sessions.back().lock(); - if (!s) { - m_sessions.pop_back(); - return; - } - - unlock(s.get()); -} - -void UILocker::updateLabel() -{ - QStringList labels; - - for (auto itor=m_sessions.rbegin(); itor!=m_sessions.rend(); ++itor) { - if (auto ss=itor->lock()) { - labels.push_back(QString("%1 (%2)").arg(ss->name()).arg(ss->pid())); - } - } - - m_ui->setInfo(labels); -} - -UILocker::Results UILocker::result() const -{ - return m_result; -} - -void UILocker::createUi(Reasons reason) -{ - if (!m_ui) { - m_ui.reset(new UILockerInterface(m_parent)); - } - - m_ui->update(reason); - - disableAll(); -} - -void UILocker::onForceUnlock() -{ - m_result = ForceUnlocked; - unlockCurrent(); -} - -void UILocker::onCancel() -{ - m_result = Cancelled; - unlockCurrent(); -} - -template -QList findChildrenImmediate(QWidget* parent) -{ - return parent->findChildren(QString(), Qt::FindDirectChildrenOnly); -} - -void UILocker::disableAll() -{ - const auto topLevels = QApplication::topLevelWidgets(); - - for (auto* w : topLevels) { - if (auto* mw=dynamic_cast(w)) { - disable(mw->centralWidget()); - disable(mw->menuBar()); - disable(mw->statusBar()); - - for (auto* tb : findChildrenImmediate(w)) { - disable(tb); - } - - for (auto* d : findChildrenImmediate(w)) { - disable(d); - } - } - - if (auto* d=dynamic_cast(w)) { - // don't disable stuff if this dialog is the overlay, which happens when - // there's no ui - if (d != m_ui->topLevel()) { - // no central widget, just disable the children, except for the overlay - for (auto* child : findChildrenImmediate(d)) { - if (child != m_ui->topLevel()) { - disable(child); - } - } - } - } - } -} - -void UILocker::enableAll() -{ - for (auto w : m_disabled) { - if (w) { - w->setEnabled(true); - } - } - - m_disabled.clear(); -} - -void UILocker::disable(QWidget* w) -{ - if (w->isEnabled()) { - w->setEnabled(false); - m_disabled.push_back(w); - } -} diff --git a/src/lockwidget.h b/src/lockwidget.h deleted file mode 100644 index d8c22999..00000000 --- a/src/lockwidget.h +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once - -#include -#include - -class UILockerInterface; - -class UILocker -{ - friend class UILockerInterface; - -public: - // reason to show the widget - // - enum Reasons - { - NoReason = 0, - - // lock the ui - LockUI, - - // because the output is required - OutputRequired, - - // to prevent exiting until all processes are completed - PreventExit - }; - - // returned by result() - // - enum Results - { - NoResult = 0, - - // the widget is still up - StillLocked, - - // force unlock was clicked - ForceUnlocked, - - // cancel was clicked - Cancelled - }; - - - class Session - { - public: - ~Session(); - - void unlock(); - void setInfo(DWORD pid, const QString& name); - Results result() const; - - DWORD pid() const; - const QString& name() const; - - private: - mutable std::mutex m_mutex; - DWORD m_pid; - QString m_name; - }; - - - UILocker(); - ~UILocker(); - - static UILocker& instance(); - - void setUserInterface(QWidget* parent); - - std::shared_ptr lock(Reasons reason); - - Results result() const; - -private: - QWidget* m_parent; - std::unique_ptr m_ui; - std::vector> m_sessions; - std::atomic m_result; - std::vector> m_disabled; - - void createUi(Reasons reason); - - void unlockCurrent(); - void unlock(Session* s); - void updateLabel(); - - void onForceUnlock(); - void onCancel(); - - void disableAll(); - void enableAll(); - void disable(QWidget* w); -}; diff --git a/src/organizercore.h b/src/organizercore.h index 47630dc2..6c9edb9f 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -14,7 +14,7 @@ #include "usvfsconnector.h" #include "moshortcut.h" #include "processrunner.h" -#include "lockwidget.h" +#include "uilocker.h" #include #include #include diff --git a/src/processrunner.h b/src/processrunner.h index af5cd416..c61d6b70 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -2,7 +2,7 @@ #define PROCESSRUNNER_H #include "spawn.h" -#include "lockwidget.h" +#include "uilocker.h" #include "envmodule.h" #include diff --git a/src/uilocker.cpp b/src/uilocker.cpp new file mode 100644 index 00000000..01794d6b --- /dev/null +++ b/src/uilocker.cpp @@ -0,0 +1,548 @@ +#include "uilocker.h" +#include "mainwindow.h" +#include +#include +#include + +class UILockerInterface +{ +public: + UILockerInterface(QWidget* mainUI) : + m_mainUI(mainUI), m_target(nullptr), m_message(nullptr), m_info(nullptr), + m_buttons(nullptr), m_reason(UILocker::NoReason) + { + m_timer.reset(new QTimer); + QObject::connect(m_timer.get(), &QTimer::timeout, [&]{ checkTarget(); }); + m_timer->start(200); + + set(); + } + + ~UILockerInterface() + { + } + + void checkTarget() + { + if (set()) { + update(m_reason); + } + } + + bool set() + { + QWidget* newTarget = nullptr; + + newTarget = m_mainUI; + if (auto* w = QApplication::activeModalWidget()) { + newTarget = w; + } + + if (newTarget == m_target) { + return false; + } + + m_target = newTarget; + + QFrame* center = nullptr; + + if (m_target) { + center = createOverlay(m_target); + } else { + center = createDialog(); + } + + createMessageLabel(); + createInfoLabel(); + createButtonsPanel(); + + center->layout()->addWidget(m_message); + center->layout()->addWidget(m_info); + center->layout()->addWidget(m_buttons); + + m_topLevel->setFocusPolicy(Qt::TabFocus); + m_topLevel->setFocus(); + m_topLevel->show(); + m_topLevel->setEnabled(true); + + m_topLevel->raise(); + m_topLevel->activateWindow(); + + return true; + } + + void update(UILocker::Reasons reason) + { + m_reason = reason; + updateMessage(reason); + updateButtons(reason); + setInfo(m_labels); + } + + void setInfo(const QStringList& labels) + { + const int MaxLabels = 2; + + m_labels = labels; + + QString s; + + if (labels.size() > MaxLabels) { + s = labels.mid(0, MaxLabels).join(", ") + "..."; + } else { + s = labels.join(", "); + } + + m_info->setText(s); + } + + QWidget* topLevel() + { + return m_topLevel.get(); + } + +private: + class Filter : public QObject + { + public: + std::function resized; + std::function closed; + + protected: + bool eventFilter(QObject* o, QEvent* e) override + { + if (e->type() == QEvent::Resize) { + if (resized) { + resized(); + } + } else if (e->type() == QEvent::Close) { + if (closed) { + closed(); + } + } + + return QObject::eventFilter(o, e); + } + }; + + + std::unique_ptr m_timer; + QWidget* m_mainUI; + QWidget* m_target; + std::unique_ptr m_topLevel; + QLabel* m_message; + QLabel* m_info; + QStringList m_labels; + QWidget* m_buttons; + std::unique_ptr m_filter; + UILocker::Reasons m_reason; + + + bool hasMainUI() const + { + return (m_target != nullptr); + } + + QWidget* createTransparentWidget(QWidget* parent=nullptr) + { + auto* w = new QWidget(parent); + + w->setWindowOpacity(0); + w->setAttribute(Qt::WA_NoSystemBackground); + w->setAttribute(Qt::WA_TranslucentBackground); + + return w; + } + + QFrame* createOverlay(QWidget* mainUI) + { + m_topLevel.reset(createTransparentWidget(mainUI)); + m_topLevel->setWindowFlags(m_topLevel->windowFlags() & Qt::FramelessWindowHint); + m_topLevel->setGeometry(mainUI->rect()); + + m_filter.reset(new Filter); + m_filter->resized = [=]{ m_topLevel->setGeometry(mainUI->rect()); }; + m_filter->closed = [=]{ checkTarget(); }; + + mainUI->installEventFilter(m_filter.get()); + + return createFrame(); + } + + QFrame* createDialog() + { + m_topLevel.reset(new QDialog); + + return createFrame(); + } + + QFrame* createFrame() + { + auto* frame = new QFrame; + auto* ly = new QVBoxLayout(frame); + + if (hasMainUI()) { + frame->setFrameStyle(QFrame::StyledPanel); + frame->setLineWidth(1); + frame->setAutoFillBackground(true); + + auto* shadow = new QGraphicsDropShadowEffect; + shadow->setBlurRadius(50); + shadow->setOffset(0); + shadow->setColor(QColor(0, 0, 0, 100)); + frame->setGraphicsEffect(shadow); + } else { + ly->setContentsMargins(0, 0, 0, 0); + } + + auto* grid = new QGridLayout(m_topLevel.get()); + grid->addWidget(createTransparentWidget(), 0, 1); + grid->addWidget(createTransparentWidget(), 2, 1); + grid->addWidget(createTransparentWidget(), 1, 0); + grid->addWidget(createTransparentWidget(), 1, 2); + grid->addWidget(frame, 1, 1); + + if (!hasMainUI()) { + grid->setContentsMargins(0, 0, 0, 0); + } + + grid->setRowStretch(0, 1); + grid->setRowStretch(2, 1); + grid->setColumnStretch(0, 1); + grid->setColumnStretch(2, 1); + + return frame; + } + + void createMessageLabel() + { + m_message = new QLabel; + m_message->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + } + + void createInfoLabel() + { + m_info = new QLabel(" "); + m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + } + + void createButtonsPanel() + { + m_buttons = new QWidget; + m_buttons->setLayout(new QHBoxLayout); + } + + + void updateMessage(UILocker::Reasons reason) + { + switch (reason) + { + case UILocker::LockUI: + { + QString s; + + if (hasMainUI()) { + s = QObject::tr( + "Mod Organizer is locked while the application is running."); + } else { + s = QObject::tr("Mod Organizer is currently running an application."); + } + + m_message->setText(s); + + break; + } + + case UILocker::OutputRequired: + { + m_message->setText(QObject::tr( + "The application must run to completion because its output is " + "required.")); + + break; + } + + case UILocker::PreventExit: + { + m_message->setText(QObject::tr( + "Mod Organizer is waiting on application to close before exiting.")); + + break; + } + } + } + + void updateButtons(UILocker::Reasons reason) + { + MOBase::deleteChildWidgets(m_buttons); + auto* ly = m_buttons->layout(); + + switch (reason) + { + case UILocker::LockUI: // fall-through + case UILocker::OutputRequired: + { + auto* unlock = new QPushButton(QObject::tr("Unlock")); + + QObject::connect(unlock, &QPushButton::clicked, [&]{ + UILocker::instance().onForceUnlock(); + }); + + ly->addWidget(unlock); + + break; + } + + case UILocker::PreventExit: + { + auto* exit = new QPushButton(QObject::tr("Exit Now")); + QObject::connect(exit, &QPushButton::clicked, [&]{ + UILocker::instance().onForceUnlock(); + }); + + ly->addWidget(exit); + + auto* cancel = new QPushButton(QObject::tr("Cancel")); + QObject::connect(cancel, &QPushButton::clicked, [&]{ + UILocker::instance().onCancel(); + }); + + ly->addWidget(cancel); + + break; + } + } + } +}; + +UILocker::Session::~Session() +{ + unlock(); +} + +void UILocker::Session::unlock() +{ + QMetaObject::invokeMethod(qApp, [this]{ + UILocker::instance().unlock(this); + }); +} + +void UILocker::Session::setInfo(DWORD pid, const QString& name) +{ + { + std::scoped_lock lock(m_mutex); + m_pid = pid; + m_name = name; + } + + QMetaObject::invokeMethod(qApp, [this]{ + UILocker::instance().updateLabel(); + }); +} + +DWORD UILocker::Session::pid() const +{ + std::scoped_lock lock(m_mutex); + return m_pid; +} + +const QString& UILocker::Session::name() const +{ + std::scoped_lock lock(m_mutex); + return m_name; +} + +UILocker::Results UILocker::Session::result() const +{ + return UILocker::instance().result(); +} + + +static UILocker* g_instance = nullptr; + + +UILocker::UILocker() + : m_parent(nullptr), m_result(NoResult) +{ + Q_ASSERT(!g_instance); + g_instance = this; +} + +UILocker::~UILocker() +{ + const auto v = m_sessions; + + for (auto& wp : v) { + if (auto s=wp.lock()) { + unlock(s.get()); + } + } +} + +UILocker& UILocker::instance() +{ + Q_ASSERT(g_instance); + return *g_instance; +} + +void UILocker::setUserInterface(QWidget* parent) +{ + m_parent = parent; +} + +std::shared_ptr UILocker::lock(Reasons reason) +{ + m_result = StillLocked; + createUi(reason); + + auto ls = std::make_shared(); + m_sessions.push_back(ls); + + updateLabel(); + + return ls; +} + +void UILocker::unlock(Session* s) +{ + auto itor = m_sessions.begin(); + for (;;) { + if (itor == m_sessions.end()) { + break; + } + + if (auto ss=itor->lock()) { + if (ss.get() == s) { + itor = m_sessions.erase(itor); + continue; + } + } else { + itor = m_sessions.erase(itor); + continue; + } + + ++itor; + } + + if (m_sessions.empty()) { + m_ui.reset(); + enableAll(); + } else { + updateLabel(); + } +} + +void UILocker::unlockCurrent() +{ + if (m_sessions.empty()) { + return; + } + + auto s = m_sessions.back().lock(); + if (!s) { + m_sessions.pop_back(); + return; + } + + unlock(s.get()); +} + +void UILocker::updateLabel() +{ + QStringList labels; + + for (auto itor=m_sessions.rbegin(); itor!=m_sessions.rend(); ++itor) { + if (auto ss=itor->lock()) { + labels.push_back(QString("%1 (%2)").arg(ss->name()).arg(ss->pid())); + } + } + + m_ui->setInfo(labels); +} + +UILocker::Results UILocker::result() const +{ + return m_result; +} + +void UILocker::createUi(Reasons reason) +{ + if (!m_ui) { + m_ui.reset(new UILockerInterface(m_parent)); + } + + m_ui->update(reason); + + disableAll(); +} + +void UILocker::onForceUnlock() +{ + m_result = ForceUnlocked; + unlockCurrent(); +} + +void UILocker::onCancel() +{ + m_result = Cancelled; + unlockCurrent(); +} + +template +QList findChildrenImmediate(QWidget* parent) +{ + return parent->findChildren(QString(), Qt::FindDirectChildrenOnly); +} + +void UILocker::disableAll() +{ + const auto topLevels = QApplication::topLevelWidgets(); + + for (auto* w : topLevels) { + if (auto* mw=dynamic_cast(w)) { + disable(mw->centralWidget()); + disable(mw->menuBar()); + disable(mw->statusBar()); + + for (auto* tb : findChildrenImmediate(w)) { + disable(tb); + } + + for (auto* d : findChildrenImmediate(w)) { + disable(d); + } + } + + if (auto* d=dynamic_cast(w)) { + // don't disable stuff if this dialog is the overlay, which happens when + // there's no ui + if (d != m_ui->topLevel()) { + // no central widget, just disable the children, except for the overlay + for (auto* child : findChildrenImmediate(d)) { + if (child != m_ui->topLevel()) { + disable(child); + } + } + } + } + } +} + +void UILocker::enableAll() +{ + for (auto w : m_disabled) { + if (w) { + w->setEnabled(true); + } + } + + m_disabled.clear(); +} + +void UILocker::disable(QWidget* w) +{ + if (w->isEnabled()) { + w->setEnabled(false); + m_disabled.push_back(w); + } +} diff --git a/src/uilocker.h b/src/uilocker.h new file mode 100644 index 00000000..d8c22999 --- /dev/null +++ b/src/uilocker.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +class UILockerInterface; + +class UILocker +{ + friend class UILockerInterface; + +public: + // reason to show the widget + // + enum Reasons + { + NoReason = 0, + + // lock the ui + LockUI, + + // because the output is required + OutputRequired, + + // to prevent exiting until all processes are completed + PreventExit + }; + + // returned by result() + // + enum Results + { + NoResult = 0, + + // the widget is still up + StillLocked, + + // force unlock was clicked + ForceUnlocked, + + // cancel was clicked + Cancelled + }; + + + class Session + { + public: + ~Session(); + + void unlock(); + void setInfo(DWORD pid, const QString& name); + Results result() const; + + DWORD pid() const; + const QString& name() const; + + private: + mutable std::mutex m_mutex; + DWORD m_pid; + QString m_name; + }; + + + UILocker(); + ~UILocker(); + + static UILocker& instance(); + + void setUserInterface(QWidget* parent); + + std::shared_ptr lock(Reasons reason); + + Results result() const; + +private: + QWidget* m_parent; + std::unique_ptr m_ui; + std::vector> m_sessions; + std::atomic m_result; + std::vector> m_disabled; + + void createUi(Reasons reason); + + void unlockCurrent(); + void unlock(Session* s); + void updateLabel(); + + void onForceUnlock(); + void onCancel(); + + void disableAll(); + void enableAll(); + void disable(QWidget* w); +}; -- cgit v1.3.1 From 838446e262ac628c748b2e2b2f2ec0cfaff01508 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 16 Nov 2019 19:41:17 -0500 Subject: split loot stuff to loot.h/cpp, no changes in functionality --- src/CMakeLists.txt | 3 + src/loot.cpp | 244 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/loot.h | 10 +++ src/mainwindow.cpp | 227 +------------------------------------------------ src/mainwindow.h | 4 - 5 files changed, 261 insertions(+), 227 deletions(-) create mode 100644 src/loot.cpp create mode 100644 src/loot.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d935981d..70c501ae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -142,6 +142,7 @@ SET(organizer_SRCS sanitychecks.cpp processrunner.cpp uilocker.cpp + loot.cpp shared/windows_error.cpp shared/error_report.cpp @@ -263,6 +264,7 @@ SET(organizer_HDRS colortable.h processrunner.h uilocker.h + loot.h shared/windows_error.h shared/error_report.h @@ -465,6 +467,7 @@ set(utilities shared/util usvfsconnector shared/windows_error + loot ) set(widgets diff --git a/src/loot.cpp b/src/loot.cpp new file mode 100644 index 00000000..b19c59ed --- /dev/null +++ b/src/loot.cpp @@ -0,0 +1,244 @@ +#include "spawn.h" +#include "organizercore.h" +#include +#include + +using namespace MOBase; + +void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) +{ + SECURITY_ATTRIBUTES secAttributes; + secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + secAttributes.bInheritHandle = TRUE; + secAttributes.lpSecurityDescriptor = nullptr; + + if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { + log::error("failed to create stdout reroute"); + } + + if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { + log::error("failed to correctly set up the stdout reroute"); + *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; + } +} + +std::string readFromPipe(HANDLE stdOutRead) +{ + static const int chunkSize = 128; + std::string result; + + char buffer[chunkSize + 1]; + buffer[chunkSize] = '\0'; + + DWORD read = 1; + while (read > 0) { + if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { + break; + } + if (read > 0) { + result.append(buffer, read); + if (read < chunkSize) { + break; + } + } + } + return result; +} + +void processLOOTOut( + OrganizerCore& core, + const std::string &lootOut, std::string &errorMessages, + QProgressDialog &dialog) +{ + std::vector lines; + boost::split(lines, lootOut, boost::is_any_of("\r\n")); + + std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); + std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); + + for (const std::string &line : lines) { + if (line.length() > 0) { + size_t progidx = line.find("[progress]"); + size_t erroridx = line.find("[error]"); + if (progidx != std::string::npos) { + dialog.setLabelText(line.substr(progidx + 11).c_str()); + } else if (erroridx != std::string::npos) { + log::warn("{}", line); + errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); + } else { + std::smatch match; + if (std::regex_match(line, match, exRequires)) { + std::string modName(match[1].first, match[1].second); + std::string dependency(match[2].first, match[2].second); + core.pluginList()->addInformation(modName.c_str(), QObject::tr("depends on missing \"%1\"").arg(dependency.c_str())); + } else if (std::regex_match(line, match, exIncompatible)) { + std::string modName(match[1].first, match[1].second); + std::string dependency(match[2].first, match[2].second); + core.pluginList()->addInformation(modName.c_str(), QObject::tr("incompatible with \"%1\"").arg(dependency.c_str())); + } else { + log::debug("[loot] {}", line); + } + } + } + } +} + +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) +{ + std::string errorMessages; + + //m_OrganizerCore.currentProfile()->writeModlistNow(); + core.savePluginList(); + + //Create a backup of the load orders w/ LOOT in name + //to make sure that any sorting is easily undo-able. + //Need to figure out how I want to do that. + + bool success = false; + + try { + QProgressDialog dialog(parent); + + dialog.setLabelText(QObject::tr("Please wait while LOOT is running")); + dialog.setMaximum(0); + dialog.show(); + + QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); + + QStringList parameters; + parameters + << "--game" << core.managedGame()->gameShortName() + << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) + << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) + << "--out" << QString("\"%1\"").arg(outPath); + + if (didUpdateMasterList) { + parameters << "--skipUpdateMasterlist"; + } + + HANDLE stdOutWrite = INVALID_HANDLE_VALUE; + HANDLE stdOutRead = INVALID_HANDLE_VALUE; + createStdoutPipe(&stdOutRead, &stdOutWrite); + + try { + core.prepareVFS(); + } catch (const UsvfsConnectorException &e) { + log::debug("{}", e.what()); + return false; + } catch (const std::exception &e) { + QMessageBox::warning(parent, QObject::tr("Error"), e.what()); + return false; + } + + spawn::SpawnParameters sp; + sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); + sp.arguments = parameters.join(" "); + sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); + sp.hooked = true; + sp.stdOut = stdOutWrite; + + HANDLE loot = spawn::startBinary(parent, sp); + + // we don't use the write end + ::CloseHandle(stdOutWrite); + + core.pluginList()->clearAdditionalInformation(); + + DWORD retLen; + JOBOBJECT_BASIC_PROCESS_ID_LIST info; + HANDLE processHandle = loot; + + if (loot != INVALID_HANDLE_VALUE) { + bool isJobHandle = true; + ULONG lastProcessID; + DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { + if (isJobHandle) { + if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { + if (info.NumberOfProcessIdsInList == 0) { + log::debug("no more processes in job"); + break; + } else { + if (lastProcessID != info.ProcessIdList[0]) { + lastProcessID = info.ProcessIdList[0]; + if (processHandle != loot) { + ::CloseHandle(processHandle); + } + processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); + } + } + } else { + // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there + // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. + // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without + // the right to break out. + if (::GetLastError() != ERROR_MORE_DATA) { + isJobHandle = false; + } + } + } + + if (dialog.wasCanceled()) { + if (isJobHandle) { + ::TerminateJobObject(loot, 1); + } else { + ::TerminateProcess(loot, 1); + } + } + + // keep processing events so the app doesn't appear dead + QCoreApplication::processEvents(); + std::string lootOut = readFromPipe(stdOutRead); + processLOOTOut(core, lootOut, errorMessages, dialog); + + res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + } + + std::string remainder = readFromPipe(stdOutRead).c_str(); + if (remainder.length() > 0) { + processLOOTOut(core, remainder, errorMessages, dialog); + } + + DWORD exitCode = 0UL; + ::GetExitCodeProcess(processHandle, &exitCode); + ::CloseHandle(processHandle); + if (exitCode != 0UL) { + reportError(QObject::tr("loot failed. Exit code was: %1").arg(exitCode)); + return false; + } else { + success = true; + QFile outFile(outPath); + outFile.open(QIODevice::ReadOnly); + QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); + QJsonArray array = doc.array(); + for (auto iter = array.begin(); iter != array.end(); ++iter) { + QJsonObject pluginObj = (*iter).toObject(); + QJsonArray pluginMessages = pluginObj["messages"].toArray(); + for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { + QJsonObject msg = (*msgIter).toObject(); + core.pluginList()->addInformation(pluginObj["name"].toString(), + QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); + } + if (pluginObj["dirty"].toString() == "yes") + core.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); + } + + } + } else { + reportError(QObject::tr("failed to start loot")); + } + } catch (const std::exception &e) { + reportError(QObject::tr("failed to run loot: %1").arg(e.what())); + } + + if (errorMessages.length() > 0) { + QMessageBox *warn = new QMessageBox( + QMessageBox::Warning, QObject::tr("Errors occurred"), + errorMessages.c_str(), QMessageBox::Ok, parent); + + warn->setModal(false); + warn->show(); + } + + return success; +} diff --git a/src/loot.h b/src/loot.h new file mode 100644 index 00000000..2314e5b3 --- /dev/null +++ b/src/loot.h @@ -0,0 +1,10 @@ +#ifndef MODORGANIZER_LOOT_H +#define MODORGANIZER_LOOT_H + +#include + +class OrganizerCore; + +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + +#endif // MODORGANIZER_LOOT_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9453f07c..11edfc81 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -39,7 +39,7 @@ along with Mod Organizer. If not, see . #include "spawn.h" #include "versioninfo.h" #include "instancemanager.h" - +#include "loot.h" #include "report.h" #include "modlist.h" #include "modlistsortproxy.h" @@ -6369,233 +6369,14 @@ void MainWindow::on_showHiddenBox_toggled(bool checked) m_OrganizerCore.downloadManager()->setShowHidden(checked); } - -void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) -{ - SECURITY_ATTRIBUTES secAttributes; - secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - secAttributes.bInheritHandle = TRUE; - secAttributes.lpSecurityDescriptor = nullptr; - - if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { - log::error("failed to create stdout reroute"); - } - - if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { - log::error("failed to correctly set up the stdout reroute"); - *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; - } -} - -std::string MainWindow::readFromPipe(HANDLE stdOutRead) -{ - static const int chunkSize = 128; - std::string result; - - char buffer[chunkSize + 1]; - buffer[chunkSize] = '\0'; - - DWORD read = 1; - while (read > 0) { - if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { - break; - } - if (read > 0) { - result.append(buffer, read); - if (read < chunkSize) { - break; - } - } - } - return result; -} - -void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog) -{ - std::vector lines; - boost::split(lines, lootOut, boost::is_any_of("\r\n")); - - std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); - std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); - - for (const std::string &line : lines) { - if (line.length() > 0) { - size_t progidx = line.find("[progress]"); - size_t erroridx = line.find("[error]"); - if (progidx != std::string::npos) { - dialog.setLabelText(line.substr(progidx + 11).c_str()); - } else if (erroridx != std::string::npos) { - log::warn("{}", line); - errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); - } else { - std::smatch match; - if (std::regex_match(line, match, exRequires)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str())); - } else if (std::regex_match(line, match, exIncompatible)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str())); - } else { - log::debug("[loot] {}", line); - } - } - } - } -} - void MainWindow::on_bossButton_clicked() { - std::string errorMessages; - - //m_OrganizerCore.currentProfile()->writeModlistNow(); m_OrganizerCore.savePluginList(); - //Create a backup of the load orders w/ LOOT in name - //to make sure that any sorting is easily undo-able. - //Need to figure out how I want to do that. - bool success = false; - - try { - setEnabled(false); - ON_BLOCK_EXIT([&] () { setEnabled(true); }); - QProgressDialog dialog(this); - dialog.setLabelText(tr("Please wait while LOOT is running")); - dialog.setMaximum(0); - dialog.show(); - - QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); - - QStringList parameters; - parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName() - << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()) - << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath()) - << "--out" << QString("\"%1\"").arg(outPath); - - if (m_DidUpdateMasterList) { - parameters << "--skipUpdateMasterlist"; - } - HANDLE stdOutWrite = INVALID_HANDLE_VALUE; - HANDLE stdOutRead = INVALID_HANDLE_VALUE; - createStdoutPipe(&stdOutRead, &stdOutWrite); - try { - m_OrganizerCore.prepareVFS(); - } catch (const UsvfsConnectorException &e) { - log::debug("{}", e.what()); - return; - } catch (const std::exception &e) { - QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what()); - return; - } - - spawn::SpawnParameters sp; - sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); - sp.arguments = parameters.join(" "); - sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); - sp.hooked = true; - sp.stdOut = stdOutWrite; - - HANDLE loot = spawn::startBinary(this, sp); - - // we don't use the write end - ::CloseHandle(stdOutWrite); - - m_OrganizerCore.pluginList()->clearAdditionalInformation(); - - DWORD retLen; - JOBOBJECT_BASIC_PROCESS_ID_LIST info; - HANDLE processHandle = loot; - - if (loot != INVALID_HANDLE_VALUE) { - bool isJobHandle = true; - ULONG lastProcessID; - DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); - while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { - if (isJobHandle) { - if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { - if (info.NumberOfProcessIdsInList == 0) { - log::debug("no more processes in job"); - break; - } else { - if (lastProcessID != info.ProcessIdList[0]) { - lastProcessID = info.ProcessIdList[0]; - if (processHandle != loot) { - ::CloseHandle(processHandle); - } - processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); - } - } - } else { - // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there - // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. - // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without - // the right to break out. - if (::GetLastError() != ERROR_MORE_DATA) { - isJobHandle = false; - } - } - } - - if (dialog.wasCanceled()) { - if (isJobHandle) { - ::TerminateJobObject(loot, 1); - } else { - ::TerminateProcess(loot, 1); - } - } - - // keep processing events so the app doesn't appear dead - QCoreApplication::processEvents(); - std::string lootOut = readFromPipe(stdOutRead); - processLOOTOut(lootOut, errorMessages, dialog); - - res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); - } - - std::string remainder = readFromPipe(stdOutRead).c_str(); - if (remainder.length() > 0) { - processLOOTOut(remainder, errorMessages, dialog); - } - DWORD exitCode = 0UL; - ::GetExitCodeProcess(processHandle, &exitCode); - ::CloseHandle(processHandle); - if (exitCode != 0UL) { - reportError(tr("loot failed. Exit code was: %1").arg(exitCode)); - return; - } else { - success = true; - QFile outFile(outPath); - outFile.open(QIODevice::ReadOnly); - QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); - QJsonArray array = doc.array(); - for (auto iter = array.begin(); iter != array.end(); ++iter) { - QJsonObject pluginObj = (*iter).toObject(); - QJsonArray pluginMessages = pluginObj["messages"].toArray(); - for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { - QJsonObject msg = (*msgIter).toObject(); - m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), - QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); - } - if (pluginObj["dirty"].toString() == "yes") - m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); - } - - } - } else { - reportError(tr("failed to start loot")); - } - } catch (const std::exception &e) { - reportError(tr("failed to run loot: %1").arg(e.what())); - } - - if (errorMessages.length() > 0) { - QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occurred"), errorMessages.c_str(), QMessageBox::Ok, this); - warn->setModal(false); - warn->show(); - } + setEnabled(false); + ON_BLOCK_EXIT([&] () { setEnabled(true); }); - if (success) { + if (runLoot(this, m_OrganizerCore, m_DidUpdateMasterList)) { m_DidUpdateMasterList = true; m_OrganizerCore.refreshESPList(false); m_OrganizerCore.savePluginList(); diff --git a/src/mainwindow.h b/src/mainwindow.h index c80287b2..7c2ee1eb 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -136,10 +136,6 @@ public: void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info); - void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite); - std::string readFromPipe(HANDLE stdOutRead); - void processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog); - void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo); QString getOriginDisplayName(int originID); -- cgit v1.3.1 From 2b5747c19d942974295be18042fbdde8ddc4cc78 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 18 Nov 2019 17:04:01 -0500 Subject: handles changes in lootcli for a more formal communication protocol --- src/CMakeLists.txt | 2 + src/loot.cpp | 129 +++++++++++++++++++++++++++++++++++++---------------- src/loot.h | 12 +++-- 3 files changed, 101 insertions(+), 42 deletions(-) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 70c501ae..824ffa33 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -569,11 +569,13 @@ INCLUDE_DIRECTORIES(${project_path}/uibase/src ${project_path}/bsatk/src ${project_path}/esptk/src ${project_path}/archive/src + ${project_path}/lootcli/include ${dependency_project_path}/usvfs/include ${project_path}/game_gamebryo/src/gamebryo ${project_path}/game_gamebryo/src/creation ${project_path}/game_features/src ${project_path}/githubpp/src + ${SPDLOG_ROOT}/include ${LZ4_ROOT}/lib) INCLUDE_DIRECTORIES(shared ${ZLIB_INCLUDE_DIRS}) diff --git a/src/loot.cpp b/src/loot.cpp index 1fc0e438..7184cce8 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -22,19 +22,15 @@ public: QObject::connect( &m_loot, &Loot::progress, - this, [&](auto&& s){ setText(s); }, Qt::QueuedConnection); + this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); QObject::connect( - &m_loot, &Loot::information, this, - [&](auto&& mod, auto&& i){ setInfo(mod, i); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::errorMessage, this, - [&](auto&& s){ onErrorMessage(s); }, Qt::QueuedConnection); + &m_loot, &Loot::log, this, + [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); QObject::connect( - &m_loot, &Loot::error, this, - [&](auto&& s){ onError(s); }, Qt::QueuedConnection); + &m_loot, &Loot::information, this, + [&](auto&& mod, auto&& i){ setInfo(mod, i); }, Qt::QueuedConnection); QObject::connect( &m_loot, &Loot::finished, this, @@ -46,6 +42,29 @@ public: m_label->setText(s); } + void setProgress(lootcli::Progress p) + { + setText(progressToString(p)); + } + + QString progressToString(lootcli::Progress p) + { + using P = lootcli::Progress; + + switch (p) + { + case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); + case P::UpdatingMasterlist: return tr("Updating masterlist"); + case P::LoadingLists: return tr("Loading lists"); + case P::ReadingPlugins: return tr("Reading plugins"); + case P::SortingPlugins: return tr("Sorting plugins"); + case P::WritingLoadorder: return tr("Writing loadorder.txt"); + case P::ParsingLootMessages: return tr("Parsing loot messages"); + case P::Done: return tr("Done"); + default: return QString("unknown progress %1").arg(static_cast(p)); + } + } + void setIndeterminate() { m_progress->setMaximum(0); @@ -153,10 +172,6 @@ private: void addLineOutput(const QString& line) { - if (line == m_lastLine) { - return; - } - m_output->appendPlainText(line); m_lastLine = line; } @@ -164,12 +179,19 @@ private: void onFinished() { m_finished = true; - close(); } - void onErrorMessage(const QString& s) + void log(log::Levels lv, const QString& s) { - m_errorMessages += s; + if (lv == log::Levels::Error) { + MOBase::log::error("{}", s); + + if (!m_errorMessages.isEmpty()) { + m_errorMessages += "\n"; + } + + m_errorMessages += s; + } } }; @@ -235,7 +257,7 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) HANDLE lootHandle = spawn::startBinary(parent, sp); if (lootHandle == INVALID_HANDLE_VALUE) { - emit error(tr("failed to start loot")); + emit log(log::Levels::Error, tr("failed to start loot")); return false; } @@ -275,7 +297,7 @@ void Loot::lootThread() m_result = true; processOutputFile(); } catch (const std::exception &e) { - emit error(tr("failed to run loot: %1").arg(e.what())); + emit log(log::Levels::Error, tr("failed to run loot: %1").arg(e.what())); } } @@ -316,7 +338,7 @@ bool Loot::waitForCompletion() } if (exitCode != 0UL) { - emit error(tr("Loot failed. Exit code was: %1").arg(exitCode)); + emit log(log::Levels::Error, tr("Loot failed. Exit code was: %1").arg(exitCode)); return false; } @@ -350,40 +372,69 @@ void Loot::processStdout(const std::string &lootOut) { emit output(QString::fromStdString(lootOut)); - std::vector lines; - boost::split(lines, lootOut, boost::is_any_of("\r\n")); - - std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); - std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); - - for (const std::string &line : lines) { - if (line.length() > 0) { - size_t progidx = line.find("[progress]"); - size_t erroridx = line.find("[error]"); - if (progidx != std::string::npos) { - emit progress(line.substr(progidx + 11).c_str()); - } else if (erroridx != std::string::npos) { - log::warn("{}", line); - emit errorMessage(QString::fromStdString( - boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n")); - } else { + m_outputBuffer += lootOut; + std::size_t start = 0; + + for (;;) { + const auto newline = m_outputBuffer.find("\n", start); + if (newline == std::string::npos) { + break; + } + + const std::string_view line(m_outputBuffer.c_str() + start, newline - start); + const auto m = lootcli::parseMessage(line); + + if (m.type == lootcli::MessageType::None) { + log::error("unrecognised loot output: '{}'", line); + continue; + } + + processMessage(m); + + start = newline + 1; + } + + m_outputBuffer.erase(0, start); +} + +void Loot::processMessage(const lootcli::Message& m) +{ + static const std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); + static const std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); + + switch (m.type) + { + case lootcli::MessageType::Log: + { + if (m.logLevel == spdlog::level::err) { std::smatch match; - if (std::regex_match(line, match, exRequires)) { + + if (std::regex_match(m.log, match, exRequires)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); emit information( QString::fromStdString(modName), tr("depends on missing \"%1\"").arg(dependency.c_str())); - } else if (std::regex_match(line, match, exIncompatible)) { + } else if (std::regex_match(m.log, match, exIncompatible)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); emit information( QString::fromStdString(modName), tr("incompatible with \"%1\"").arg(dependency.c_str())); } else { - log::debug("[loot] {}", line); + emit log(log::levelFromSpdlog(m.logLevel), QString::fromStdString(m.log)); } + } else { + emit log(log::levelFromSpdlog(m.logLevel), QString::fromStdString(m.log)); } + + break; + } + + case lootcli::MessageType::Progress: + { + emit progress(m.progress); + break; } } } diff --git a/src/loot.h b/src/loot.h index 11bb4987..ab959f2e 100644 --- a/src/loot.h +++ b/src/loot.h @@ -3,9 +3,13 @@ #include "envmodule.h" #include +#include #include #include +Q_DECLARE_METATYPE(lootcli::Progress); +Q_DECLARE_METATYPE(MOBase::log::Levels); + class OrganizerCore; class Loot : public QObject @@ -22,10 +26,9 @@ public: signals: void output(const QString& s); - void progress(const QString& s); + void progress(const lootcli::Progress p); + void log(MOBase::log::Levels level, const QString& s); void information(const QString& mod, const QString& info); - void errorMessage(const QString& s); - void error(const QString& s); void finished(); private: @@ -35,12 +38,15 @@ private: QString m_outPath; env::HandlePtr m_lootProcess; env::HandlePtr m_stdout; + std::string m_outputBuffer; std::string readFromPipe(); void lootThread(); bool waitForCompletion(); + void processStdout(const std::string &lootOut); + void processMessage(const lootcli::Message& m); void processOutputFile(); bool processOutputPlugin(const QJsonValue& pluginValue); -- cgit v1.3.1 From 981c1b773966a30374a90bc21d6ebd9c7bf86040 Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 19 Nov 2019 14:42:50 -0600 Subject: Move all Qt deployment to main MO project --- src/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d935981d..f168ef36 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -653,6 +653,10 @@ INSTALL( ${qt5bin}/windeployqt.exe --verbose 0 uibase.dll ${windeploy_parameters} WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin ) + EXECUTE_PROCESS(COMMAND + ${qt5bin}/windeployqt.exe --verbose 0 plugins/bsa_packer.dll ${windeploy_parameters} + WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin + ) file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/platforms) file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/styles) file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/dlls/imageformats) -- cgit v1.3.1 From 5c9de17b6376ce94b1cdff4f4edbba798e9bfd08 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 23 Nov 2019 22:08:53 -0500 Subject: rewrite of json report parsing added json.h with some utilities --- src/CMakeLists.txt | 2 + src/json.h | 190 ++++++++++++++++++++++++++++++ src/loot.cpp | 332 ++++++++++++++++++++++++++++++++--------------------- src/loot.h | 32 +++--- 4 files changed, 412 insertions(+), 144 deletions(-) create mode 100644 src/json.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 824ffa33..3b74ea37 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -265,6 +265,7 @@ SET(organizer_HDRS processrunner.h uilocker.h loot.h + json.h shared/windows_error.h shared/error_report.h @@ -468,6 +469,7 @@ set(utilities usvfsconnector shared/windows_error loot + json ) set(widgets diff --git a/src/json.h b/src/json.h new file mode 100644 index 00000000..d182f330 --- /dev/null +++ b/src/json.h @@ -0,0 +1,190 @@ +#ifndef MODORGANIZER_JSON_INCLUDED +#define MODORGANIZER_JSON_INCLUDED + +#include +#include +#include +#include +#include + +namespace json +{ + +class failed {}; + + +namespace details +{ + +QString typeName(const QJsonValue& v) +{ + if (v.isUndefined()) { + return "undefined"; + } else if (v.isNull()) { + return "null"; + } else if (v.isArray()) { + return "an array"; + } else if (v.isBool()) { + return "a bool"; + } else if (v.isDouble()) { + return "a double"; + } else if (v.isObject()) { + return "an object"; + } else if (v.isString()) { + return "a string"; + } else { + return "an unknown type"; + } +} + +QString typeName(const QJsonDocument& doc) +{ + if (doc.isEmpty()) { + return "empty"; + } else if (doc.isNull()) { + return "null"; + } else if (doc.isArray()) { + return "an array"; + } else if (doc.isObject()) { + return "an object"; + } else { + return "an unknown type"; + } +} + + +template +T convert(const QJsonValue& v) = delete; + +template <> +bool convert(const QJsonValue& v) +{ + if (!v.isBool()) { + throw failed(); + } + + return v.toBool(); +} + +template <> +QJsonObject convert(const QJsonValue& v) +{ + if (!v.isObject()) { + throw failed(); + } + + return v.toObject(); +} + +template <> +QString convert(const QJsonValue& v) +{ + if (!v.isString()) { + throw failed(); + } + + return v.toString(); +} + +template <> +QJsonArray convert(const QJsonValue& v) +{ + if (!v.isArray()) { + throw failed(); + } + + return v.toArray(); +} + +template <> +qint64 convert(const QJsonValue& v) +{ + if (!v.isDouble()) { + throw failed(); + } + + return static_cast(v.toDouble()); +} + +} // namespace + + +template +T convert(const QJsonValue& value, const char* what) +{ + try + { + return details::convert(value); + } + catch(failed&) + { + MOBase::log::error( + "'{}' is a {}, not a {}", + what, details::typeName(value), typeid(T).name); + + throw; + } +} + +template +T convertWarn(const QJsonValue& value, const char* what, T def={}) +{ + try + { + return details::convert(value); + } + catch(failed&) + { + MOBase::log::warn( + "'{}' is a {}, not a {}", + what, details::typeName(value), typeid(T).name()); + + return def; + } +} + +template +T get(const QJsonObject& o, const char* e) +{ + if (!o.contains(e)) { + MOBase::log::error("property '{}' is missing", e); + throw failed(); + } + + return convert(o[e], e); +} + +template +T getWarn(const QJsonObject& o, const char* e, T def={}) +{ + if (!o.contains(e)) { + MOBase::log::warn("property '{}' is missing", e); + return def; + } + + return convertWarn(o[e], e); +} + +template +T getOpt(const QJsonObject& o, const char* e, T def={}) +{ + if (!o.contains(e)) { + return def; + } + + return convertWarn(o[e], e); +} + + +template +void requireObject(const Value& v, const char* what) +{ + if (!v.isObject()) { + MOBase::log::error("{} is {}, not an object", what, details::typeName(v)); + throw failed(); + } +} + +} // namespace + +#endif // MODORGANIZER_JSON_INCLUDED diff --git a/src/loot.cpp b/src/loot.cpp index eef16b45..88ea8ce8 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -1,10 +1,12 @@ #include "loot.h" #include "spawn.h" #include "organizercore.h" +#include "json.h" #include #include using namespace MOBase; +using namespace json; log::Levels levelFromLoot(lootcli::LogLevels level) { @@ -193,7 +195,6 @@ private: ly->addLayout(buttons); m_output = new QPlainTextEdit; - m_output->setWordWrapMode(QTextOption::NoWrap); ly->addWidget(m_output); m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); @@ -254,6 +255,81 @@ private: }; +struct Loot::Message +{ + QString type; + QString text; +}; + +struct Loot::File +{ + QString name; + QString displayName; +}; + +struct Loot::Dirty +{ + qint64 crc=0; + qint64 itm=0; + qint64 deletedReferences=0; + qint64 deletedNavmesh=0; + QString cleaningUtility; + QString info; + + QString toString(bool isClean) const + { + if (isClean) { + return QObject::tr("Verified clean by %1") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility); + } + + QString s = cleaningString(); + + if (!info.isEmpty()) { + s += " " + info; + } + + return s; + } + + QString cleaningString() const + { + return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility) + .arg(itm) + .arg(deletedReferences) + .arg(deletedNavmesh); + } +}; + +struct Loot::Plugin +{ + QString name; + std::vector incompatibilities; + std::vector messages; + std::vector dirty, clean; + std::vector missingMasters; + bool loadsArchive = false; + bool isMaster = false; + bool isLightMaster = false; +}; + +struct Loot::Stats +{ + qint64 time = 0; + QString version; +}; + +struct Loot::Report +{ + std::vector messages; + std::vector plugins; + Stats stats; +}; + + +class ReportFailed {}; + Loot::Loot() : m_thread(nullptr), m_cancel(false), m_result(false) { @@ -522,49 +598,16 @@ void Loot::processMessage(const lootcli::Message& m) } } -QString jsonType(const QJsonValue& v) -{ - if (v.isUndefined()) { - return "undefined"; - } else if (v.isNull()) { - return "null"; - } else if (v.isArray()) { - return "an array"; - } else if (v.isBool()) { - return "a bool"; - } else if (v.isDouble()) { - return "a double"; - } else if (v.isObject()) { - return "an object"; - } else if (v.isString()) { - return "a string"; - } else { - return "an unknown type"; - } -} - -QString jsonType(const QJsonDocument& doc) -{ - if (doc.isEmpty()) { - return "empty"; - } else if (doc.isNull()) { - return "null"; - } else if (doc.isArray()) { - return "an array"; - } else if (doc.isObject()) { - return "an object"; - } else { - return "an unknown type"; - } -} - void Loot::processOutputFile() { + log::info("parsing json output file at '{}'", m_outPath); + QFile outFile(m_outPath); if (!outFile.open(QIODevice::ReadOnly)) { - logJsonError( - "failed to open file, {} (error {})", - outFile.errorString(), outFile.error()); + emit log( + MOBase::log::Error, + QString("failed to open file, %1 (error %2)") + .arg(outFile.errorString()).arg(outFile.error())); return; } @@ -572,149 +615,178 @@ void Loot::processOutputFile() QJsonParseError e; const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e); if (doc.isNull()) { - logJsonError("invalid json, {} (error {})", e.errorString(), e.error); - return; - } + emit log( + MOBase::log::Error, + QString("invalid json, %1 (error %2)") + .arg(e.errorString()).arg(e.error)); - if (!doc.isObject()) { - logJsonError("root is {}, not an object", jsonType(doc)); return; } - const QJsonObject object = doc.object(); + const auto report = createReport(doc); - if (object.contains("messages")) { - const auto messagesValue = object["messages"]; + for (auto&& m : report.messages) { + emit log(levelFromLoot( + lootcli::logLevelFromString(m.type.toStdString())), + m.text); + } - if (messagesValue.isArray()) { - processMessages(messagesValue.toArray()); - } else { - logJsonError( - "'messages' property is {}, not an array", jsonType(messagesValue)); + for (auto&& p : report.plugins) { + for (auto&& d : p.dirty) { + emit information(p.name, d.toString(false)); } - } +} - if (object.contains("plugins")) { - const auto pluginsValue = object["plugins"]; +Loot::Report Loot::createReport(const QJsonDocument& doc) const +{ + requireObject(doc, "root"); - if (pluginsValue.isArray()) { - processPlugins(pluginsValue.toArray()); - } else { - logJsonError( - "'plugins' property is {}, not an array", jsonType(pluginsValue)); - } - } + Report r; + const QJsonObject object = doc.object(); + + r.messages = reportMessages(getOpt(object, "messages")); + r.plugins = reportPlugins(getOpt(object, "plugins")); + + return r; } -bool Loot::processMessages(const QJsonArray& messages) +std::vector Loot::reportPlugins(const QJsonArray& plugins) const { - for (auto messageValue : messages) { - if (messageValue.isObject()) { - processMessage(messageValue.toObject()); - } else { - logJsonError("a message is {}, not an object", jsonType(messageValue)); + std::vector v; + + for (auto pluginValue : plugins) { + const auto o = convertWarn(pluginValue, "plugin"); + if (o.isEmpty()) { + continue; + } + + auto p = reportPlugin(o); + if (!p.name.isEmpty()) { + v.emplace_back(std::move(p)); } } - return true; + return v; } -bool Loot::processMessage(const QJsonObject& message) +Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const { - const auto messageType = message["type"].toString(); - const auto messageString = message["message"].toString(); + Plugin p; - if (messageType.isEmpty()) { - logJsonError("there's a message with no 'type' property"); - return false; + p.name = getWarn(plugin, "name"); + if (p.name.isEmpty()) { + return {}; } - if (messageString.isEmpty()) { - logJsonError("there's a message with no 'message' property"); - return false; + if (plugin.contains("incompatibilities")) { + p.incompatibilities = reportFiles(getOpt(plugin, "incompatibilities")); } - emit log(levelFromLoot( - lootcli::logLevelFromString(messageType.toStdString())), - messageString); + if (plugin.contains("messages")) { + p.messages = reportMessages(getOpt(plugin, "messages")); + } - return true; -} + if (plugin.contains("dirty")) { + p.dirty = reportDirty(getOpt(plugin, "dirty")); + } -bool Loot::processPlugins(const QJsonArray& plugins) -{ - for (auto pluginValue : plugins) { - if (pluginValue.isObject()) { - processPlugin(pluginValue.toObject()); - } else { - logJsonError("a plugin is {}, not an object", jsonType(pluginValue)); - } + if (plugin.contains("clean")) { + p.clean = reportDirty(getOpt(plugin, "clean")); } - return true; + if (plugin.contains("missingMasters")) { + p.missingMasters = reportStringArray(getOpt(plugin, "missingMasters")); + } + + p.loadsArchive = getOpt(plugin, "loadsArchive", false); + p.isMaster = getOpt(plugin, "isMaster", false); + p.isLightMaster = getOpt(plugin, "isLightMaster", false); + + return p; } -bool Loot::processPlugin(const QJsonObject& plugin) +std::vector Loot::reportMessages(const QJsonArray& array) const { - if (!plugin.contains("name")) { - logJsonError("plugin missing 'name' property"); - return false; - } + std::vector v; - const auto nameValue = plugin["name"]; - if (!nameValue.isString()) { - logJsonError("plugin property 'name' is {}, not a string", jsonType(nameValue)); - return false; - } + for (auto messageValue : array) { + const auto o = convertWarn(messageValue, "message"); + if (o.isEmpty()) { + continue; + } - const auto name = nameValue.toString(); + Message m; + m.type = getWarn(o, "type"); + m.text = getWarn(o, "text"); - processPluginDirty(name, plugin); + if (!m.text.isEmpty()) { + v.emplace_back(std::move(m)); + } + } - return true; + return v; } - -bool Loot::processPluginDirty(const QString& name, const QJsonObject& plugin) +std::vector Loot::reportFiles(const QJsonArray& array) const { - if (!plugin.contains("dirty")) { - return true; - } + std::vector v; + + for (auto&& fileValue : array) { + const auto o = convertWarn(fileValue, "file"); + if (o.isEmpty()) { + continue; + } - const auto dirtyValue = plugin["dirty"]; + File f; - if (!dirtyValue.isArray()) { - logJsonError( - "'dirty' value for plugin '{}' is {}, not an array", - name, jsonType(dirtyValue)); + f.name = getWarn(o, "name"); + f.displayName = getOpt(o, "displayName"); - return false; + if (!f.name.isEmpty()) { + v.emplace_back(std::move(f)); + } } - const auto dirty = dirtyValue.toArray(); + return v; +} + +std::vector Loot::reportDirty(const QJsonArray& array) const +{ + std::vector v; + for (auto&& dirtyValue : array) { + const auto o = convertWarn(dirtyValue, "dirty"); - for (auto stringValue : dirty) { - if (!stringValue.isString()) { - logJsonError( - "'dirty' value for plugin '{}' is {}, not a string", - name, jsonType(stringValue)); + Dirty d; - continue; - } + d.crc = getWarn(o, "crc"); + d.itm = getOpt(o, "itm"); + d.deletedReferences = getOpt(o, "deletedReferences"); + d.deletedNavmesh = getOpt(o, "deletedNavmesh"); + d.cleaningUtility = getOpt(o, "cleaningUtility"); + d.info = getOpt(o, "info"); - const auto string = stringValue.toString(); + v.emplace_back(std::move(d)); + } + + return v; +} - if (string.isEmpty()) { - logJsonError("'dirty' string for plugin '{}' is empty", name); +std::vector Loot::reportStringArray(const QJsonArray& array) const +{ + std::vector v; + + for (auto&& sv : array) { + auto s = convertWarn(sv, "string"); + if (s.isEmpty()) { continue; } - emit information(name, string); + v.emplace_back(std::move(s)); } - return true; + return v; } diff --git a/src/loot.h b/src/loot.h index 54fc6fd1..95dbbe50 100644 --- a/src/loot.h +++ b/src/loot.h @@ -33,6 +33,14 @@ signals: void finished(); private: + struct Report; + struct Stats; + struct Message; + struct Plugin; + struct Dirty; + struct File; + class BadReport {}; + std::unique_ptr m_thread; std::atomic m_cancel; std::atomic m_result; @@ -50,20 +58,16 @@ private: void processMessage(const lootcli::Message& m); void processOutputFile(); - bool processMessages(const QJsonArray& messages); - bool processMessage(const QJsonObject& message); - bool processPlugins(const QJsonArray& plugins); - bool processPlugin(const QJsonObject& plugin); - - bool processPluginDirty(const QString& name, const QJsonObject& plugin); - - template - void logJsonError(Format&& f, Args&&... args) - { - MOBase::log::error( - std::string("loot output file '{}': ") + f, - m_outPath, std::forward(args)...); - }; + + Report createReport(const QJsonDocument& doc) const; + Message reportMessage(const QJsonObject& message) const; + std::vector reportPlugins(const QJsonArray& plugins) const; + Loot::Plugin reportPlugin(const QJsonObject& plugin) const; + + std::vector reportMessages(const QJsonArray& array) const; + std::vector reportFiles(const QJsonArray& array) const; + std::vector reportDirty(const QJsonArray& array) const; + std::vector reportStringArray(const QJsonArray& array) const; }; -- cgit v1.3.1 From cd876a0f9ffd03c711812c2ade92836e5d6c0203 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 20:11:50 -0500 Subject: split loot dialog, added ui file --- src/CMakeLists.txt | 10 +- src/loot.cpp | 271 +---------------------------------------------------- src/lootdialog.cpp | 222 +++++++++++++++++++++++++++++++++++++++++++ src/lootdialog.h | 47 ++++++++++ src/lootdialog.ui | 214 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 492 insertions(+), 272 deletions(-) create mode 100644 src/lootdialog.cpp create mode 100644 src/lootdialog.h create mode 100644 src/lootdialog.ui (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b74ea37..db3bda73 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -143,6 +143,7 @@ SET(organizer_SRCS processrunner.cpp uilocker.cpp loot.cpp + lootdialog.cpp shared/windows_error.cpp shared/error_report.cpp @@ -265,6 +266,7 @@ SET(organizer_HDRS processrunner.h uilocker.h loot.h + lootdialog.h json.h shared/windows_error.h @@ -392,6 +394,11 @@ set(executables editexecutablesdialog ) +set(loot + loot + lootdialog +) + set(modinfo modinfo modinfobackup @@ -468,7 +475,6 @@ set(utilities shared/util usvfsconnector shared/windows_error - loot json ) @@ -490,7 +496,7 @@ set(widgets ) set(src_filters - application core browser dialogs downloads env executables modinfo + application core browser dialogs downloads env executables loot modinfo modinfo\\dialog modlist plugins previews profiles settings settingsdialog utilities widgets ) diff --git a/src/loot.cpp b/src/loot.cpp index c73dcaa7..f0618b0b 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -1,4 +1,5 @@ #include "loot.h" +#include "lootdialog.h" #include "spawn.h" #include "organizercore.h" #include "json.h" @@ -32,276 +33,6 @@ log::Levels levelFromLoot(lootcli::LogLevels level) } } -class LootDialog : public QDialog -{ -public: - LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : - QDialog(parent), m_core(core), m_loot(loot), - m_label(nullptr), m_progress(nullptr), - m_report(nullptr), m_output(nullptr), m_buttons(nullptr), - m_finished(false), m_cancelling(false) - { - createUI(); - m_progress->setMaximum(0); - - QObject::connect( - &m_loot, &Loot::output, this, - [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::progress, - this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::log, this, - [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::finished, this, - [&]{ onFinished(); }, Qt::QueuedConnection); - } - - void setText(const QString& s) - { - m_label->setText(s); - } - - void setProgress(lootcli::Progress p) - { - setText(progressToString(p)); - - if (p == lootcli::Progress::Done) { - m_progress->setRange(0, 1); - m_progress->setValue(1); - } - } - - QString progressToString(lootcli::Progress p) - { - using P = lootcli::Progress; - - switch (p) - { - case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); - case P::UpdatingMasterlist: return tr("Updating masterlist"); - case P::LoadingLists: return tr("Loading lists"); - case P::ReadingPlugins: return tr("Reading plugins"); - case P::SortingPlugins: return tr("Sorting plugins"); - case P::WritingLoadorder: return tr("Writing loadorder.txt"); - case P::ParsingLootMessages: return tr("Parsing loot messages"); - case P::Done: return tr("Done"); - default: return QString("unknown progress %1").arg(static_cast(p)); - } - } - - void addOutput(const QString& s) - { - if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { - return; - } - - const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); - - for (auto&& line : lines) { - if (line.isEmpty()) { - continue; - } - - addLineOutput(line); - } - } - - bool result() const - { - return m_loot.result(); - } - - void cancel() - { - if (!m_finished && !m_cancelling) { - addLineOutput(tr("Stopping LOOT...")); - m_loot.cancel(); - m_buttons->setEnabled(false); - m_cancelling = true; - } - } - - int exec() override - { - return QDialog::exec(); - } - - void openReport() - { - const auto path = m_loot.outPath(); - shell::Open(path); - } - -private: - OrganizerCore& m_core; - Loot& m_loot; - QLabel* m_label; - QProgressBar* m_progress; - QTextEdit* m_messages; - QPushButton* m_report; - QPlainTextEdit* m_output; - QDialogButtonBox* m_buttons; - bool m_finished; - bool m_cancelling; - - void createUI() - { - auto* root = new QWidget(this); - auto* ly = new QVBoxLayout(root); - - setLayout(new QVBoxLayout); - layout()->setContentsMargins(0, 0, 0, 0); - layout()->addWidget(root); - - m_label = new QLabel; - ly->addWidget(m_label); - - m_progress = new QProgressBar; - ly->addWidget(m_progress); - - m_messages = new QTextEdit; - ly->addWidget(m_messages); - - auto* more = createMoreUI(); - ly->addWidget(more); - - resize(700, 400); - } - - QWidget* createMoreUI() - { - auto* more = new QWidget; - auto* ly = new QVBoxLayout(more); - ly->setContentsMargins(0, 0, 0, 0); - - auto* buttons = new QHBoxLayout; - buttons->setContentsMargins(0, 0, 0, 0); - m_report = new QPushButton(tr("Open JSON report")); - m_report->setEnabled(false); - connect(m_report, &QPushButton::clicked, [&]{ openReport(); }); - buttons->addWidget(m_report); - buttons->addStretch(1); - ly->addLayout(buttons); - - m_output = new QPlainTextEdit; - ly->addWidget(m_output); - - m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); - connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); - ly->addWidget(m_buttons); - - return more; - } - - void closeEvent(QCloseEvent* e) override - { - if (m_finished) { - QDialog::closeEvent(e); - } else { - cancel(); - e->ignore(); - } - } - - void onButton(QAbstractButton* b) - { - if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - if (m_finished) { - close(); - } else { - cancel(); - } - } - } - - void addLineOutput(const QString& line) - { - m_output->appendPlainText(line); - } - - void onFinished() - { - m_finished = true; - - if (m_cancelling) { - close(); - } else { - handleReport(); - m_report->setEnabled(true); - m_buttons->setStandardButtons(QDialogButtonBox::Close); - } - } - - void log(log::Levels lv, const QString& s) - { - if (lv >= log::Levels::Warning) { - log::log(lv, "{}", s); - } - - if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { - addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); - } - } - - void handleReport() - { - const auto& report = m_loot.report(); - - if (!report.messages.empty()) { - addLineOutput(""); - } - - QString html; - - if (!report.messages.empty()) { - html += "
    "; - - for (auto&& m : report.messages) { - log(m.type, m.text); - - html += "
  • "; - - switch (m.type) - { - case log::Error: - { - html += "" + QObject::tr("Error") + ": "; - break; - } - - case log::Warning: - { - html += "" + QObject::tr("Warning") + ": "; - break; - } - - default: - { - break; - } - } - - html += m.text + "
  • "; - } - - html + "
"; - } else { - html += QObject::tr("No messages."); - } - - m_messages->setHtml(html); - - for (auto&& p : report.plugins) { - m_core.pluginList()->addLootReport(p.name, p); - } - } -}; - QString Loot::Dirty::toString(bool isClean) const { diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp new file mode 100644 index 00000000..db41959d --- /dev/null +++ b/src/lootdialog.cpp @@ -0,0 +1,222 @@ +#include "lootdialog.h" +#include "ui_lootdialog.h" +#include "loot.h" +#include "organizercore.h" +#include +#include + +using namespace MOBase; + +LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : + QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot), + m_finished(false), m_cancelling(false) +{ + createUI(); + + QObject::connect( + &m_loot, &Loot::output, this, + [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::progress, + this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::log, this, + [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::finished, this, + [&]{ onFinished(); }, Qt::QueuedConnection); +} + +LootDialog::~LootDialog() = default; + +void LootDialog::setText(const QString& s) +{ + ui->progressText->setText(s); +} + +void LootDialog::setProgress(lootcli::Progress p) +{ + setText(progressToString(p)); + + if (p == lootcli::Progress::Done) { + ui->progressBar->setRange(0, 1); + ui->progressBar->setValue(1); + } +} + +QString LootDialog::progressToString(lootcli::Progress p) +{ + using P = lootcli::Progress; + + switch (p) + { + case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); + case P::UpdatingMasterlist: return tr("Updating masterlist"); + case P::LoadingLists: return tr("Loading lists"); + case P::ReadingPlugins: return tr("Reading plugins"); + case P::SortingPlugins: return tr("Sorting plugins"); + case P::WritingLoadorder: return tr("Writing loadorder.txt"); + case P::ParsingLootMessages: return tr("Parsing loot messages"); + case P::Done: return tr("Done"); + default: return QString("unknown progress %1").arg(static_cast(p)); + } +} + +void LootDialog::addOutput(const QString& s) +{ + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + return; + } + + const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); + + for (auto&& line : lines) { + if (line.isEmpty()) { + continue; + } + + addLineOutput(line); + } +} + +bool LootDialog::result() const +{ + return m_loot.result(); +} + +void LootDialog::cancel() +{ + if (!m_finished && !m_cancelling) { + addLineOutput(tr("Stopping LOOT...")); + m_loot.cancel(); + ui->buttons->setEnabled(false); + m_cancelling = true; + } +} + +void LootDialog::openReport() +{ + const auto path = m_loot.outPath(); + shell::Open(path); +} + +void LootDialog::createUI() +{ + ui->setupUi(this); + ui->progressBar->setMaximum(0); + + ui->openJsonReport->setEnabled(false); + connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); + + new ExpanderWidget(ui->details, ui->detailsPanel); + + connect(ui->buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); + + resize(480, 275); +} + +void LootDialog::closeEvent(QCloseEvent* e) +{ + if (m_finished) { + QDialog::closeEvent(e); + } else { + cancel(); + e->ignore(); + } +} + +void LootDialog::onButton(QAbstractButton* b) +{ + if (ui->buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { + if (m_finished) { + close(); + } else { + cancel(); + } + } +} + +void LootDialog::addLineOutput(const QString& line) +{ + ui->output->appendPlainText(line); +} + +void LootDialog::onFinished() +{ + m_finished = true; + + if (m_cancelling) { + close(); + } else { + handleReport(); + ui->openJsonReport->setEnabled(true); + ui->buttons->setStandardButtons(QDialogButtonBox::Close); + } +} + +void LootDialog::log(log::Levels lv, const QString& s) +{ + if (lv >= log::Levels::Warning) { + log::log(lv, "{}", s); + } + + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); + } +} + +void LootDialog::handleReport() +{ + const auto& report = m_loot.report(); + + if (!report.messages.empty()) { + addLineOutput(""); + } + + QString html; + + if (!report.messages.empty()) { + html += "
    "; + + for (auto&& m : report.messages) { + log(m.type, m.text); + + html += "
  • "; + + switch (m.type) + { + case log::Error: + { + html += "" + QObject::tr("Error") + ": "; + break; + } + + case log::Warning: + { + html += "" + QObject::tr("Warning") + ": "; + break; + } + + default: + { + break; + } + } + + html += m.text + "
  • "; + } + + html + "
"; + } else { + html += QObject::tr("No messages."); + } + + ui->report->setHtml(html); + + for (auto&& p : report.plugins) { + m_core.pluginList()->addLootReport(p.name, p); + } +} diff --git a/src/lootdialog.h b/src/lootdialog.h new file mode 100644 index 00000000..e4647b5c --- /dev/null +++ b/src/lootdialog.h @@ -0,0 +1,47 @@ +#ifndef MODORGANIZER_LOOTDIALOG_H +#define MODORGANIZER_LOOTDIALOG_H + +#include +#include + +namespace Ui { class LootDialog; } + +class OrganizerCore; +class Loot; + +class LootDialog : public QDialog +{ +public: + LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot); + ~LootDialog(); + + void setText(const QString& s); + void setProgress(lootcli::Progress p); + + QString progressToString(lootcli::Progress p); + + void addOutput(const QString& s); + + bool result() const; + + void cancel(); + + void openReport(); + +private: + std::unique_ptr ui; + OrganizerCore& m_core; + Loot& m_loot; + bool m_finished; + bool m_cancelling; + + void createUI(); + void closeEvent(QCloseEvent* e) override; + void onButton(QAbstractButton* b); + void addLineOutput(const QString& line); + void onFinished(); + void log(MOBase::log::Levels lv, const QString& s); + void handleReport(); +}; + +#endif // MODORGANIZER_LOOTDIALOG_H diff --git a/src/lootdialog.ui b/src/lootdialog.ui new file mode 100644 index 00000000..7e10b1db --- /dev/null +++ b/src/lootdialog.ui @@ -0,0 +1,214 @@ + + + LootDialog + + + + 0 + 0 + 457 + 343 + + + + LOOT + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Progress + + + + + + + 24 + + + false + + + + + + + true + + + Qt::TextBrowserInteraction + + + LOOT Report + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Details + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Open JSON report + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttons + accepted() + LootDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttons + rejected() + LootDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + -- cgit v1.3.1 From 64304a6368cf8357bb04d1c0057aec637ebb479a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 23:23:15 -0500 Subject: tweaked css, finished markdown report copy markdown.html to resources/ --- src/CMakeLists.txt | 4 +- src/loot.cpp | 139 ++++++++++++ src/loot.h | 13 +- src/lootdialog.cpp | 44 +--- src/resources/markdown.html | 528 ++++++++++++++++++++++---------------------- 5 files changed, 424 insertions(+), 304 deletions(-) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index db3bda73..5a510806 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -676,4 +676,6 @@ INSTALL( ) # qdds.dll needs installing manually as Qt no longer ships with it by default. -INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) \ No newline at end of file +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) + +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/markdown.html DESTINATION bin/resources) diff --git a/src/loot.cpp b/src/loot.cpp index 5d75eeaa..2faf9c2e 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -34,6 +34,99 @@ log::Levels levelFromLoot(lootcli::LogLevels level) } +QString Loot::Report::toMarkdown() const +{ + QString s; + + if (!messages.empty()) { + s += "### " + QObject::tr("General messages") + "\n"; + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + } + + if (!plugins.empty()) { + if (!s.isEmpty()) { + s += "\n"; + } + + s += "### " + QObject::tr("Plugins") + "\n"; + + for (auto&& p : plugins) { + const auto ps = p.toMarkdown(); + if (!ps.isEmpty()) { + s += ps + "\n"; + } + } + } + + if (s.isEmpty()) { + s += "**" + QObject::tr("No messages.") + "**"; + } + + s += stats.toMarkdown(); + + return s; +} + +QString Loot::Stats::toMarkdown() const +{ + return QString("`stats: %1s, lootcli %2, loot %3`") + .arg(QString::number(time / 1000.0, 'f', 2)) + .arg(lootcliVersion) + .arg(lootVersion); +} + +QString Loot::Plugin::toMarkdown() const +{ + QString s; + + if (!incompatibilities.empty()) { + s += " - **" + QObject::tr("Incompatibilities") + ": "; + + QString fs; + for (auto&& f : incompatibilities) { + if (!fs.isEmpty()) { + fs += ", "; + } + + fs += f.displayName.isEmpty() ? f.name : f.displayName; + } + + s += fs + "**\n"; + } + + if (!missingMasters.empty()) { + s += " - **" + QObject::tr("Missing masters") + ": "; + + QString ms; + for (auto&& m : missingMasters) { + if (!ms.isEmpty()) { + ms += ", "; + } + + ms += m; + } + + s += ms + "**\n"; + } + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + + for (auto&& d : dirty) { + s += " - " + d.toMarkdown(false) + "\n"; + } + + if (!s.isEmpty()) { + s = "#### " + name + "\n" + s; + } + + return s; +} + QString Loot::Dirty::toString(bool isClean) const { if (isClean) { @@ -50,6 +143,11 @@ QString Loot::Dirty::toString(bool isClean) const return s; } +QString Loot::Dirty::toMarkdown(bool isClean) const +{ + return toString(isClean); +} + QString Loot::Dirty::cleaningString() const { return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") @@ -59,6 +157,35 @@ QString Loot::Dirty::cleaningString() const .arg(deletedNavmesh); } +QString Loot::Message::toMarkdown() const +{ + QString s; + + switch (type) + { + case log::Error: + { + s += "**" + QObject::tr("Error") + "**: "; + break; + } + + case log::Warning: + { + s += "**" + QObject::tr("Warning") + "**: "; + break; + } + + default: + { + break; + } + } + + s += text; + + return s; +} + Loot::Loot() : m_thread(nullptr), m_cancel(false), m_result(false) @@ -348,6 +475,7 @@ Loot::Report Loot::createReport(const QJsonDocument& doc) const r.messages = reportMessages(getOpt(object, "messages")); r.plugins = reportPlugins(getOpt(object, "plugins")); + r.stats = reportStats(getWarn(object, "stats")); return r; } @@ -407,6 +535,17 @@ Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const return p; } +Loot::Stats Loot::reportStats(const QJsonObject& stats) const +{ + Stats s; + + s.time = getWarn(stats, "time"); + s.lootcliVersion = getWarn(stats, "lootcliVersion"); + s.lootVersion = getWarn(stats, "lootVersion"); + + return s; +} + std::vector Loot::reportMessages(const QJsonArray& array) const { std::vector v; diff --git a/src/loot.h b/src/loot.h index 3a7c6aa9..30ef4b60 100644 --- a/src/loot.h +++ b/src/loot.h @@ -21,6 +21,8 @@ public: { MOBase::log::Levels type; QString text; + + QString toMarkdown() const; }; struct File @@ -39,6 +41,7 @@ public: QString info; QString toString(bool isClean) const; + QString toMarkdown(bool isClean) const; QString cleaningString() const; }; @@ -52,12 +55,17 @@ public: bool loadsArchive = false; bool isMaster = false; bool isLightMaster = false; + + QString toMarkdown() const; }; struct Stats { qint64 time = 0; - QString version; + QString lootcliVersion; + QString lootVersion; + + QString toMarkdown() const; }; struct Report @@ -65,6 +73,8 @@ public: std::vector messages; std::vector plugins; Stats stats; + + QString toMarkdown() const; }; @@ -107,6 +117,7 @@ private: Message reportMessage(const QJsonObject& message) const; std::vector reportPlugins(const QJsonArray& plugins) const; Loot::Plugin reportPlugin(const QJsonObject& plugin) const; + Loot::Stats reportStats(const QJsonObject& stats) const; std::vector reportMessages(const QJsonArray& array) const; std::vector reportFiles(const QJsonArray& array) const; diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 9c7b482e..c7cdfecd 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -226,49 +226,19 @@ void LootDialog::log(log::Levels lv, const QString& s) void LootDialog::handleReport() { - const auto& report = m_loot.report(); + const auto& lootReport = m_loot.report(); - if (!report.messages.empty()) { + if (!lootReport.messages.empty()) { addLineOutput(""); } - QString md; - - if (!report.messages.empty()) { - for (auto&& m : report.messages) { - log(m.type, m.text); - - md += " - "; - - switch (m.type) - { - case log::Error: - { - md += "**" + QObject::tr("Error") + "**: "; - break; - } - - case log::Warning: - { - md += "**" + QObject::tr("Warning") + "**: "; - break; - } - - default: - { - break; - } - } - - md += m.text + "\n"; - } - } else { - md += QObject::tr("**No messages.**"); + for (auto&& m : lootReport.messages) { + log(m.type, m.text); } - m_report.setText(md); - - for (auto&& p : report.plugins) { + for (auto&& p : lootReport.plugins) { m_core.pluginList()->addLootReport(p.name, p); } + + m_report.setText(lootReport.toMarkdown()); } diff --git a/src/resources/markdown.html b/src/resources/markdown.html index a09b2209..1ecaf1b9 100644 --- a/src/resources/markdown.html +++ b/src/resources/markdown.html @@ -2,278 +2,276 @@ - - - -- cgit v1.3.1 From 4bbdbb000fd5051fe80b5dca21dda60910284333 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 27 Nov 2019 19:14:37 -0500 Subject: split filter list --- src/CMakeLists.txt | 3 + src/filterlist.cpp | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/filterlist.h | 42 ++++++++++++ src/mainwindow.cpp | 177 ++++--------------------------------------------- src/mainwindow.h | 12 +--- 5 files changed, 249 insertions(+), 174 deletions(-) create mode 100644 src/filterlist.cpp create mode 100644 src/filterlist.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3a42409..a46908ef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,6 +144,7 @@ SET(organizer_SRCS uilocker.cpp loot.cpp lootdialog.cpp + filterlist.cpp shared/windows_error.cpp shared/error_report.cpp @@ -268,6 +269,7 @@ SET(organizer_HDRS loot.h lootdialog.h json.h + filterlist.h shared/windows_error.h shared/error_report.h @@ -320,6 +322,7 @@ SET(organizer_RCS source_group(src REGULAR_EXPRESSION ".*\\.(h|cpp|ui)") set(application + filterlist iuserinterface main mainwindow diff --git a/src/filterlist.cpp b/src/filterlist.cpp new file mode 100644 index 00000000..8ef4b62d --- /dev/null +++ b/src/filterlist.cpp @@ -0,0 +1,189 @@ +#include "filterlist.h" +#include "ui_mainwindow.h" +#include "categories.h" +#include "categoriesdialog.h" +#include + +using namespace MOBase; + +FilterList::FilterList(Ui::MainWindow* ui, CategoryFactory& factory) + : ui(ui), m_factory(factory) +{ + QObject::connect( + ui->categoriesList, &QTreeWidget::customContextMenuRequested, + [&](auto&& pos){ onContextMenu(pos); }); + + QObject::connect( + ui->categoriesList, &QTreeWidget::itemSelectionChanged, + [&]{ onSelection(); }); +} + +QTreeWidgetItem* FilterList::addFilterItem( + QTreeWidgetItem *root, const QString &name, int categoryID, + ModListSortProxy::FilterType type) +{ + QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name)); + item->setData(0, Qt::ToolTipRole, name); + item->setData(0, Qt::UserRole, categoryID); + item->setData(0, Qt::UserRole + 1, type); + if (root != nullptr) { + root->addChild(item); + } else { + ui->categoriesList->addTopLevelItem(item); + } + return item; +} + +void FilterList::addContentFilters() +{ + for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) { + addFilterItem(nullptr, tr("").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT); + } +} + +void FilterList::addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID) +{ + for (unsigned int i = 1; + i < static_cast(m_factory.numCategories()); ++i) { + if ((m_factory.getParentID(i) == targetID)) { + int categoryID = m_factory.getCategoryID(i); + if (categoriesUsed.find(categoryID) != categoriesUsed.end()) { + QTreeWidgetItem *item = + addFilterItem(root, m_factory.getCategoryName(i), + categoryID, ModListSortProxy::TYPE_CATEGORY); + if (m_factory.hasChildren(i)) { + addCategoryFilters(item, categoriesUsed, categoryID); + } + } + } + } +} + +void FilterList::refresh() +{ + QItemSelection currentSelection = ui->modList->selectionModel()->selection(); + + QVariant currentIndexName = ui->modList->currentIndex().data(); + ui->modList->setCurrentIndex(QModelIndex()); + + QStringList selectedItems; + for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) { + selectedItems.append(item->text(0)); + } + + ui->categoriesList->clear(); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NONEXUSID, ModListSortProxy::TYPE_SPECIAL); + addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOGAMEDATA, ModListSortProxy::TYPE_SPECIAL); + + addContentFilters(); + std::set categoriesUsed; + for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx); + for (int categoryID : modInfo->getCategories()) { + int currentID = categoryID; + std::set cycleTest; + // also add parents so they show up in the tree + while (currentID != 0) { + categoriesUsed.insert(currentID); + if (!cycleTest.insert(currentID).second) { + log::warn("cycle in categories: {}", SetJoin(cycleTest, ", ")); + break; + } + currentID = m_factory.getParentID(m_factory.getCategoryIndex(currentID)); + } + } + } + + addCategoryFilters(nullptr, categoriesUsed, 0); + + for (const QString &item : selectedItems) { + QList matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive); + if (matches.size() > 0) { + matches.at(0)->setSelected(true); + } + } + ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select); + QModelIndexList matchList; + if (currentIndexName.isValid()) { + matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName); + } + + if (matchList.size() > 0) { + ui->modList->setCurrentIndex(matchList.at(0)); + } +} + +void FilterList::setSelection(std::vector categories) +{ + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } +} + +void FilterList::clearSelection() +{ + ui->categoriesList->clearSelection(); +} + +void FilterList::onSelection() +{ + QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows(); + std::vector categories; + std::vector content; + for (const QModelIndex &index : indices) { + int filterType = index.data(Qt::UserRole + 1).toInt(); + if ((filterType == ModListSortProxy::TYPE_CATEGORY) + || (filterType == ModListSortProxy::TYPE_SPECIAL)) { + int categoryId = index.data(Qt::UserRole).toInt(); + if (categoryId != CategoryFactory::CATEGORY_NONE) { + categories.push_back(categoryId); + } + } else if (filterType == ModListSortProxy::TYPE_CONTENT) { + int contentId = index.data(Qt::UserRole).toInt(); + content.push_back(contentId); + } + } + + emit changed(categories, content); + + ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0); + + if (indices.count() == 0) { + ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); + } else if (indices.count() > 1) { + ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); + } else { + ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString())); + } + ui->modList->reset(); +} + +void FilterList::onContextMenu(const QPoint &pos) +{ + QMenu menu; + menu.addAction(tr("Edit Categories..."), [&]{ editCategories(); }); + menu.addAction(tr("Deselect filter"), [&]{ clearSelection(); }); + + menu.exec(ui->categoriesList->viewport()->mapToGlobal(pos)); +} + +void FilterList::editCategories() +{ + CategoriesDialog dialog(qApp->activeWindow()); + + if (dialog.exec() == QDialog::Accepted) { + dialog.commitChanges(); + } +} diff --git a/src/filterlist.h b/src/filterlist.h new file mode 100644 index 00000000..ca74021a --- /dev/null +++ b/src/filterlist.h @@ -0,0 +1,42 @@ +#ifndef MODORGANIZER_CATEGORIESLIST_INCLUDED +#define MODORGANIZER_CATEGORIESLIST_INCLUDED + +#include "modlistsortproxy.h" +#include + +namespace Ui { class MainWindow; }; +class CategoryFactory; + +class FilterList : public QObject +{ + Q_OBJECT; + +public: + FilterList(Ui::MainWindow* ui, CategoryFactory& factory); + + void setSelection(std::vector categories); + void clearSelection(); + void refresh(); + +signals: + void changed(std::vector categories, std::vector content); + +private: + Ui::MainWindow* ui; + CategoryFactory& m_factory; + + void onContextMenu(const QPoint &pos); + void onSelection(); + + void editCategories(); + + QTreeWidgetItem* addFilterItem( + QTreeWidgetItem *root, const QString &name, int categoryID, + ModListSortProxy::FilterType type); + + void addContentFilters(); + void addCategoryFilters( + QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID); +}; + +#endif // MODORGANIZER_CATEGORIESLIST_INCLUDED diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d5636ab9..b63f9211 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -75,6 +75,7 @@ along with Mod Organizer. If not, see . #include "appconfig.h" #include "eventfilter.h" #include "statusbar.h" +#include "filterlist.h" #include #include #include @@ -260,6 +261,11 @@ MainWindow::MainWindow(Settings &settings languageChange(settings.interface().language()); m_CategoryFactory.loadCategories(); + m_Filters.reset(new FilterList(ui, m_CategoryFactory)); + connect(m_Filters.get(), &FilterList::changed, [&](auto&& cats, auto&& content) { + m_ModListSortProxy->setCategoryFilter(cats); + m_ModListSortProxy->setContentFilter(content); + }); ui->logList->setCore(m_OrganizerCore); @@ -1223,7 +1229,7 @@ void MainWindow::showEvent(QShowEvent *event) if (!m_WasVisible) { readSettings(); - refreshFilters(); + m_Filters->refresh(); // this needs to be connected here instead of in the constructor because the // actual changing of the stylesheet is done by MOApplication, which @@ -2646,108 +2652,6 @@ void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName } } -QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type) -{ - QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name)); - item->setData(0, Qt::ToolTipRole, name); - item->setData(0, Qt::UserRole, categoryID); - item->setData(0, Qt::UserRole + 1, type); - if (root != nullptr) { - root->addChild(item); - } else { - ui->categoriesList->addTopLevelItem(item); - } - return item; -} - -void MainWindow::addContentFilters() -{ - for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) { - addFilterItem(nullptr, tr("").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT); - } -} - -void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID) -{ - for (unsigned int i = 1; - i < static_cast(m_CategoryFactory.numCategories()); ++i) { - if ((m_CategoryFactory.getParentID(i) == targetID)) { - int categoryID = m_CategoryFactory.getCategoryID(i); - if (categoriesUsed.find(categoryID) != categoriesUsed.end()) { - QTreeWidgetItem *item = - addFilterItem(root, m_CategoryFactory.getCategoryName(i), - categoryID, ModListSortProxy::TYPE_CATEGORY); - if (m_CategoryFactory.hasChildren(i)) { - addCategoryFilters(item, categoriesUsed, categoryID); - } - } - } - } -} - -void MainWindow::refreshFilters() -{ - QItemSelection currentSelection = ui->modList->selectionModel()->selection(); - - QVariant currentIndexName = ui->modList->currentIndex().data(); - ui->modList->setCurrentIndex(QModelIndex()); - - QStringList selectedItems; - for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) { - selectedItems.append(item->text(0)); - } - - ui->categoriesList->clear(); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NONEXUSID, ModListSortProxy::TYPE_SPECIAL); - addFilterItem(nullptr, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOGAMEDATA, ModListSortProxy::TYPE_SPECIAL); - - addContentFilters(); - std::set categoriesUsed; - for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx); - for (int categoryID : modInfo->getCategories()) { - int currentID = categoryID; - std::set cycleTest; - // also add parents so they show up in the tree - while (currentID != 0) { - categoriesUsed.insert(currentID); - if (!cycleTest.insert(currentID).second) { - log::warn("cycle in categories: {}", SetJoin(cycleTest, ", ")); - break; - } - currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID)); - } - } - } - - addCategoryFilters(nullptr, categoriesUsed, 0); - - for (const QString &item : selectedItems) { - QList matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive); - if (matches.size() > 0) { - matches.at(0)->setSelected(true); - } - } - ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select); - QModelIndexList matchList; - if (currentIndexName.isValid()) { - matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName); - } - - if (matchList.size() > 0) { - ui->modList->setCurrentIndex(matchList.at(0)); - } -} - void MainWindow::renameMod_clicked() { @@ -4121,7 +4025,7 @@ void MainWindow::addRemoveCategories_MenuHandler() { m_OrganizerCore.modList()->notifyChange(m_ContextRow); } - refreshFilters(); + m_Filters->refresh(); } void MainWindow::replaceCategories_MenuHandler() { @@ -4165,7 +4069,7 @@ void MainWindow::replaceCategories_MenuHandler() { m_OrganizerCore.modList()->notifyChange(m_ContextRow); } - refreshFilters(); + m_Filters->refresh(); } void MainWindow::saveArchiveList() @@ -4217,12 +4121,7 @@ void MainWindow::checkModsForUpdates() if (updatesAvailable || checkingModsForUpdate) { m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } + m_Filters->setSelection({CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE}); } } @@ -4848,40 +4747,6 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) } -void MainWindow::on_categoriesList_itemSelectionChanged() -{ - QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows(); - std::vector categories; - std::vector content; - for (const QModelIndex &index : indices) { - int filterType = index.data(Qt::UserRole + 1).toInt(); - if ((filterType == ModListSortProxy::TYPE_CATEGORY) - || (filterType == ModListSortProxy::TYPE_SPECIAL)) { - int categoryId = index.data(Qt::UserRole).toInt(); - if (categoryId != CategoryFactory::CATEGORY_NONE) { - categories.push_back(categoryId); - } - } else if (filterType == ModListSortProxy::TYPE_CONTENT) { - int contentId = index.data(Qt::UserRole).toInt(); - content.push_back(contentId); - } - } - - m_ModListSortProxy->setCategoryFilter(categories); - m_ModListSortProxy->setContentFilter(content); - ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0); - - if (indices.count() == 0) { - ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); - } else if (indices.count() > 1) { - ui->currentCategoryLabel->setText(QString("(%1)").arg(tr(""))); - } else { - ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString())); - } - ui->modList->reset(); -} - - void MainWindow::deleteSavegame_clicked() { SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature(); @@ -5071,7 +4936,7 @@ void MainWindow::on_actionSettings_triggered() instManager->setDownloadDirectory(settings.paths().downloads()); fixCategories(); - refreshFilters(); + m_Filters->refresh(); if (settings.paths().profiles() != oldProfilesDirectory) { refreshProfiles(); @@ -6213,27 +6078,9 @@ void MainWindow::on_displayCategoriesBtn_toggled(bool checked) setCategoryListVisible(checked); } -void MainWindow::editCategories() -{ - CategoriesDialog dialog(this); - - if (dialog.exec() == QDialog::Accepted) { - dialog.commitChanges(); - } -} - void MainWindow::deselectFilters() { - ui->categoriesList->clearSelection(); -} - -void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) -{ - QMenu menu; - menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); - menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters())); - - menu.exec(ui->categoriesList->viewport()->mapToGlobal(pos)); + m_Filters->clearSelection(); } diff --git a/src/mainwindow.h b/src/mainwindow.h index c99c724b..04ba7a35 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -39,6 +39,7 @@ along with Mod Organizer. If not, see . class Executable; class CategoryFactory; class OrganizerCore; +class FilterList; class PluginListSortProxy; namespace BSA { class Archive; } @@ -235,8 +236,6 @@ private: void writeDataToFile(QFile &file, const QString &directory, const MOShared::DirectoryEntry &directoryEntry); - void refreshFilters(); - /** * Sets category selections from menu; for multiple mods, this will only apply * the changes made in the menu (which is the delta between the current menu selection and the reference mod) @@ -266,10 +265,6 @@ private: size_t checkForProblems(); - QTreeWidgetItem *addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type); - void addContentFilters(); - void addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID); - void setCategoryListVisible(bool visible); void displaySaveGameInfo(QListWidgetItem *newItem); @@ -327,6 +322,8 @@ private: MOBase::TutorialControl m_Tutorial; + std::unique_ptr m_Filters; + int m_OldProfileIndex; std::vector m_ModNameList; // the mod-list to go with the directory structure @@ -515,7 +512,6 @@ private slots: void onRequestsChanged(const APIStats& stats, const APIUserAccount& user); - void editCategories(); void deselectFilters(); void displayModInformation(const QString &modName, ModInfoTabIDs tabID); @@ -630,7 +626,6 @@ private slots: // ui slots void on_clearFiltersButton_clicked(); void on_btnRefreshData_clicked(); void on_btnRefreshDownloads_clicked(); - void on_categoriesList_customContextMenuRequested(const QPoint &pos); void on_conflictsCheckBox_toggled(bool checked); void on_showArchiveDataCheckBox_toggled(bool checked); void on_dataTree_customContextMenuRequested(const QPoint &pos); @@ -647,7 +642,6 @@ private slots: // ui slots void on_espList_customContextMenuRequested(const QPoint &pos); void on_displayCategoriesBtn_toggled(bool checked); void on_groupCombo_currentIndexChanged(int index); - void on_categoriesList_itemSelectionChanged(); void on_linkButton_pressed(); void on_showHiddenBox_toggled(bool checked); void on_bsaList_itemChanged(QTreeWidgetItem *item, int column); -- cgit v1.3.1 From b3331ef2c0b50ab2cea4c328e78f6ab58bca099d Mon Sep 17 00:00:00 2001 From: Silarn Date: Fri, 6 Dec 2019 23:12:18 -0600 Subject: Separate conflict flags and render them in separate columns --- src/CMakeLists.txt | 3 + src/colortable.cpp | 61 +- src/mainwindow.cpp | 22 +- src/modconflicticondelegate.cpp | 160 ++ src/modconflicticondelegate.h | 36 + src/modflagicondelegate.cpp | 74 +- src/modflagicondelegate.h | 4 - src/modinfo.h | 25 +- src/modinfobackup.cpp | 1 + src/modinfodialogfwd.h | 1 + src/modinfooverwrite.cpp | 9 + src/modinfooverwrite.h | 1 + src/modinfowithconflictinfo.cpp | 4 +- src/modinfowithconflictinfo.h | 3 +- src/modlist.cpp | 40 +- src/modlist.h | 3 + src/modlistsortproxy.cpp | 21 + src/modlistsortproxy.h | 1 + src/organizer_en.ts | 3671 ++++++++++++++++++++++----------------- 19 files changed, 2390 insertions(+), 1750 deletions(-) create mode 100644 src/modconflicticondelegate.cpp create mode 100644 src/modconflicticondelegate.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a46908ef..d0215930 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -114,6 +114,7 @@ SET(organizer_SRCS previewdialog.cpp aboutdialog.cpp modflagicondelegate.cpp + modconflicticondelegate.cpp genericicondelegate.cpp organizerproxy.cpp viewmarkingscrollbar.cpp @@ -238,6 +239,7 @@ SET(organizer_HDRS previewdialog.h aboutdialog.h modflagicondelegate.h + modconflicticondelegate.h genericicondelegate.h organizerproxy.h viewmarkingscrollbar.h @@ -491,6 +493,7 @@ set(widgets loglist loghighlighter modflagicondelegate + modconflicticondelegate modidlineedit noeditdelegate qtgroupingproxy diff --git a/src/colortable.cpp b/src/colortable.cpp index 61c5ee5f..b1e4ef6c 100644 --- a/src/colortable.cpp +++ b/src/colortable.cpp @@ -1,5 +1,6 @@ #include "colortable.h" #include "modflagicondelegate.h" +#include "modconflicticondelegate.h" #include "settings.h" class ColorItem; @@ -52,21 +53,17 @@ public: } void paint( - QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override + QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override { paintBackground(m_table, painter, option, index); ModFlagIconDelegate::paintIcons(painter, option, index, getIcons(index)); } protected: - QList getIcons(const QModelIndex &index) const override + QList getIcons(const QModelIndex& index) const override { const auto flags = { - ModInfo::FLAG_CONFLICT_MIXED, - ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, - ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, - ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED, ModInfo::FLAG_BACKUP, ModInfo::FLAG_NOTENDORSED, ModInfo::FLAG_NOTES, @@ -76,7 +73,48 @@ protected: return getIconsForFlags(flags, false); } - size_t getNumIcons(const QModelIndex &index) const override + size_t getNumIcons(const QModelIndex& index) const override + { + return getIcons(index).size(); + } + +private: + QTableWidget* m_table; +}; + + +// delegate for the icons column; paints the background and icons +// +class FakeModConflictIconDelegate : public ModConflictIconDelegate +{ +public: + explicit FakeModConflictIconDelegate(QTableWidget* table) + : m_table(table) + { + } + + void paint( + QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + paintBackground(m_table, painter, option, index); + ModFlagIconDelegate::paintIcons(painter, option, index, getIcons(index)); + } + +protected: + QList getIcons(const QModelIndex& index) const override + { + const auto flags = { + ModInfo::FLAG_CONFLICT_MIXED, + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, + ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED + }; + + return getIconsForFlags(flags, false); + } + + size_t getNumIcons(const QModelIndex& index) const override { return getIcons(index).size(); } @@ -183,11 +221,12 @@ void paintBackground( ColorTable::ColorTable(QWidget* parent) : QTableWidget(parent), m_settings(nullptr) { - setColumnCount(3); - setHorizontalHeaderLabels({"", "", ""}); + setColumnCount(4); + setHorizontalHeaderLabels({"", "", "", ""}); setItemDelegateForColumn(1, new ColoredBackgroundDelegate(this)); - setItemDelegateForColumn(2, new FakeModFlagIconDelegate(this)); + setItemDelegateForColumn(2, new FakeModConflictIconDelegate(this)); + setItemDelegateForColumn(3, new FakeModFlagIconDelegate(this)); connect( this, &QTableWidget::cellActivated, diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cb242b88..68627c90 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -61,6 +61,7 @@ along with Mod Organizer. If not, see . #include "filedialogmemory.h" #include "tutorialmanager.h" #include "modflagicondelegate.h" +#include "modconflicticondelegate.h" #include "genericicondelegate.h" #include "selectiondialog.h" #include "csvbuilder.h" @@ -552,7 +553,16 @@ void MainWindow::setupModList() flagDelegate, SLOT(columnResized(int,int,int))); + ModConflictIconDelegate* conflictFlagDelegate = new ModConflictIconDelegate( + ui->modList, ModList::COL_CONFLICTFLAGS, 120); + + connect( + ui->modList->header(), SIGNAL(sectionResized(int, int, int)), + conflictFlagDelegate, SLOT(columnResized(int, int, int))); + + ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate); + ui->modList->setItemDelegateForColumn(ModList::COL_CONFLICTFLAGS, conflictFlagDelegate); ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate); ui->modList->header()->installEventFilter(m_OrganizerCore.modList()); @@ -2317,6 +2327,15 @@ void MainWindow::processUpdates(Settings& settings) { ui->downloadView->header()->hideSection(i); } } + + if (lastVersion < QVersionNumber(2, 2, 2)) { + bool lastHidden = true; + for (int i = ModList::COL_CONFLICTFLAGS; i < ui->modList->model()->columnCount(); ++i) { + bool hidden = ui->modList->header()->isSectionHidden(i); + ui->modList->header()->setSectionHidden(i, lastHidden); + lastHidden = hidden; + } + } } if (currentVersion < lastVersion) { @@ -3877,7 +3896,8 @@ void MainWindow::on_modList_doubleClicked(const QModelIndex &index) case ModList::COL_MODID: tab = ModInfoTabIDs::Nexus; break; case ModList::COL_GAME: tab = ModInfoTabIDs::Nexus; break; case ModList::COL_CATEGORY: tab = ModInfoTabIDs::Categories; break; - case ModList::COL_FLAGS: tab = ModInfoTabIDs::Conflicts; break; + case ModList::COL_CONFLICTFLAGS: tab = ModInfoTabIDs::Conflicts; break; + case ModList::COL_FLAGS: tab = ModInfoTabIDs::Flags; break; } displayModInformation(sourceIdx.row(), tab); diff --git a/src/modconflicticondelegate.cpp b/src/modconflicticondelegate.cpp new file mode 100644 index 00000000..2ccf3363 --- /dev/null +++ b/src/modconflicticondelegate.cpp @@ -0,0 +1,160 @@ +#include "modconflicticondelegate.h" +#include +#include + +using namespace MOBase; + +ModInfo::EConflictFlag ModConflictIconDelegate::m_ConflictFlags[4] = { ModInfo::FLAG_CONFLICT_MIXED + , ModInfo::FLAG_CONFLICT_OVERWRITE + , ModInfo::FLAG_CONFLICT_OVERWRITTEN + , ModInfo::FLAG_CONFLICT_REDUNDANT }; + +ModInfo::EConflictFlag ModConflictIconDelegate::m_ArchiveLooseConflictFlags[2] = { ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE + , ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN }; + +ModInfo::EConflictFlag ModConflictIconDelegate::m_ArchiveConflictFlags[3] = { ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED + , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE + , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN }; + +ModConflictIconDelegate::ModConflictIconDelegate(QObject *parent, int logicalIndex, int compactSize) + : IconDelegate(parent) + , m_LogicalIndex(logicalIndex) + , m_CompactSize(compactSize) + , m_Compact(false) +{ +} + +void ModConflictIconDelegate::columnResized(int logicalIndex, int, int newSize) +{ + if (logicalIndex == m_LogicalIndex) { + m_Compact = newSize < m_CompactSize; + } +} + +QList ModConflictIconDelegate::getIconsForFlags( + std::vector flags, bool compact) +{ + QList result; + + // Don't do flags for overwrite + if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE_CONFLICT) != flags.end()) + return result; + + // insert conflict icons to provide nicer alignment + { // insert loose file conflicts first + auto iter = std::find_first_of(flags.begin(), flags.end(), + m_ConflictFlags, m_ConflictFlags + 4); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); + } + } + + { // insert loose vs archive overwrite second + auto iter = std::find(flags.begin(), flags.end(), + ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); + } + } + + { // insert loose vs archive overwritten third + auto iter = std::find_first_of(flags.begin(), flags.end(), + m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); + } + } + + { // insert archive conflicts last + auto iter = std::find_first_of(flags.begin(), flags.end(), + m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3); + if (iter != flags.end()) { + result.append(getFlagIcon(*iter)); + flags.erase(iter); + } else if (!compact) { + result.append(QString()); + } + } + + for (auto iter = flags.begin(); iter != flags.end(); ++iter) { + auto iconPath = getFlagIcon(*iter); + if (!iconPath.isEmpty()) + result.append(iconPath); + } + + return result; +} + +QList ModConflictIconDelegate::getIcons(const QModelIndex &index) const +{ + QVariant modid = index.data(Qt::UserRole + 1); + + if (modid.isValid()) { + ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt()); + return getIconsForFlags(info->getConflictFlags(), m_Compact); + } + + return {}; +} + +QString ModConflictIconDelegate::getFlagIcon(ModInfo::EConflictFlag flag) +{ + switch (flag) { + case ModInfo::FLAG_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/emblem_conflict_mixed"); + case ModInfo::FLAG_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/emblem_conflict_overwrite"); + case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/emblem_conflict_overwritten"); + case ModInfo::FLAG_CONFLICT_REDUNDANT: return QStringLiteral(":/MO/gui/emblem_conflict_redundant"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwrite"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwritten"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/archive_conflict_mixed"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_conflict_winner"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_conflict_loser"); + case ModInfo::FLAG_OVERWRITE_CONFLICT: return QString(); + default: + log::warn("ModInfo flag {} has no defined icon", flag); + return QString(); + } +} + +size_t ModConflictIconDelegate::getNumIcons(const QModelIndex &index) const +{ + unsigned int modIdx = index.data(Qt::UserRole + 1).toInt(); + if (modIdx < ModInfo::getNumMods()) { + ModInfo::Ptr info = ModInfo::getByIndex(modIdx); + std::vector flags = info->getConflictFlags(); + size_t count = flags.size(); + if (std::find_first_of(flags.begin(), flags.end(), m_ConflictFlags, m_ConflictFlags + 4) == flags.end()) { + ++count; + } + return count; + } else { + return 0; + } +} + + +QSize ModConflictIconDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &modelIndex) const +{ + size_t count = getNumIcons(modelIndex); + unsigned int index = modelIndex.data(Qt::UserRole + 1).toInt(); + QSize result; + if (index < ModInfo::getNumMods()) { + result = QSize(static_cast(count) * 40, 20); + } else { + result = QSize(1, 20); + } + if (option.rect.width() > 0) { + result.setWidth(std::min(option.rect.width(), result.width())); + } + return result; +} + diff --git a/src/modconflicticondelegate.h b/src/modconflicticondelegate.h new file mode 100644 index 00000000..d36477c6 --- /dev/null +++ b/src/modconflicticondelegate.h @@ -0,0 +1,36 @@ +#ifndef MODCONFLICTICONDELEGATE_H +#define MODCONFLICTICONDELEGATE_H + +#include "icondelegate.h" + +class ModConflictIconDelegate : public IconDelegate +{ + Q_OBJECT; + +public: + explicit ModConflictIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 120); + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + + static QList getIconsForFlags( + std::vector flags, bool compact); + + static QString getFlagIcon(ModInfo::EConflictFlag flag); + +public slots: + void columnResized(int logicalIndex, int oldSize, int newSize); + +protected: + virtual QList getIcons(const QModelIndex &index) const; + virtual size_t getNumIcons(const QModelIndex &index) const; + +private: + static ModInfo::EConflictFlag m_ConflictFlags[4]; + static ModInfo::EConflictFlag m_ArchiveLooseConflictFlags[2]; + static ModInfo::EConflictFlag m_ArchiveConflictFlags[3]; + + int m_LogicalIndex; + int m_CompactSize; + bool m_Compact; +}; + +#endif // MODCONFLICTICONDELEGATE_H diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp index a5e9aa22..6e1df147 100644 --- a/src/modflagicondelegate.cpp +++ b/src/modflagicondelegate.cpp @@ -4,18 +4,6 @@ using namespace MOBase; -ModInfo::EFlag ModFlagIconDelegate::m_ConflictFlags[4] = { ModInfo::FLAG_CONFLICT_MIXED - , ModInfo::FLAG_CONFLICT_OVERWRITE - , ModInfo::FLAG_CONFLICT_OVERWRITTEN - , ModInfo::FLAG_CONFLICT_REDUNDANT }; - -ModInfo::EFlag ModFlagIconDelegate::m_ArchiveLooseConflictFlags[2] = { ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE - , ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN }; - -ModInfo::EFlag ModFlagIconDelegate::m_ArchiveConflictFlags[3] = { ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED - , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE - , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN }; - ModFlagIconDelegate::ModFlagIconDelegate(QObject *parent, int logicalIndex, int compactSize) : IconDelegate(parent) , m_LogicalIndex(logicalIndex) @@ -37,54 +25,9 @@ QList ModFlagIconDelegate::getIconsForFlags( QList result; // Don't do flags for overwrite - if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE) != flags.end()) + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) return result; - // insert conflict icons to provide nicer alignment - { // insert loose file conflicts first - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ConflictFlags, m_ConflictFlags + 4); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!compact) { - result.append(QString()); - } - } - - { // insert loose vs archive overwrite second - auto iter = std::find(flags.begin(), flags.end(), - ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!compact) { - result.append(QString()); - } - } - - { // insert loose vs archive overwritten third - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!compact) { - result.append(QString()); - } - } - - { // insert archive conflicts last - auto iter = std::find_first_of(flags.begin(), flags.end(), - m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3); - if (iter != flags.end()) { - result.append(getFlagIcon(*iter)); - flags.erase(iter); - } else if (!compact) { - result.append(QString()); - } - } - for (auto iter = flags.begin(); iter != flags.end(); ++iter) { auto iconPath = getFlagIcon(*iter); if (!iconPath.isEmpty()) @@ -113,15 +56,6 @@ QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) case ModInfo::FLAG_INVALID: return QStringLiteral(":/MO/gui/problem"); case ModInfo::FLAG_NOTENDORSED: return QStringLiteral(":/MO/gui/emblem_notendorsed"); case ModInfo::FLAG_NOTES: return QStringLiteral(":/MO/gui/emblem_notes"); - case ModInfo::FLAG_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/emblem_conflict_mixed"); - case ModInfo::FLAG_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/emblem_conflict_overwrite"); - case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/emblem_conflict_overwritten"); - case ModInfo::FLAG_CONFLICT_REDUNDANT: return QStringLiteral(":/MO/gui/emblem_conflict_redundant"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwrite"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwritten"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/archive_conflict_mixed"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_conflict_winner"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_conflict_loser"); case ModInfo::FLAG_ALTERNATE_GAME: return QStringLiteral(":/MO/gui/alternate_game"); case ModInfo::FLAG_FOREIGN: return QString(); case ModInfo::FLAG_SEPARATOR: return QString(); @@ -140,11 +74,7 @@ size_t ModFlagIconDelegate::getNumIcons(const QModelIndex &index) const if (modIdx < ModInfo::getNumMods()) { ModInfo::Ptr info = ModInfo::getByIndex(modIdx); std::vector flags = info->getFlags(); - size_t count = flags.size(); - if (std::find_first_of(flags.begin(), flags.end(), m_ConflictFlags, m_ConflictFlags + 4) == flags.end()) { - ++count; - } - return count; + return flags.size(); } else { return 0; } diff --git a/src/modflagicondelegate.h b/src/modflagicondelegate.h index 4f22dd90..ecab7e95 100644 --- a/src/modflagicondelegate.h +++ b/src/modflagicondelegate.h @@ -24,10 +24,6 @@ protected: virtual size_t getNumIcons(const QModelIndex &index) const; private: - static ModInfo::EFlag m_ConflictFlags[4]; - static ModInfo::EFlag m_ArchiveLooseConflictFlags[2]; - static ModInfo::EFlag m_ArchiveConflictFlags[3]; - int m_LogicalIndex; int m_CompactSize; bool m_Compact; diff --git a/src/modinfo.h b/src/modinfo.h index 30a115c7..7c41e0a1 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -60,14 +60,7 @@ public: static QString s_HiddenExt; - enum EFlag { - FLAG_INVALID, - FLAG_BACKUP, - FLAG_SEPARATOR, - FLAG_OVERWRITE, - FLAG_FOREIGN, - FLAG_NOTENDORSED, - FLAG_NOTES, + enum EConflictFlag { FLAG_CONFLICT_OVERWRITE, FLAG_CONFLICT_OVERWRITTEN, FLAG_CONFLICT_MIXED, @@ -77,6 +70,17 @@ public: FLAG_ARCHIVE_CONFLICT_OVERWRITE, FLAG_ARCHIVE_CONFLICT_OVERWRITTEN, FLAG_ARCHIVE_CONFLICT_MIXED, + FLAG_OVERWRITE_CONFLICT, + }; + + enum EFlag { + FLAG_INVALID, + FLAG_BACKUP, + FLAG_SEPARATOR, + FLAG_OVERWRITE, + FLAG_FOREIGN, + FLAG_NOTENDORSED, + FLAG_NOTES, FLAG_PLUGIN_SELECTED, FLAG_ALTERNATE_GAME, FLAG_TRACKED, @@ -519,6 +523,11 @@ public: */ virtual std::vector getFlags() const = 0; + /** + * @return a list of conflict flags for this mod + */ + virtual std::vector getConflictFlags() const = 0; + /** * @return a list of content types contained in a mod */ diff --git a/src/modinfobackup.cpp b/src/modinfobackup.cpp index 6e307103..6a34b86a 100644 --- a/src/modinfobackup.cpp +++ b/src/modinfobackup.cpp @@ -1,5 +1,6 @@ #include "modinfobackup.h" + std::vector ModInfoBackup::getFlags() const { std::vector result = ModInfoRegular::getFlags(); diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h index 2147fc04..e4a61208 100644 --- a/src/modinfodialogfwd.h +++ b/src/modinfodialogfwd.h @@ -14,6 +14,7 @@ enum class ModInfoTabIDs Images, Esps, Conflicts, + Flags, Categories, Nexus, Notes, diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp index fb110abb..9a6a22c1 100644 --- a/src/modinfooverwrite.cpp +++ b/src/modinfooverwrite.cpp @@ -37,6 +37,15 @@ std::vector ModInfoOverwrite::getFlags() const return result; } +std::vector ModInfoOverwrite::getConflictFlags() const +{ + std::vector result; + result.push_back(FLAG_OVERWRITE_CONFLICT); + for (auto flag : ModInfoWithConflictInfo::getConflictFlags()) + result.push_back(flag); + return result; +} + int ModInfoOverwrite::getHighlight() const { int highlight = (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER; diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index c5f58c2e..ecbdbe3d 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -51,6 +51,7 @@ public: virtual QDateTime getExpires() const { return QDateTime(); } virtual std::vector getIniTweaks() const { return std::vector(); } virtual std::vector getFlags() const; + virtual std::vector getConflictFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; virtual int getNexusFileStatus() const { return 0; } diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 7a2b0071..f5a243ae 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -14,9 +14,9 @@ void ModInfoWithConflictInfo::clearCaches() m_LastConflictCheck = QTime(); } -std::vector ModInfoWithConflictInfo::getFlags() const +std::vector ModInfoWithConflictInfo::getConflictFlags() const { - std::vector result; + std::vector result; switch (isConflicted()) { case CONFLICT_MIXED: { result.push_back(ModInfo::FLAG_CONFLICT_MIXED); diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 13d87d25..9869bc5a 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -12,7 +12,8 @@ public: ModInfoWithConflictInfo(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure); - std::vector getFlags() const; + std::vector getConflictFlags() const; + virtual std::vector getFlags() const { return std::vector(); }; /** * @brief clear all caches held for this mod diff --git a/src/modlist.cpp b/src/modlist.cpp index b7a9b0a1..c5b8c856 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -159,15 +159,6 @@ QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const output << QString("%1").arg(modInfo->notes()); return output.join(""); } - case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files"); - case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files"); - case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten"); - case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); case ModInfo::FLAG_ALTERNATE_GAME: return tr("
This mod is for a different game, " "make sure it's compatible or it could cause crashes."); case ModInfo::FLAG_TRACKED: return tr("Mod is being tracked on the website"); @@ -176,6 +167,23 @@ QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const } +QString ModList::getConflictFlagText(ModInfo::EConflictFlag flag, ModInfo::Ptr modInfo) const +{ + switch (flag) { + case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files"); + case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files"); + case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten"); + case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); + default: return ""; + } +} + + QVariantList ModList::contentsToIcons(const std::vector &contents) const { QVariantList result; @@ -218,7 +226,8 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) { if ((column == COL_FLAGS) - || (column == COL_CONTENT)) { + || (column == COL_CONTENT) + || (column == COL_CONFLICTFLAGS)) { return QVariant(); } else if (column == COL_NAME) { auto flags = modInfo->getFlags(); @@ -443,6 +452,15 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const result += getFlagText(flag, modInfo); } + return result; + } else if (column == COL_CONFLICTFLAGS) { + QString result; + + for (ModInfo::EConflictFlag flag : modInfo->getConflictFlags()) { + if (result.length() != 0) result += "
"; + result += getConflictFlagText(flag, modInfo); + } + return result; } else if (column == COL_CONTENT) { return contentsToToolTip(modInfo->getContents()); @@ -1274,6 +1292,7 @@ void ModList::dropModeUpdate(bool dropOnItems) QString ModList::getColumnName(int column) { switch (column) { + case COL_CONFLICTFLAGS: return tr("Conflicts"); case COL_FLAGS: return tr("Flags"); case COL_CONTENT: return tr("Content"); case COL_NAME: return tr("Mod Name"); @@ -1299,6 +1318,7 @@ QString ModList::getColumnToolTip(int column) case COL_CATEGORY: return tr("Category of the mod."); case COL_GAME: return tr("The source game which was the origin of this mod."); case COL_MODID: return tr("Id of the mod as used on Nexus."); + case COL_CONFLICTFLAGS: return tr("Indicators of file conflicts between mods."); case COL_FLAGS: return tr("Emblems to highlight things that might require attention."); case COL_CONTENT: return tr("Depicts the content of the mod:
" "" diff --git a/src/modlist.h b/src/modlist.h index 631401c0..8841ec29 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -57,6 +57,7 @@ public: enum EColumn { COL_NAME, + COL_CONFLICTFLAGS, COL_FLAGS, COL_CONTENT, COL_CATEGORY, @@ -289,6 +290,8 @@ private: QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const; + QString getConflictFlagText(ModInfo::EConflictFlag flag, ModInfo::Ptr modInfo) const; + static QString getColumnToolTip(int column); QVariantList contentsToIcons(const std::vector &content) const; diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp index 64d5de42..440786c5 100644 --- a/src/modlistsortproxy.cpp +++ b/src/modlistsortproxy.cpp @@ -115,6 +115,17 @@ unsigned long ModListSortProxy::flagsId(const std::vector &flags return result; } +unsigned long ModListSortProxy::conflictFlagsId(const std::vector& flags) const +{ + unsigned long result = 0; + for (ModInfo::EConflictFlag flag : flags) { + if ((flag != ModInfo::FLAG_OVERWRITE_CONFLICT)) { + result += 1 << (int)flag; + } + } + return result; +} + bool ModListSortProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const { @@ -155,6 +166,16 @@ bool ModListSortProxy::lessThan(const QModelIndex &left, lt = flagsId(leftFlags) < flagsId(rightFlags); } } break; + case ModList::COL_CONFLICTFLAGS: { + std::vector leftFlags = leftMod->getConflictFlags(); + std::vector rightFlags = rightMod->getConflictFlags(); + if (leftFlags.size() != rightFlags.size()) { + lt = leftFlags.size() < rightFlags.size(); + } + else { + lt = conflictFlagsId(leftFlags) < conflictFlagsId(rightFlags); + } + } break; case ModList::COL_CONTENT: { std::vector lContent = leftMod->getContents(); std::vector rContent = rightMod->getContents(); diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h index 46356fe9..d733b783 100644 --- a/src/modlistsortproxy.h +++ b/src/modlistsortproxy.h @@ -142,6 +142,7 @@ protected: private: unsigned long flagsId(const std::vector &flags) const; + unsigned long conflictFlagsId(const std::vector& flags) const; bool hasConflictFlag(const std::vector &flags) const; void updateFilterActive(); bool filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const; diff --git a/src/organizer_en.ts b/src/organizer_en.ts index e3531cb2..72972e4f 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -15,117 +15,122 @@ - - <html><head/><body><p>Source code can be found at <a href="https://github.com/ModOrganizer2/modorganizer"><span style=" text-decoration: underline; color:#007af4;">GitHub</span></a>.</p></body></html> + + usvfs: - + + <html><head/><body><p>Source code can be found at <a href="https://github.com/ModOrganizer2/modorganizer">GitHub</a>.</p></body></html> + + + + Used Software - + Thanks - + Lead Developers && Maintainers - + LePresidente (Project Lead) - + MO2 Developers && Contributors - + Translators - + Cyb3r (Dutch) - + fruttyx (French) - + Yoplala (French) - + Faron (German) - + yohru (Japanese) - + Mordan (Greek) - + Yoosk (Polish) - + Brgodfx (Portuguese) - + zDas (Portuguese) - + Jax (Swedish) - + Nubbie (Swedish) - + ...and all other contributors! - + Other Supporters && Contributors - + Tannin (Original Creator) - + Close - + No license @@ -173,17 +178,17 @@ p, li { white-space: pre-wrap; } AdvancedConflictListModel - + Overwrites - + File - + Overwritten By @@ -289,33 +294,43 @@ p, li { white-space: pre-wrap; } ConflictsTab - - &Hide + + &Execute - - &Unhide + + &Open - - &Open/Execute + + Open with &VFS - + &Preview - + + &Go to... + + + + Open in &Explorer - - &Go to... + + &Hide + + + + + &Unhide @@ -403,79 +418,84 @@ p, li { white-space: pre-wrap; } - - < game %1 mod %2 file %3 > + + Source Game - Unknown + < game %1 mod %2 file %3 > + Unknown + + + + Pending - + Started - + Canceling - + Pausing - + Canceled - + Paused - + Error - - - + + + Fetching Info - + Downloaded - + Installed - + Uninstalled - + Pending download - + Information missing, please select "Query Info" from the context menu to re-retrieve. @@ -483,156 +503,153 @@ p, li { white-space: pre-wrap; } DownloadListWidget - + Install - + Query Info - + Visit on Nexus - + Open File - - - + + + Show in Folder - - + + Delete - + Un-Hide - + Hide - + Cancel - + Pause - + Resume - + Delete Installed Downloads... - Delete Installed... - + Delete Uninstalled Downloads... - Delete Uninstalled... - + Delete All Downloads... - Delete All... - + Hide Installed... - + Hide Uninstalled... - + Hide All... - + Un-Hide All... - - - - + + + + Delete Files? - + This will permanently delete the selected download. Are you absolutely sure you want to proceed? - + This will remove all finished downloads from this list and from disk. Are you absolutely sure you want to proceed? - + This will remove all installed downloads from this list and from disk. Are you absolutely sure you want to proceed? - + This will remove all uninstalled downloads from this list and from disk. Are you absolutely sure you want to proceed? - - - + + + Hide Files? - + This will remove all finished downloads from this list (but NOT from disk). - + This will remove all installed downloads from this list (but NOT from disk). - + This will remove all uninstalled downloads from this list (but NOT from disk). @@ -645,37 +662,37 @@ Are you absolutely sure you want to proceed? - + Memory allocation error (in refreshing directory). - + failed to download %1: could not open output file: %2 - + Download again? - + A file with the same name "%1" has already been downloaded. Do you want to download it again? The new file will receive a different name. - + Wrong Game - + The download link is for a mod for "%1" but this instance of MO has been set up for "%2". - + There is already a download queued for this file. Mod %1 @@ -683,12 +700,12 @@ File %2 - + Already Queued - + There is already a download started for this file. Mod %1: %2 @@ -696,276 +713,277 @@ File %3: %4 - + Already Started - - + + remove: invalid download index %1 - + failed to delete %1 - + failed to delete meta file for %1 - + restore: invalid download index: %1 - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - - + + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + Hashing download file '%1' - + Cancel - + VisitNexus: invalid download index %1 - + Nexus ID for this Mod is unknown - + OpenFile: invalid download index %1 - + OpenFileInDownloadsFolder: invalid download index %1 - + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Miscellaneous - + Deleted - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - + + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -998,19 +1016,18 @@ Canceling download "%2"... - + Remove the selected executable - + Remove - @@ -1018,7 +1035,11 @@ Canceling download "%2"... - + + Up + + + @@ -1026,261 +1047,339 @@ Canceling download "%2"... - + + Down + + + + Adds the executables provided by the game plugin and moves any existing executables out of the way - + Reset - + List of configured executables - + This is a list of your configured executables. Executables in grey are automatically recognised and can not be modified. - + Title - + Name of the executable. This is only for display purposes. - + Binary - + Binary to run - + Browse filesystem - + Browse filesystem for the executable to run. - - + + ... - + Start in - + Arguments - + Arguments to pass to the application - + Allow the Steam AppID to be used for this executable to be changed. - + Allow the Steam AppID to be used for this executable to be changed. Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game. Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID. This overwrite is already preconfigured. - + Overwrite Steam AppID - + Steam AppID to use for this executable that differs from the games AppID. - + Steam AppID to use for this executable that differs from the games AppID. Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game (usually 72850). Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID (usually 202480). This overwrite is already preconfigured. - + If this is enabled, new files are created in the specified mod instead of the "Overwrite" mod. - - Create Files in Mod instead of Overwrite (*) + + Create files in mod instead of overwrite (*) - + If this is enabled, the configured libraries will be automatically loaded when this executable is launched. - - Force Load Libraries (*) + + Force load libraries (*) - + Configure Libraries - - Use Application's Icon for desktop shortcuts - Use Application's Icon for shortcuts + + Use application's icon for desktop shortcuts + + + + + + This executable will not appear in the list, on the toolbar or in the menu. It will still be visible in this dialog. + + + + + Hide in user interface + + + + + (*) Profile specific + + + + + Add from file... + + + + + Add empty - - (*) Profile Specific + + Clone selected - + Reset plugin executables - + This will restore all the executables provided by the game plugin. If there are existing executables with the same names, they will be automatically renamed and left unchanged. - - + + New Executable - - Select a binary + + Select a directory - - Executable (%1) + + Executables (*.exe *.bat *.jar) - - Select a directory + + All Files (*.*) - - Java (32-bit) required + + Select an executable - - MO requires 32-bit java to run this application. If you already have it installed, select javaw.exe from that installation as the binary. + + Java required + + + + + MO requires Java to run this application. If you already have it installed, select javaw.exe from that installation as the binary. FileTreeTab - + &New Folder - + &Open/Execute - + + Open with &VFS + + + + &Preview - + Open in &Explorer - + &Rename - + &Delete - + &Hide - + &Unhide - - + + New Folder - + Failed to create "%1" - + Are you sure you want to delete "%1"? - + Are you sure you want to delete the selected files? - + Confirm - + Failed to delete %1 + + + &Execute + + + + + &Open + + + + + FilterList + + + Not + + + + + Filter separators + + + + + Show separators + + + + + Hide separators + + + + + <Contains %1> + + FindDialog @@ -1416,9 +1515,16 @@ Right now the only case I know of where this needs to be overwritten is for the Extracting files + + + + + Extraction failed: %1 + + - failed to create backup + Failed to create backup @@ -1511,29 +1617,6 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - - LockedDialog - - - Running virtualized processes - - - - - This dialog should disappear automatically if the application/game is done. Click unlock if it didn't. - - - - - MO is locked while the executable is running. - - - - - Unlock - - - LogList @@ -1572,6 +1655,57 @@ This is likely due to a corrupted or incompatible download or unrecognized archi + + Loot + + + failed to start loot + + + + + Loot failed. Exit code was: %1 + + + + + LootDialog + + + LOOT + + + + + Progress + + + + + about:blank + + + + + Details + + + + + Open JSON report + + + + + Stopping LOOT... + + + + + Loot failed to run + + + MOApplication @@ -1598,17 +1732,17 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + failed to write to %1 - + file not found: %1 - + Save @@ -1632,48 +1766,59 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MainWindow - - - Categories + + Filters - + Clear - - If checked, only mods that match all selected categories are displayed. + + Edit... + + + + + Display mods that match all selected categories. - + And - - If checked, all mods that match at least one of the selected categories are displayed. + + Display mods that match at least one of the selected categories - + Or - + + Filter: only show the separators that match the current filters +Show: always show separators +Hide: never show separators + + + + Profile - + Pick a module collection - + <!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; } @@ -1683,84 +1828,84 @@ p, li { white-space: pre-wrap; } - + Open list options... - + Refresh list. This is usually not necessary unless you modified data outside the program. - + Show Open Folders menu... - - + + Restore Backup... - - - + + + Create Backup - - + + Active: - + This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter. - - List of available mods. - - - - + This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders. - - - - + + + + Filter - + Clear all Filters - + No groups - + + Categories + + + + Nexus IDs - + Pick a program to run. - + <!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; } @@ -1770,12 +1915,12 @@ p, li { white-space: pre-wrap; } - + Run program - + <!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; } @@ -1784,17 +1929,17 @@ p, li { white-space: pre-wrap; } - + Run - + Create a shortcut in your start menu or on the desktop to the specified program - + <!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; } @@ -1803,32 +1948,32 @@ p, li { white-space: pre-wrap; } - + Shortcut - + Plugins - + Sort - + 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. - + List of available esp/esm files - + <!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; } @@ -1837,27 +1982,27 @@ p, li { white-space: pre-wrap; } - + Archives - + <html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html> - - <html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html> + + <html><head/><body><p>Currently detected archives. (<a href="#">What is an archive?</a>)</p></body></html> - + List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order. - + BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! @@ -1865,72 +2010,72 @@ p, li { white-space: pre-wrap; } - + Data - + refresh data-directory overview - + Refresh the overview. This may take a moment. - - - - + + + + Refresh - + This is an overview of your data directory as visible to the game (and tools). - + File - + Mod - - + + Filters the above list so that only conflicts are displayed. - + Show only conflicts - - + + Filters the above list so that files from archives are not shown - + Show files from Archives - + Saves - + <!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; } @@ -1941,1170 +2086,1119 @@ p, li { white-space: pre-wrap; } - + Downloads - + Refresh downloads view - + This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here. - + Show Hidden - + Main ToolBar - + &File - - + + &Tools - - - + + + &Help - + &View - + &Toolbars - + &Run - - + + Log - + Install &Mod... - + Install &Mod - - + + Install a new mod from an archive - + Ctrl+M - + &Profiles... - + &Profiles - - + + Configure profiles - + Ctrl+P - + &Executables... - + &Executables - - + + Configure the executables that can be started through Mod Organizer - + Ctrl+E - + &Tool Plugins - + Tools - + Ctrl+I - + &Settings... - + &Settings - - + + Configure settings and workarounds - + Ctrl+S - - + + Visit &Nexus - - + + Visit the Nexus website in your browser for more mods - + Ctrl+N - - + + &Update Mod Organizer - - + + Mod Organizer is up-to-date - + &Notifications... - - + + Open the notifications dialog - + This button will be highlighted on the toolbar if MO discovered potential problems in your setup and provide tips on how to fix them. - - + + Show help options - + Ctrl+H - - + + &Endorse ModOrganizer - - - + + + Endorse Mod Organizer - + &Change Game... - + &Change Game - - + + Open the Instance selection dialog to manage a different Game - - + + E&xit - - + + Exits Mod Organizer - + M&ain Toolbar - + &Small Icons - + Lar&ge Icons - + &Icons Only - + &Text Only - + I&cons and Text - + M&edium Icons - + &Menu - + Status &bar - St&atus bar - + Toolbar and Menu - + Desktop - + Start Menu - + There is no supported sort mechanism for this game. You will probably have to use a third-party tool. - + Crash on exit - + MO crashed while exiting. Some settings may not be saved. Error: %1 - + There are notifications to read - + There are no notifications - - - + + + Endorse - + Won't Endorse - + Help on UI - + Documentation - + Chat on Discord - + Report Issue - + Tutorials - + About - + About Qt - + Name - + Please enter a name for the new profile - + failed to create profile: %1 - + Show tutorial? - + You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu. - + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - + Browse Mod Page - + Also in: <br> - + No conflict - + <Edit...> - + + (no executables) + + + + This bsa is enabled in the ini file so it may be required! - + Activating Network Proxy - + Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues. - + Choose Mod - + Mod Archive - + Start Tutorial? - + You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue? - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - - <Contains %1> + + failed to rename mod: %1 - - <Checked> + + Overwrite? - - <Unchecked> + + This will replace the existing mod "%1". Continue? - - <Update> + + failed to remove mod "%1" - - <Mod Backup> + + + + failed to rename "%1" to "%2" - - <Managed by MO> + + + + + Confirm - - <Managed outside MO> + + Remove the following mods?<br><ul>%1</ul> - - <No category> + + failed to remove mod: %1 - - <Conflicted> + + + + Failed - - <Not Endorsed> + + Installation file no longer exists - - failed to rename mod: %1 - - - - - Overwrite? - - - - - This will replace the existing mod "%1". Continue? - - - - - failed to remove mod "%1" - - - - - - - failed to rename "%1" to "%2" - - - - - - - - Confirm - - - - - Remove the following mods?<br><ul>%1</ul> - - - - - failed to remove mod: %1 - - - - - - - Failed - - - - - Installation file no longer exists - - - - + Mods installed with old versions of MO can't be reinstalled in this way. - + Failed to create backup. - + Endorsing multiple mods will take a while. Please wait... - + Unendorsing multiple mods will take a while. Please wait... - + Failed to display overwrite dialog: %1 - + Opening Nexus Links - + You are trying to open %1 links to Nexus Mods. Are you sure you want to do this? - + Nexus ID for this mod is unknown - + Opening Web Pages - + You are trying to open %1 Web Pages. Are you sure you want to do this? - + <table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table> - + <table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table> - - - + + + Create Mod... - + This will create an empty mod. Please enter a name: - - + + A mod with this name already exists - + Create Separator... - + This will create a new separator. Please enter a name: - + A separator with this name already exists - + This will move all files from overwrite into a new, regular mod. Please enter a name: - + Move successful. - - + + Are you sure? - + About to recursively delete: - + Continue? - + The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated. - + Sorry - + I don't know a versioning scheme where %1 is newer than %2. - + Really enable all visible mods? - + Really disable all visible mods? - + Export to csv - + CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead. - + Select what mods you want export: - + All installed mods - + Only active (checked) mods from your current profile - + All currently visible mods in the mod list - + Choose what Columns to export: - + Mod_Priority - + Mod_Name - + Notes_column - + Mod_Status - + Primary_Category - + Nexus_ID - + Mod_Nexus_URL - + Mod_Version - + Install_Date - + Download_File_Name - + export failed: %1 - + Open Game folder - + Open MyGames folder - + Open INIs folder - + Open Instance folder - + Open Mods folder - + Open Profile folder - + Open Downloads folder - + Open MO2 Install folder - + Open MO2 Plugins folder - + + Open MO2 Stylesheets folder + + + + Open MO2 Logs folder - + Install Mod... - + Create empty mod - + Create Separator - + Enable all visible - + Disable all visible - + Check for updates - + Export to csv... - - + + Send to - - + + Top - - + + Bottom - - + + Priority... - + Separator... - + All Mods - + Sync to Mods... - + Move content to Mod... - + Clear Overwrite... - - + + Open in Explorer - + Restore Backup - + Remove Backup... - - + + Change Categories - - + + Primary Category - + Rename Separator... - + Remove Separator... - + Select Color... - + Reset Color - + Change versioning scheme - + Force-check updates - + Un-ignore update - + Ignore update - - + + Enable selected - - + + Disable selected - + Rename Mod... - + Reinstall Mod - + Remove Mod... - + Un-Endorse - + Won't endorse - + Endorsement state unknown - + Start tracking - + Stop tracking - + Tracked state unknown - + Ignore missing data - + Mark as converted/working - + Visit on Nexus - + Visit on %1 - + Information... - - + + Exception: - - + + Unknown exception - - <All> - - - - - <Multiple> - - - - + %1 more - + Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin. @@ -3112,12 +3206,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -3125,349 +3219,330 @@ You can also use online editors and converters instead. - - Restarting MO + + Restart Mod Organizer - - Changing the managed game directory requires restarting MO. -Any pending downloads will be paused. - -Click OK to restart MO now. + + Mod Organizer must restart to finish configuration changes + + + + + Restart + + + + + Continue - + + Some things might be weird. + + + + Can't change download directory while downloads are in progress! - + failed to write to file %1 - + %1 written - + Enter Name - - Please enter a name for the executable + + Enter a name for the executable - + Not an executable - + This is not a recognized executable. - + Replace file? - + There already is a hidden version of this file. Replace it? - + File operation failed - + Failed to remove "%1". Maybe you lack the required file permissions? - + There already is a visible version of this file. Replace it? - - + + Set Priority - + Set the priority of the selected plugins - + Update available - - Open/Execute + + &Execute - - Add as Executable + + &Open + + + + + Open with &VFS - + + &Add as Executable + + + + Preview - + Un-Hide - + Hide - + Write To File... - + Do you want to endorse Mod Organizer on %1 now? - + Abstain from Endorsing Mod Organizer - + Are you sure you want to abstain from endorsing Mod Organizer 2? You will have to visit the mod page on the %1 Nexus site to change your mind. - + Thank you for endorsing MO2! :) - + Please reconsider endorsing MO2 on Nexus! - + Thank you! - + Thank you for your endorsement! - + Mod ID %1 no longer seems to be available on Nexus. - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - - + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + This will restart MO, continue? - - Edit Categories... - - - - - Deselect filter + + <Multiple> - + Remove '%1' from the toolbar - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - - depends on missing "%1" - - - - - incompatible with "%1" - - - - - Please wait while LOOT is running - - - - - loot failed. Exit code was: %1 - - - - - failed to start loot - - - - - failed to run loot: %1 - - - - - Errors occurred - - - - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of mod list created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - + Set the priority of the selected mods @@ -3484,101 +3559,100 @@ You will have to visit the mod page on the %1 Nexus site to change your mind. ModInfo - + Plugins - + Textures - + Meshes - + Bethesda Archive - + UI Changes - + Sound Effects - + Scripts - + Script Extender - + Script Extender Files - + SkyProc Tools - + MCM Data - + INI files - + ModGroup files - + invalid content type: %1 - + invalid mod index: %1 - + remove: invalid mod index %1 - + All of your mods have been checked recently. We restrict update checks to help preserve your available API requests. - + You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods. - You have mods that haven't been checked within 30 days using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods. ModInfoBackup - + This is the backup of a mod @@ -3638,27 +3712,32 @@ You will have to visit the mod page on the %1 Nexus site to change your mind. + Open with Preview Plugin + + + + Open in Explorer - + 0x0 - - + + Optional ESPs - + List of esps, esms, and esls that can not be loaded by the game. - + List of esps, esms, and esls contained in this plugin that currently can not be loaded by the game. They will not even appear in the esp-list in the main MO-window. They usually contain optional functionality, see the readme. @@ -3666,135 +3745,132 @@ Most mods do not have optional esps, so chances are good you are looking at an e - + Move a file to the data directory. - + This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes "available", it will not necessarily be loaded! That is configured in the main window of MO. - This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes "available", it will not necessarily be loaded! That is configured in the main window of omo. - + Make the selected mod in the right list unavailable. - Make the selected mod in the lower list unavailable. - + The selected esp (in the right list) will be pushed into a subdirectory of the mod and will thus become "invisible" to the game. It can then no longer be activated. - The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become "invisible" to the game. It can then no longer be activated. - + Available ESPs - + ESPs in the data directory and thus visible to the game. - + <html><head/><body><p>These are the mod files that are in the (virtual) data directory of your game and will thus be selectable in the esp list in the main window.</p></body></html> - + Conflicts - + General - + The following conflicted files are provided by this mod - + The following conflicted files are provided by other mods - + The following files have no conflicts - + Advanced - + Whether files that have no conflicts should be visible in the list - + Show files that have no conflicts - + Shows all mods overwriting or being overwritten by this mod - + Show all conflicting mods - + Shows only the nearest conflicting mods, in order of priority - + Show nearest conflicting mod - + Filter - + Categories - + Primary Category - + Nexus Info - + Mod ID - + Mod ID for this mod on Nexus. - + <!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; } @@ -3803,22 +3879,22 @@ p, li { white-space: pre-wrap; } - + Source Game - + Source game for this mod. - + <html><head/><body><p>Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.</p></body></html> - + <!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; } @@ -3827,83 +3903,83 @@ p, li { white-space: pre-wrap; } - + Version - - + + Refresh - + Refresh all information from Nexus. - - + + Open in Browser - + Endorse - + Track - + about:blank - + Use Custom URL - + Notes - - - + + + Enter comments about the mod here. These are displayed in the notes column of the mod list. - - - + + + Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column. - + Filetree - + Open Mod in Explorer - + A directory view of this mod - + <!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; } @@ -3913,17 +3989,17 @@ p, li { white-space: pre-wrap; } - + Previous - + Next - + Close @@ -3954,7 +4030,7 @@ p, li { white-space: pre-wrap; } ModInfoOverwrite - + This pseudo mod contains files from the virtual data tree that got modified (i.e. by the construction kit) @@ -3983,318 +4059,328 @@ p, li { white-space: pre-wrap; } ModList - + Game Plugins (ESP/ESM/ESL) - + Interface - + Meshes - + Bethesda Archive - + Scripts (Papyrus) - + Script Extender Plugin - + SkyProc Patcher - + Sound or Music - + Textures - + MCM Configuration - + INI files - + ModGroup files - + This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit) - + Backup - + Separator - + No valid game data - + Not endorsed yet - + + <br>This mod is for a different game, make sure it's compatible or it could cause crashes. + + + + + Mod is being tracked on the website + + + + Overwrites loose files - + Overwritten loose files - + Loose files Overwrites & Overwritten - + Redundant - + Overwrites an archive with loose files - + Archive is overwritten by loose files - + Overwrites another archive file - + Overwritten by another archive file - + Archive files overwrites & overwritten - - <br>This mod is for a different game, make sure it's compatible or it could cause crashes. - - - - - Mod is being tracked on the website - - - - + Non-MO - + invalid - + installed version: "%1", newest version: "%2" - + The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade". - + This file has been marked as "Old". There is most likely an updated version of this file available. - + This file has been marked as "Deleted"! You may want to check for an update or remove the nexus ID from this mod! - + %1 minute(s) and %2 second(s) - + This mod will be available to check in %2. - + Categories: <br> - + Invalid name - + Name is already in use by another mod - + drag&drop failed: %1 - + Confirm - + Are you sure you want to remove "%1"? - + + Conflicts + + + + Flags - + Content - + Mod Name - + Version - + Priority - + Category - + Source Game - + Nexus ID - + Installation - + Notes - - + + unknown - + Name of your mods - + Version of the mod (if available) - + Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority. - + Category of the mod. - + The source game which was the origin of this mod. - + Id of the mod as used on Nexus. - - Emblemes to highlight things that might require attention. + + Indicators of file conflicts between mods. + + + + + Emblems to highlight things that might require attention. - + Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr><tr><td><img src=":/MO/gui/content/inifile" width=32/></td><td>INI files</td></tr><tr><td><img src=":/MO/gui/content/modgroup" width=32/></td><td>ModGroup files</td></tr></table> - + Time this mod was installed - + User notes about the mod @@ -4302,7 +4388,7 @@ p, li { white-space: pre-wrap; } ModListSortProxy - + Drag&Drop is only supported when sorting by priority @@ -4345,31 +4431,31 @@ p, li { white-space: pre-wrap; } NexusInterface - Failed to guess mod id for "%1", please pick the correct one + Please pick the mod ID for "%1" - + You must authorize MO2 in Settings -> Nexus to use the Nexus API. - + You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds. - + Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer. - + empty response - + invalid response @@ -4452,7 +4538,7 @@ p, li { white-space: pre-wrap; } NoConflictListModel - + File @@ -4460,212 +4546,207 @@ p, li { white-space: pre-wrap; } OrganizerCore - - Failed to write settings - - - - + File is write protected - + Invalid file format (probably a bug) - + Unknown error %1 - + + Failed to write settings + + + + An error occurred trying to write back MO settings to %1: %2 - - + + Download started - + Download failed - - - - + + + + Installation cancelled - - + + Another installation is currently in progress. - - + + Installation successful - - + + Configure Mod - - + + This mod contains ini tweaks. Do you want to configure them now? - - + + mod not found: %1 - - + + The mod was not installed completely. - + file not found: %1 - + failed to generate preview for %1 - + Sorry - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + File '%1' not found. - + Failed to generate preview for %1 - - Error - - - - - No profile set - - - - + Failed to refresh list of esps: %1 - + Multiple esps/esls activated, please check that they don't conflict. - + You need to be logged in with Nexus - + Download? - + A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue? - - + + failed to update mod list: %1 - - + + login successful - + Login failed - + Login failed, try again? - + login failed: %1. Download will not be associated with an account - + login failed: %1 - + login failed: %1. You need to log-in with Nexus to update MO. - + MO1 "Script Extender" load mechanism has left hook.dll in your game folder - - + + Description missing - + <a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn. - + failed to save load order: %1 - + + Error + + + + The designated write target "%1" is not enabled. @@ -4673,12 +4754,12 @@ Continue? OverwriteConflictListModel - + File - + Overwritten Mods @@ -4721,43 +4802,43 @@ Continue? - + mod not found: %1 - + Failed to delete "%1" - - - - + + + + Confirm - - + + Are you sure you want to delete "%1"? - - + + Are you sure you want to delete the selected files? - - + + New Folder - + Failed to create "%1" @@ -4765,12 +4846,12 @@ Continue? OverwrittenConflictListModel - + File - + Providing Mod @@ -4778,18 +4859,18 @@ Continue? PluginContainer - + Some plugins could not be loaded - - + + Description missing - + The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version: @@ -4797,152 +4878,176 @@ Continue? PluginList - + Name - + Priority - + Mod Index - + Flags - - + + unknown - - Name of your mods + + Name of the plugin + + + + + Emblems to highlight things that might require attention. - - Load priority of your mod. The higher, the more "important" it is and thus overwrites data from plugins with lower priority. + + Load priority of plugins. The higher, the more "important" it is and thus overwrites data from plugins with lower priority. - - The modindex determines the formids of objects originating from this mods. + + Determines the formids of objects originating from this mods. - + failed to update esp info for file %1 (source id: %2), error: %3 - + Plugin not found: %1 - - + + Confirm - + Really enable all plugins? - + Really disable all plugins? - + The file containing locked plugin indices is broken - - - <b>Origin</b>: %1 + + Origin - - <br><b><i>This plugin can't be disabled (enforced by the game).</i></b> + + This plugin can't be disabled (enforced by the game). - + Author - + Description - + Missing Masters - + Enabled Masters - + Loads Archives - + There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin) - + Loads INI settings - + There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts. - + This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space. - - failed to restore load order for %1 + + Incompatible with %1 - - - PluginListSortProxy - - Drag&Drop is only supported when sorting by priority or mod index + + Depends on missing %1 - - - PreviewDialog - - Preview + + Warning + + + + + Error + + + + + failed to restore load order for %1 + + + + + PluginListSortProxy + + + Drag&Drop is only supported when sorting by priority or mod index + + + + + PreviewDialog + + + Preview @@ -4965,11 +5070,6 @@ Continue? 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;">Click a notification above to get more details...</p></body></html> - <!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="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;"><br /></p></body></html> @@ -5326,33 +5426,53 @@ p, li { white-space: pre-wrap; } - QApplication + QObject - + + + + INI file is read-only - - - Mod Organizer is attempting to write to "%1" which is currently set to read-only. Clear the read-only flag to allow the write? + + + Mod Organizer is attempting to write to "%1" which is currently set to read-only. - - File is read-only + + + Clear the read-only flag + + + + + + Allow the write once + + + + + + The file will be set to read-only again. + + + + + + Skip this file - - - QObject - - - - + + + + + Error @@ -5379,8 +5499,8 @@ p, li { white-space: pre-wrap; } - - failed to open temporary file + + Failed to save '{}', could not create a temporary file: {} (error {}) @@ -5399,52 +5519,73 @@ p, li { white-space: pre-wrap; } - - + + Error %1 + + + + + failed to create directory "%1" - - + + failed to copy "%1" to "%2" - + %1 MB - + %1 GB - + %1 TB - + %1 KB - + %1 B/s - + %1 KB/s - + %1 MB/s + + + Regular + + + + + Premium + + + + + + None + + Failed to save custom categories @@ -5454,15 +5595,95 @@ p, li { white-space: pre-wrap; } - + invalid category index: %1 - + + <Active> + + + + + <Update available> + + + + + <Has category> + + + + + <Conflicted> + + + + + <Endorsed> + + + + + <Has backup> + + + + + <Managed> + + + + + <Has valid game data> + + + + + <Has Nexus ID> + + + + + <Tracked on Nexus> + + + + invalid category id: %1 + + + Is overwritten (loose files) + + + + + Is overwriting (loose files) + + + + + Is overwritten (archives) + + + + + Is overwriting (archives) + + + + + Mod contains selected plugin + + + + + Plugin is contained in selected mod + + invalid field name "%1" @@ -5504,34 +5725,40 @@ p, li { white-space: pre-wrap; } - + The hidden file "%1" already exists. Replace it? - + The visible file "%1" already exists. Replace it? - + Replace file? - - + + File operation failed - - Failed to remove "%1". Maybe you lack the required file permissions? + + Failed to remove "%1": %2 - - failed to rename %1 to %2 + + Failed to rename file: %1. + +Source: +"%2" + +Destination: +"%3" @@ -5670,122 +5897,214 @@ If the folder was still in use, restart MO and try again. - + failed to create %1 - + Data directory created - + New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings. - - + + General messages + + + + + Plugins + + + + + No messages. + + + + + Incompatibilities + + + + + Missing masters + + + + + Verified clean by %1 + + + + + %1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es). + + + + + + + Warning + + + + + failed to run loot: %1 + + + + + Checking masterlist existence + + + + + Updating masterlist + + + + + Loading lists + + + + + Reading plugins + + + + + Sorting plugins + + + + + Writing loadorder.txt + + + + + Parsing loot messages + + + + + Done + + + + + Failed to create "%1". Your user account probably lacks permission. - + Plugin to handle %1 no longer installed - - - + + + The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2's VFS and will not run correctly. - + Could not use configuration settings for game "%1", path "%2". - - - + + + Please select the installation of %1 to manage - - - + + + Please select the game to manage - + Canceled finding %1 in "%2". - + Canceled finding game in "%1". - + %1 not identified in "%2". The directory is required to contain the game binary. - + No game identified in "%1". The directory is required to contain the game binary.<br><br><b>These are the games supported by Mod Organizer:</b><ul>%2</ul> - + Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!) - + failed to start shortcut: %1 - + failed to start application: %1 - - + + Mod Organizer - + An instance of Mod Organizer is already running - + Failed to set up instance - + + <Unmanaged> + + + + Please use "Help" from the toolbar to get usage instructions to all elements - - + + <Manage...> - + failed to parse profile %1: %2 @@ -5821,512 +6140,586 @@ If the folder was still in use, restart MO and try again. - - This error typically happens because an antivirus has deleted critical files from Mod Organizer's installation folder or has made them generally inaccessible. Add an exclusion for Mod Organizer's installation folder in your antivirus, reinstall Mod Organizer and try again. + + Connecting to Nexus... - - This error typically happens because an antivirus is preventing Mod Organizer from starting programs. Add an exclusion for Mod Organizer's installation folder in your antivirus and try again. + + Waiting for Nexus... - - The file '%1' does not exist. + + Opened Nexus in browser. - - - - - Cannot start Steam + + Switch to your browser and accept the request. - - The path to the Steam executable cannot be found. You might try reinstalling Steam. + + Finished. - - - - Continue without starting Steam + + No answer from Nexus. - - - The program may fail to launch. + + + A firewall might be blocking Mod Organizer. - - Cannot launch program + + Nexus closed the connection. - - - - Cannot start %1 + + Cancelled. - - Cannot launch helper + + Failed to request %1 - - This program is requesting to run as administrator but Mod Organizer itself is not running as administrator. Running programs as administrator is typically unnecessary as long as the game and Mod Organizer have been installed outside "Program Files". - -You can restart Mod Organizer as administrator and try launching the program again. + + + Cancelled - - - Restart Mod Organizer as administrator + + Internal error - - - You must allow "helper.exe" to make changes to the system. + + HTTP code %1 - - Launch Steam + + Invalid JSON - - This program requires Steam + + Bad response - - Mod Organizer has detected that this program likely requires Steam to be running to function properly. + + API key is empty - - Start Steam + + SSL error - - - The program might fail to run. + + Timed out - - Steam is running as administrator + + One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is likely to be incompatible with MO2's virtual filesystem. - - Running Steam as administrator is typically unnecessary and can cause problems when Mod Organizer itself is not running as administrator. - -You can restart Mod Organizer as administrator and try launching the program again. + + failed to initialize plugin %1: %2 - - - - Continue + + Plugin error - - Event Log not running + + It appears the plugin "%1" failed to load last startup and caused MO to crash. Do you want to disable it? +(Please note: If this is the first time you see this message for this plugin you may want to give it another try. The plugin may be able to recover from the problem) - - The Event Log service is not running + + failed to access %1 - - The Windows Event Log service is not running. This can prevent USVFS from running properly and your mods may not be recognized by the program being launched. + + failed to set file time %1 - - - Your mods might not work. + + + + No profile set - - Blacklisted program + + Before you can use ModOrganizer, you need to create at least one profile. ATTENTION: Run the game at least once before creating a profile! - - The program %1 is blacklisted + + + attempt to store setting for unknown plugin "%1" - - The program you are attempting to launch is blacklisted in the virtual filesystem. This will likely prevent it from seeing any mods, INI files or any other virtualized files. + + Failed - - Change the blacklist + + Failed to start the helper application: %1 - - Waiting + + + Debug - - Please press OK once you're logged into steam. + + + Info (recommended) - - One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is incompatible with MO2's VFS system. + + Trace - - Select binary + + Mini (recommended) - - Binary + + Data - - failed to initialize plugin %1: %2 + + Full - - Plugin error + + Confirm? - - It appears the plugin "%1" failed to load last startup and caused MO to crash. Do you want to disable it? -(Please note: If this is the first time you see this message for this plugin you may want to give it another try. The plugin may be able to recover from the problem) + + This will reset all the choices you made to dialogs and make them all visible again. Continue? - - failed to access %1 + + Connected. - - failed to set file time %1 + + Not connected. - - Before you can use ModOrganizer, you need to create at least one profile. ATTENTION: Run the game at least once before creating a profile! + + Disconnected. - - - Elevation required + + Checking API key... - - This tracks the number of queued Nexus API requests, as well as the remaining daily and hourly requests. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens. + + Received API key. - - Loading... + + Received user acount information - - &Save + + Linked with Nexus successfully. - - &Word wrap + + Failed to set API key - - &Open in Explorer + + + + + + + + + + + + Cancel - - Regular + + + + Enter API Key Manually - - Premium + + + + Connect to Nexus - - - None + + + + + + N/A - - - Connecting to Nexus... + + Failed to create "%1", you may not have the necessary permissions. Path remains unchanged. - - Waiting for Nexus... + + Select base directory - - Opened Nexus in browser. -Switch to your browser and accept the request. + + Select download directory - - - Finished. + + Select mod directory - - No answer from Nexus. -A firewall might be blocking Mod Organizer. + + Select cache directory - - Nexus closed the connection. + + Select profiles directory - - Cancelled. + + Select overwrite directory - - Invalid JSON + + Select game executable - - Bad response + + Executables Blacklist - - There was a timeout during the request + + Enter one executable per line to be blacklisted from the virtual file system. +Mods and other virtualized files will not be visible to these executables and +any executables launched by them. + +Example: + Chrome.exe + Firefox.exe - - Cancelled + + + + Restart Mod Organizer - - Failed to request %1 + + Geometries will be reset to their default values. - - - attempt to store setting for unknown plugin "%1" + + This error typically happens because an antivirus has deleted critical files from Mod Organizer's installation folder or has made them generally inaccessible. Add an exclusion for Mod Organizer's installation folder in your antivirus, reinstall Mod Organizer and try again. - - Failed + + This error typically happens because an antivirus is preventing Mod Organizer from starting programs. Add an exclusion for Mod Organizer's installation folder in your antivirus and try again. - - Failed to start the helper application + + The file '%1' does not exist. - - Debug + + + + + Cannot start Steam - - Info (recommended) + + The path to the Steam executable cannot be found. You might try reinstalling Steam. - - Warning + + + + Continue without starting Steam - - Mini (recommended) + + + The program may fail to launch. - - Data + + Cannot launch program - - Full + + + + Cannot start %1 - - Confirm? + + Cannot launch helper + + + + + + Elevation required + + + + + This program is requesting to run as administrator but Mod Organizer itself is not running as administrator. Running programs as administrator is typically unnecessary as long as the game and Mod Organizer have been installed outside "Program Files". + +You can restart Mod Organizer as administrator and try launching the program again. + + + + + + Restart Mod Organizer as administrator + + + + + + You must allow "helper.exe" to make changes to the system. + + + + + Launch Steam + + + + + This program requires Steam + + + + + Mod Organizer has detected that this program likely requires Steam to be running to function properly. + + + + + Start Steam + + + + + + The program might fail to run. + + + + + Steam is running as administrator + + + + + Running Steam as administrator is typically unnecessary and can cause problems when Mod Organizer itself is not running as administrator. + +You can restart Mod Organizer as administrator and try launching the program again. + + + + + + + Continue + + + + + Event Log not running + + + + + The Event Log service is not running - - This will reset all the choices you made to dialogs and make them all visible again. Continue? + + The Windows Event Log service is not running. This can prevent USVFS from running properly and your mods may not be recognized by the program being launched. - - Disconnected. + + + Your mods might not work. - - Checking API key... + + Blacklisted program - - Received API key. + + The program %1 is blacklisted - - Linked with Nexus successfully. + + The program you are attempting to launch is blacklisted in the virtual filesystem. This will likely prevent it from seeing any mods, INI files or any other virtualized files. - - - - - - - - - - Cancel + + Change the blacklist - - - - Enter API Key Manually + + Waiting - - - - Connect to Nexus + + Please press OK once you're logged into steam. - - - - - - N/A + + Select binary - - Failed to create "%1", you may not have the necessary permissions. Path remains unchanged. + + Binary - - Select base directory + + This tracks the number of queued Nexus API requests, as well as the remaining daily and hourly requests. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens. - - Select download directory + + Loading... - - Select mod directory + + &Save - - Select cache directory + + &Word wrap - - Select profiles directory + + &Open in Explorer - - Select overwrite directory + + Mod Organizer is locked while the application is running. - - Select game executable + + Mod Organizer is currently running an application. - - Executables Blacklist + + The application must run to completion because its output is required. - - Enter one executable per line to be blacklisted from the virtual file system. -Mods and other virtualized files will not be visible to these executables and -any executables launched by them. - -Example: - Chrome.exe - Firefox.exe + + Mod Organizer is waiting on application to close before exiting. - - Restart Mod Organizer? + + Unlock - - In order to reset the geometry, Mod Organizer must be restarted. -Restart now? + + Exit Now @@ -6445,58 +6838,58 @@ Restart now? - + New update available (%1) - + Do you want to install update? All your mods and setup will be left untouched. Select Show Details option to see the full change-log. - + Install - + Download failed - + Failed to find correct download, please try again later. - + Update - + Download in progress - + Download failed: %1 - + Failed to install update: %1 - - Failed to start %1 + + Failed to start %1: %2 - + Error @@ -6514,473 +6907,488 @@ Select Show Details option to see the full change-log. - + + User Interface + + + + Language - - The display language + + Style - - <!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;">The display language. This will only displaye languages for which you have a translation installed.</span></p></body></html> + + + Visual theme of the user interface. - - Style + + Explore... - - graphical style + + + The language of the user interface. - - graphical style of the MO user interface + + https://www.transifex.com/tannin/mod-organizer/ - - Update to non-stable releases. + + <a href="https://www.transifex.com/tannin/mod-organizer/">Help translate Mod Organizer</a> - - If this is enabled, the integrated update mechanism will notify of all releases, including pre-releases (alphas, betas). Please use this only if you're sufficiently tech-savvy to investigate issues, look for known problems in the issue tracker and create meaningful reports. - If this is enabled, the integrated update mechanism will notify of all releases, including pre-releases (alphas, betas). - -Please use this only if you're sufficiently tech-savvy to investigate issues, look for known problems in the issue tracker and create meaningful reports. - -If you use pre-releases, never contact me directly by e-mail or via private messages! + + + Dialogs will always be centered on the main window, but will remember their size. - - Install Pre-releases (Betas) + + Always center dialogs - - User interface + + Show confirmation when changing instance - - Colors + + Whether double-clicking on a file opens the preview window or launches the program associated with it. This applies to the Data tab as well as the Conflicts and Filetree tabs in the mod info window. - - - When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section. + + Open previews on double-click - - Show mod list separator colors on the scrollbar + + + Reset all choices made in dialogs. - - Plugin is Contained in selected Mod + + Reset Dialog Choices - - Is overwritten (loose files) + + + Modify the categories available to arrange your mods. - - Is overwriting (loose files) + + Configure Mod Categories - - Reset Colors + + Download List - - Mod Contains selected Plugin + + + Show meta information instead of file names in the download list. - - Is overwritten (archive files) + + Show Meta Information - - Is overwriting (archive files) + + + Make the download list more compact. - - - Modify the categories available to arrange your mods. + + Compact List - - Configure Mod Categories + + Colors - - Reset stored information from dialogs. + + + Colors set on separators will also be shown in the mod list scrollbar at the location of the separator. This can be useful for quickly navigating to a specific separator. - - This will make all dialogs show up again where you checked the "Remember selection"-box. + + Show mod list separator colors on the scrollbar - - If checked, the download interface will be more compact. + + + Reset all colors to their default value. - - Compact Download Interface + + Reset Colors - - If checked, the download list will display meta information instead of file names. + + Updates + + + + + + Check for Mod Organizer updates on Github on startup. - - Download Meta Information + + Check for updates + + + + + + Update to non-stable releases. + + + + + Install Pre-releases (Betas) - + Paths - - - - + + + + ... - + Caches - + Overwrite - - + + Directory where downloads are stored. - + Downloads - + Profiles - + Directory where mods are stored. - + Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name). - + Mods - + Managed Game - + Base Directory - + Use %BASE_DIR% to refer to the Base Directory. - + Important: All directories have to be writable! - + Nexus - - Connect to Nexus - - - - - Manually enter the API key and try to login + + Nexus Account - - Enter API Key Manually + + User ID: - - Clear the stored Nexus API key and force reauthorization. + + id - - Disconnect from Nexus + + Name: - - - <html><head/><body><p>By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html> + + name - - Remove cache and cookies. + + Account: - - Clear Cache + + account - - Disable automatic internet features + + Statistics - - Reset Dialog Choices + + Daily requests: - - Nexus Account + + daily requests - - User ID: + + Hourly requests: - - id + + hourly requests - - Name: + + Nexus Connection - - name + + Connect to Nexus - - Account: + + Manually enter the API key and try to login - - account + + Enter API Key Manually - - Statistics + + Clear the stored Nexus API key and force reauthorization. - - Daily requests: + + Disconnect from Nexus - - daily requests + + Disable automatic internet features - - Hourly requests: + + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - - hourly requests + + Offline Mode - - Nexus Connection + + Use a proxy for network connections. - - Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) + + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - - Offline Mode + + Use HTTP Proxy (Uses System Settings) - - Use a proxy for network connections. + + Endorsement Integration - - Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. + + + <html><head/><body><p>By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html> - - Use HTTP Proxy (Uses System Settings) + + Hide API Request Counter - - Endorsement Integration + + Associate with "Download with manager" links - - Hide API Request Counter + + Remove cache and cookies. - - Associate with "Download with manager" links + + Clear Cache - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + Username - + <html><head/><body><p>If you save your steam user ID and password here, they will be used when logging into steam.</p></body></html> - + Password - + Plugins - + Author: - + Version: - + Description: - + Key - + Value - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!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; } @@ -6996,17 +7404,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -7017,28 +7425,28 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -7046,66 +7454,66 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness. - + <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> - + Enable parsing of Archives (Experimental Feature) - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -7114,81 +7522,81 @@ programs you are intentionally running. - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - + Configure Executables Blacklist - - + + Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. - + Reset Window Geometries - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - - Max Dumps To Keep + + Hint: right click link and copy link location - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + + + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> + and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. + Sending logs and/or crash dumps to the developers can help investigate issues. + It is recommended to compress large log and dmp files before sending. + - - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - Set "Crash Dumps" above to None to disable crash dump collection. - + + Log Level - - Hint: right click link and copy link location + + Decides the amount of data printed to "ModOrganizer.log" - + - Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> - and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. - Sending logs and/or crash dumps to the developers can help investigate issues. - It is recommended to compress large log and dmp files before sending. - + Decides the amount of data printed to "ModOrganizer.log". + "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. + - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -7199,41 +7607,35 @@ programs you are intentionally running. - - Log Level + + Max Dumps To Keep - - Decides the amount of data printed to "ModOrganizer.log" + + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + - Decides the amount of data printed to "ModOrganizer.log". - "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + Set "Crash Dumps" above to None to disable crash dump collection. - - Restart Mod Organizer? + + LOOT Log Level - - In order to finish configuration changes, MO must be restarted. -Restart it now? - - - - + Confirm - + Changing the mod directory affects all your profiles! Mods not present (or named differently) in the new location will be disabled in all profiles. There is no way to undo this unless you backed up your profiles manually. Proceed? @@ -7241,22 +7643,22 @@ Restart it now? SingleInstance - + SHM error: %1 - + failed to connect to running instance: %1 - + failed to communicate with running instance: %1 - + failed to receive data from secondary instance: %1 @@ -7441,7 +7843,7 @@ On Windows XP: UsvfsConnector - + Preparing vfs @@ -7449,41 +7851,29 @@ On Windows XP: ValidationProgressDialog - + Validating Nexus Connection - - Hide - - - - - WaitingOnCloseDialog - - - Waiting for virtualized processes - - - - - This dialog should disappear automatically if the application/game is done. + + + Connecting to Nexus... - - Virtualized processes are still running, it is prefered to keep MO running until they are finished. + + Cancel - - Close Now + + Hide - - Cancel + + Trying again... @@ -7801,7 +8191,6 @@ Please open the "Nexus"-tab Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile. - Use this interface to obtain an API key from NexusMods.This is used for all API connections - downloads, updatesetc. MO2 uses the Windows Credential Manager to storethis data securely. If the SSO page on Nexus is failing,use the manual entry and copy the API key from your profile. -- cgit v1.3.1