From 5e528fb4cf16ae208944d15d568c9140e3d741e4 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 18 Jul 2020 02:24:44 -0400 Subject: new CommandLine class implemented crashdump as a command, fixed dump_running_process.bat to use it attach to console if present instead of always create one --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e95179b8..73254574 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ endif() add_filter(NAME src/application GROUPS iuserinterface + commandline main moapplication moshortcut -- cgit v1.3.1 From 5513b273c679b9edda951f418ef013a762ca587a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 23 Jul 2020 04:53:20 -0400 Subject: added empty instance manager dialog --- src/CMakeLists.txt | 6 +- src/instancemanager.cpp | 30 +- src/instancemanager.h | 9 +- src/instancemanagerdialog.cpp | 36 ++ src/instancemanagerdialog.h | 20 + src/instancemanagerdialog.ui | 378 ++++++++++++++++++ src/organizer_en.ts | 885 +++++++++++++++++++++++------------------- 7 files changed, 940 insertions(+), 424 deletions(-) create mode 100644 src/instancemanagerdialog.cpp create mode 100644 src/instancemanagerdialog.h create mode 100644 src/instancemanagerdialog.ui (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 73254574..1f048be8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,6 @@ add_filter(NAME src/core GROUPS categories archivefiletree installationmanager - instancemanager loadmechanism nexusinterface nxmaccessmanager @@ -84,6 +83,11 @@ add_filter(NAME src/executables GROUPS editexecutablesdialog ) +add_filter(NAME src/instances GROUPS + instancemanager + instancemanagerdialog +) + add_filter(NAME src/loot GROUPS loot lootdialog diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 46936444..98edba47 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -182,14 +182,14 @@ QString InstanceManager::manageInstances(const QStringList &instanceList) const } if (selection.exec() == QDialog::Rejected) { - return (chooseInstance(instances())); + return (chooseInstance(instanceNames())); } else { QString choice = selection.getChoiceData().toString(); deleteLocalInstance(choice); } - return(manageInstances(instances())); + return(manageInstances(instanceNames())); } QString InstanceManager::queryInstanceName(const QStringList &instanceList) const @@ -301,7 +301,7 @@ QString InstanceManager::chooseInstance(const QStringList &instanceList) const case Special::Portable: return QString(); case Special::Manage: { - return(manageInstances(instances())); + return(manageInstances(instanceNames())); } default: throw std::runtime_error("invalid selection"); } @@ -319,27 +319,37 @@ QString InstanceManager::instancesPath() const QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } - -QStringList InstanceManager::instances() const +std::vector InstanceManager::instancePaths() const { const std::set ignore = { "cache", "qtwebengine", }; - const auto dirs = QDir(instancesPath()) - .entryList(QDir::Dirs | QDir::NoDotAndDotDot); + const QDir root(instancesPath()); + const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - QStringList list; + std::vector list; for (auto&& d : dirs) { if (!ignore.contains(QFileInfo(d).fileName().toLower())) { - list.push_back(d); + list.push_back(root.filePath(d)); } } return list; } +QStringList InstanceManager::instanceNames() const +{ + QStringList list; + + for (auto&& d : instancePaths()) { + list.push_back(d.dirName()); + } + + return list; +} + bool InstanceManager::isPortablePath(const QString& dataPath) { @@ -402,7 +412,7 @@ QString InstanceManager::determineDataPath() if (!m_overrideInstance && (instanceId.isEmpty() || !QFileInfo::exists(dataPath))) { - instanceId = chooseInstance(instances()); + instanceId = chooseInstance(instanceNames()); setCurrentInstance(instanceId); if (!instanceId.isEmpty()) { dataPath = QDir::fromNativeSeparators( diff --git a/src/instancemanager.h b/src/instancemanager.h index 13754f89..8bbbbee9 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -29,8 +29,8 @@ namespace MOBase { class IPluginGame; } class Settings; class PluginContainer; -class InstanceManager { - +class InstanceManager +{ public: static InstanceManager &instance(); @@ -49,6 +49,9 @@ public: bool allowedToChangeInstance() const; static bool isPortablePath(const QString& dataPath); + QStringList instanceNames() const; + std::vector instancePaths() const; + private: InstanceManager(); @@ -56,8 +59,6 @@ private: QString instancesPath() const; QString instancePath(const QString& instanceName) const; - QStringList instances() const; - bool deleteLocalInstance(const QString &instanceId) const; QString manageInstances(const QStringList &instanceList) const; diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp new file mode 100644 index 00000000..1b482099 --- /dev/null +++ b/src/instancemanagerdialog.cpp @@ -0,0 +1,36 @@ +#include "instancemanagerdialog.h" +#include "ui_instancemanagerdialog.h" +#include "instancemanager.h" + +class InstanceInfo +{ +public: + InstanceInfo(QDir dir) + : m_dir(std::move(dir)) + { + } + + QString name() const + { + return m_dir.dirName(); + } + +private: + QDir m_dir; +}; + + +InstanceManagerDialog::InstanceManagerDialog(QWidget *parent) + : QDialog(parent), ui(new Ui::InstanceManagerDialog) +{ + ui->setupUi(this); + + auto& m = InstanceManager::instance(); + + for (auto&& d : m.instancePaths()) { + InstanceInfo i(d); + ui->list->addItem(i.name()); + } +} + +InstanceManagerDialog::~InstanceManagerDialog() = default; diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h new file mode 100644 index 00000000..ea1cfc48 --- /dev/null +++ b/src/instancemanagerdialog.h @@ -0,0 +1,20 @@ +#ifndef MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED +#define MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED + +#include + +namespace Ui { class InstanceManagerDialog; }; + +class InstanceManagerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit InstanceManagerDialog(QWidget *parent = nullptr); + ~InstanceManagerDialog(); + +private: + std::unique_ptr ui; +}; + +#endif // MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui new file mode 100644 index 00000000..b35d2e58 --- /dev/null +++ b/src/instancemanagerdialog.ui @@ -0,0 +1,378 @@ + + + InstanceManagerDialog + + + + 0 + 0 + 531 + 413 + + + + Instance manager + + + + + + + 0 + + + 0 + + + 0 + + + 5 + + + + + Create new instance + + + + :/MO/gui/add:/MO/gui/add + + + + + + + Create portable instance + + + + :/MO/gui/package:/MO/gui/package + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance"><span style=" text-decoration: underline; color:#0000ff;">What is an instance?</span></a></p></body></html> + + + true + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + instance name + + + Qt::TextBrowserInteraction + + + + + + + Game: + + + + + + + Name: + + + + + + + Location: + + + + + + + Rename + + + + + + + location path + + + Qt::TextBrowserInteraction + + + + + + + Base folder: + + + + + + + base folder path + + + Qt::TextBrowserInteraction + + + + + + + Explore + + + + + + + Explore + + + + + + + game name + + + Qt::TextBrowserInteraction + + + + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Convert to portable + + + + + + + Convert to global + + + + + + + Delete instance + + + + :/MO/gui/remove:/MO/gui/remove + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Switch to this instance + + + + :/MO/gui/next:/MO/gui/next + + + + + + + Cancel + + + true + + + + + + + + + + + + + diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 1ad4650f..c65c946c 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -110,22 +110,22 @@ - + ...and all other contributors! - + Other Supporters && Contributors - + Tannin (Original Creator) - + Close @@ -178,17 +178,17 @@ p, li { white-space: pre-wrap; } AdvancedConflictListModel - + Overwrites - + File - + Overwritten By @@ -1462,54 +1462,54 @@ Right now the only case I know of where this needs to be overwritten is for the FileTreeModel - + Name - + Mod - + Type - + Size - + Date modified - + Directory - - + + Virtual path - + Real path - + From - - + + Also in @@ -1868,6 +1868,85 @@ This is likely due to a corrupted or incompatible download or unrecognized archi + + InstanceManagerDialog + + + Instance manager + + + + + Create new instance + + + + + Create portable instance + + + + + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance"><span style=" text-decoration: underline; color:#0000ff;">What is an instance?</span></a></p></body></html> + + + + + Game: + + + + + Name: + + + + + Location: + + + + + Rename + + + + + Base folder: + + + + + + Explore + + + + + Convert to portable + + + + + Convert to global + + + + + Delete instance + + + + + Switch to this instance + + + + + Cancel + + + ListDialog @@ -1999,12 +2078,12 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MOApplication - + an error occurred: %1 - + an error occurred @@ -2199,7 +2278,7 @@ p, li { white-space: pre-wrap; } - + Create Backup @@ -2353,7 +2432,7 @@ p, li { white-space: pre-wrap; } - + Refresh @@ -2677,7 +2756,7 @@ p, li { white-space: pre-wrap; } - + Endorse Mod Organizer @@ -2755,829 +2834,833 @@ p, li { white-space: pre-wrap; } - + 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 - + <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 - + 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 - + Restore all hidden files in the following mods?<br><ul>%1</ul> - - - + + Are you sure? - + About to restore all hidden files in: - + 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? - + + No valid Web Page for this mod + + + + <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. - + 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... - - + + Ignore missing data - - + + Mark as converted/working - - + + Visit on Nexus - - + + Visit on %1 - - + + 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... - + Restore hidden files - + Un-Endorse - + Won't endorse - + Endorsement state unknown - + Start tracking - + Stop tracking - + Tracked state unknown - + Information... - - + + Exception: - - + + Unknown exception - + %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. @@ -3585,12 +3668,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -3598,242 +3681,227 @@ You can also use online editors and converters instead. - + Restart Mod Organizer - + 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! - - + + Set Priority - + Set the priority of the selected plugins - + Update available - + 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? - - - - + <Multiple> - + Remove '%1' from the toolbar - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - - Sorting plugins - - - - - Are you sure you want to sort your plugins list? - - - - + 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 @@ -4701,7 +4769,7 @@ p, li { white-space: pre-wrap; } NoConflictListModel - + File @@ -4709,211 +4777,211 @@ p, li { white-space: pre-wrap; } OrganizerCore - + 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 - - + + Extraction cancelled - - + + The installation was cancelled while extracting files. If this was prior to a FOMOD setup, this warning may be ignored. However, if this was during installation, the mod will likely be missing files. - + 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 - + 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. @@ -4921,12 +4989,12 @@ Continue? OverwriteConflictListModel - + File - + Overwritten Mods @@ -5026,12 +5094,12 @@ Continue? OverwrittenConflictListModel - + File - + Providing Mod @@ -5653,9 +5721,10 @@ p, li { white-space: pre-wrap; } + + - - + @@ -5972,137 +6041,134 @@ Destination: - - Deleting folder - - - - - I'm about to delete the following folder: "%1". Proceed? + + Deleting instance folder - - Choose Instance to Delete + + This will delete the instance folder. + The folder will be deleted: "%1" - - Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched. + + Move the folder to the recycle bin - - Are you sure? + + Delete the folder permanently - - Are you really sure you want to delete the Instance "%1" with all its files? + + Could not delete instance folder "%1". + +%2 - - Failed to delete Instance + + Select an instance to delete - - Could not delete Instance "%1". -If the folder was still in use, restart MO and try again. + + Deleting an instance will delete all the mods, downloads, profiles (including profile-specific saves) and anything in the overwrite folder.<br><br>Custom paths outside of the instance folder will not be deleted. - + Enter a Name for the new Instance - + Enter a new name or select one from the suggested list: (This is just a name for the Instance and can be whatever you wish, the actual game selection will happen on the next screen regardless of chosen name) - - + + Canceled - + Invalid instance name - + The instance name "%1" is invalid. Use the name "%2" instead? - + The instance "%1" already exists. - + Please choose a different instance name, like: "%1 1" . - + Choose Instance - + Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder). - + New - + Create a new instance. - + Portable - + Use MO folder for data. - + Manage Instances - + Delete an Instance. - + 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. @@ -6194,112 +6260,112 @@ If the folder was still in use, restart MO and try again. - - + + Failed to create "%1". Your user account probably lacks permission. - + Plugin to handle %1 no longer installed. An antivirus might have deleted files. 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 @@ -6427,7 +6493,7 @@ If the folder was still in use, restart MO and try again. - + 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. @@ -6590,6 +6656,7 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl + @@ -7903,22 +7970,22 @@ programs you are intentionally running. 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 -- cgit v1.3.1 From 60b59ddf097fffa846a4d28e0d9256630da5149c Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 23 Jul 2020 07:32:06 -0400 Subject: started create instance dialog allow for multiple instances of Settings fill in information in instance manager --- src/CMakeLists.txt | 1 + src/createinstancedialog.cpp | 176 ++++++++++++ src/createinstancedialog.h | 30 ++ src/createinstancedialog.ui | 655 ++++++++++++++++++++++++++++++++++++++++++ src/instancemanager.h | 2 +- src/instancemanagerdialog.cpp | 102 ++++++- src/instancemanagerdialog.h | 16 +- src/instancemanagerdialog.ui | 116 ++++---- src/main.cpp | 9 + src/settings.cpp | 19 +- src/settings.h | 1 + 11 files changed, 1064 insertions(+), 63 deletions(-) create mode 100644 src/createinstancedialog.cpp create mode 100644 src/createinstancedialog.h create mode 100644 src/createinstancedialog.ui (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f048be8..af350584 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -84,6 +84,7 @@ add_filter(NAME src/executables GROUPS ) add_filter(NAME src/instances GROUPS + createinstancedialog instancemanager instancemanagerdialog ) diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp new file mode 100644 index 00000000..d43fba1c --- /dev/null +++ b/src/createinstancedialog.cpp @@ -0,0 +1,176 @@ +#include "createinstancedialog.h" +#include "ui_createinstancedialog.h" +#include "instancemanager.h" + +namespace cid +{ + +class Page +{ +public: + Page(CreateInstanceDialog& dlg, std::size_t i) + : ui(dlg.getUI()), m_dlg(dlg), m_index(i) + { + } + + virtual bool ready() const + { + return true; + } + + void next() + { + m_dlg.next(); + } + +protected: + Ui::CreateInstanceDialog* ui; + +private: + CreateInstanceDialog& m_dlg; + std::size_t m_index; +}; + +class TypePage : public Page +{ +public: + TypePage(CreateInstanceDialog& dlg, std::size_t i) + : Page(dlg, i) + { + ui->createGlobal->setDescription( + ui->createGlobal->description() + .arg(InstanceManager::instance().instancesPath())); + + ui->createPortable->setDescription( + ui->createPortable->description() + .arg(qApp->applicationDirPath())); + + QObject::connect( + ui->createGlobal, &QAbstractButton::clicked, [&]{ global(); }); + + QObject::connect( + ui->createPortable, &QAbstractButton::clicked, [&]{ portable(); }); + } + + bool ready() const override + { + return m_global.has_value(); + } + + void global() + { + m_global = true; + + ui->createGlobal->setChecked(true); + ui->createPortable->setChecked(false); + + next(); + } + + void portable() + { + m_global = false; + + ui->createGlobal->setChecked(false); + ui->createPortable->setChecked(true); + + next(); + } + +private: + std::optional m_global; +}; + + +class GamePage : public Page +{ +public: + GamePage(CreateInstanceDialog& dlg, std::size_t i) + : Page(dlg, i) + { + } +}; + + +class NamePage : public Page +{ +public: + NamePage(CreateInstanceDialog& dlg, std::size_t i) + : Page(dlg, i) + { + } +}; + + +class PathsPage : public Page +{ +public: + PathsPage(CreateInstanceDialog& dlg, std::size_t i) + : Page(dlg, i) + { + } +}; + +} // namespace + + +CreateInstanceDialog::CreateInstanceDialog(QWidget *parent) + : QDialog(parent), ui(new Ui::CreateInstanceDialog) +{ + using namespace cid; + + ui->setupUi(this); + + m_pages.push_back(std::make_unique(*this, 0)); + m_pages.push_back(std::make_unique(*this, 1)); + m_pages.push_back(std::make_unique(*this, 2)); + m_pages.push_back(std::make_unique(*this, 3)); + + ui->pages->setCurrentIndex(0); + + updateNavigationButtons(); + + connect(ui->next, &QPushButton::clicked, [&]{ next(); }); + connect(ui->back, &QPushButton::clicked, [&]{ back(); }); + + // + //SelectionDialog games(tr("Select a game to manage.")); + // + //for (auto* game : m_pc.plugins()) { + // if (game->isInstalled()) { + // games.addChoice(game->gameName(), game->gameDirectory().path(), {}); + // } else { + // games.addChoice(game->gameName(), "", {}); + // } + //} + // + //games.exec(); +} + +CreateInstanceDialog::~CreateInstanceDialog() = default; + +Ui::CreateInstanceDialog* CreateInstanceDialog::getUI() +{ + return ui.get(); +} + +void CreateInstanceDialog::next() +{ + ui->pages->setCurrentIndex(ui->pages->currentIndex() + 1); + updateNavigationButtons(); +} + +void CreateInstanceDialog::back() +{ + ui->pages->setCurrentIndex(ui->pages->currentIndex() - 1); + updateNavigationButtons(); +} + +void CreateInstanceDialog::updateNavigationButtons() +{ + const auto i = ui->pages->currentIndex(); + const auto last = (i == (ui->pages->count() - 1)); + + ui->next->setEnabled(m_pages[i]->ready() && !last); + ui->back->setEnabled(i > 0); +} diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h new file mode 100644 index 00000000..03e9de01 --- /dev/null +++ b/src/createinstancedialog.h @@ -0,0 +1,30 @@ +#ifndef MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED +#define MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED + +#include + +namespace Ui { class CreateInstanceDialog; }; +namespace cid { class Page; } + +class CreateInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CreateInstanceDialog(QWidget *parent = nullptr); + + ~CreateInstanceDialog(); + + Ui::CreateInstanceDialog* getUI(); + + void next(); + void back(); + +private: + std::unique_ptr ui; + std::vector> m_pages; + + void updateNavigationButtons(); +}; + +#endif // MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED diff --git a/src/createinstancedialog.ui b/src/createinstancedialog.ui new file mode 100644 index 00000000..bcfddb20 --- /dev/null +++ b/src/createinstancedialog.ui @@ -0,0 +1,655 @@ + + + CreateInstanceDialog + + + + 0 + 0 + 493 + 423 + + + + Creating an instance + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <h2>Creating a new instance</h2> + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + + 0 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <h3>Select the type of instance to create.</h3> + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Create a global instance + + + true + + + Global instances are stored in %1, but some paths can be changed to be on a different drive if necessary. A single installation of Mod Organizer can manage multiple global instances. + + + + + + + Create a portable instance + + + true + + + A portable instance stores everything in Mod Organizer's installation folder, currently %1. There can only be one portable instance per installation of Mod Organizer. + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <h3>Select the game to manage.</h3> + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show all supported games + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <h3>Pick a name for this instance.</h3> + + + + + + + + + + + 0 + + + + + Instance name + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <h3>Select a folder where the data should be stored.</h3> + + + + + + + This includes downloads, mods, profiles and overwrite. If there is enough space on this drive, you should use the default folder. + + + true + + + + + + + + + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + + + + Location + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Overwrite + + + + + + + Mods + + + + + + + Base directory + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Profiles + + + + + + + + + + Downloads + + + + + + + + + + + + + + + + + + + Use <code>%BASE_DIR%</code> to refer to the Base Directory. + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 292 + 20 + + + + + + + + Show advanced options + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + < Back + + + + + + + Next > + + + true + + + + + + + Cancel + + + + + + + + + + + diff --git a/src/instancemanager.h b/src/instancemanager.h index 8bbbbee9..d53f4391 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -49,6 +49,7 @@ public: bool allowedToChangeInstance() const; static bool isPortablePath(const QString& dataPath); + QString instancesPath() const; QStringList instanceNames() const; std::vector instancePaths() const; @@ -56,7 +57,6 @@ private: InstanceManager(); - QString instancesPath() const; QString instancePath(const QString& instanceName) const; bool deleteLocalInstance(const QString &instanceId) const; diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index 1b482099..c7042aa8 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -1,12 +1,19 @@ #include "instancemanagerdialog.h" #include "ui_instancemanagerdialog.h" #include "instancemanager.h" +#include "createinstancedialog.h" +#include "settings.h" +#include "selectiondialog.h" +#include "plugincontainer.h" +#include "shared/appconfig.h" +#include class InstanceInfo { public: - InstanceInfo(QDir dir) - : m_dir(std::move(dir)) + InstanceInfo(QDir dir) : + m_dir(std::move(dir)), + m_settings(dir.filePath(QString::fromStdWString(AppConfig::iniFileName()))) { } @@ -15,22 +22,105 @@ public: return m_dir.dirName(); } + QString gameName() const + { + if (auto n=m_settings.game().name()) { + if (auto e=m_settings.game().edition()) { + if (!e->isEmpty()) { + return *n + " (" + *e + ")"; + } + } + + return *n; + } else { + return {}; + } + } + + QString gamePath() const + { + if (auto n=m_settings.game().directory()) { + return *n; + } else { + return {}; + } + } + + QString location() const + { + return m_dir.path(); + } + + QString baseDirectory() const + { + return m_settings.paths().base(); + } + private: QDir m_dir; + Settings m_settings; }; -InstanceManagerDialog::InstanceManagerDialog(QWidget *parent) - : QDialog(parent), ui(new Ui::InstanceManagerDialog) +InstanceManagerDialog::InstanceManagerDialog( + const PluginContainer& pc, QWidget *parent) + : QDialog(parent), ui(new Ui::InstanceManagerDialog), m_pc(pc) { ui->setupUi(this); + ui->splitter->setSizes({200, 1}); + ui->splitter->setStretchFactor(0, 0); + ui->splitter->setStretchFactor(1, 1); auto& m = InstanceManager::instance(); for (auto&& d : m.instancePaths()) { - InstanceInfo i(d); - ui->list->addItem(i.name()); + auto ii = std::make_unique(d); + + ui->list->addItem(ii->name()); + m_instances.push_back(std::move(ii)); + } + + if (!m_instances.empty()) { + select(0); } + + connect(ui->createNew, &QPushButton::clicked, [&]{ createNew(); }); + connect(ui->list, &QListWidget::itemSelectionChanged, [&]{ onSelection(); }); } InstanceManagerDialog::~InstanceManagerDialog() = default; + +void InstanceManagerDialog::select(std::size_t i) +{ + if (i >= m_instances.size()) { + return; + } + + const auto& ii = m_instances[i]; + fill(*ii); +} + +void InstanceManagerDialog::onSelection() +{ + const auto sel = ui->list->selectionModel()->selectedIndexes(); + if (sel.size() != 1) { + return; + } + + select(static_cast(sel[0].row())); +} + +void InstanceManagerDialog::createNew() +{ + CreateInstanceDialog dlg(this); + dlg.exec(); +} + +void InstanceManagerDialog::fill(const InstanceInfo& ii) +{ + ui->name->setText(ii.name()); + ui->location->setText(ii.location()); + ui->baseDirectory->setText(ii.baseDirectory()); + ui->gameName->setText(ii.gameName()); + ui->gameDir->setText(ii.gamePath()); +} diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h index ea1cfc48..c3e947dd 100644 --- a/src/instancemanagerdialog.h +++ b/src/instancemanagerdialog.h @@ -5,16 +5,30 @@ namespace Ui { class InstanceManagerDialog; }; +class InstanceInfo; +class PluginContainer; + class InstanceManagerDialog : public QDialog { Q_OBJECT public: - explicit InstanceManagerDialog(QWidget *parent = nullptr); + explicit InstanceManagerDialog( + const PluginContainer& pc, QWidget *parent = nullptr); + ~InstanceManagerDialog(); + void select(std::size_t i); + private: std::unique_ptr ui; + const PluginContainer& m_pc; + std::vector> m_instances; + + void onSelection(); + void createNew(); + + void fill(const InstanceInfo& ii); }; #endif // MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui index b35d2e58..146b5fb5 100644 --- a/src/instancemanagerdialog.ui +++ b/src/instancemanagerdialog.ui @@ -6,7 +6,7 @@ 0 0 - 531 + 689 413 @@ -40,17 +40,6 @@ - - - - Create portable instance - - - - :/MO/gui/package:/MO/gui/package - - - @@ -65,9 +54,9 @@ - + - <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance"><span style=" text-decoration: underline; color:#0000ff;">What is an instance?</span></a></p></body></html> + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance">What is an instance?</a></p></body></html> true @@ -134,7 +123,7 @@ - + 0 @@ -148,33 +137,51 @@ 0 - + + + true + + + + + - instance name + Name - - Qt::TextBrowserInteraction + + + + + + Explore - + - Game: + Game - - - - Name: + + + + true + + + + + + + true - Location: + Location @@ -185,30 +192,24 @@ - - + + - location path - - - Qt::TextBrowserInteraction + Game location - Base folder: + Base folder - - - base folder path - - - Qt::TextBrowserInteraction + + + true @@ -219,20 +220,17 @@ - - - - Explore + + + + true - - + + - game name - - - Qt::TextBrowserInteraction + Explore @@ -292,6 +290,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -371,6 +382,13 @@ + + + LinkLabel + QLabel +
linklabel.h
+
+
diff --git a/src/main.cpp b/src/main.cpp index 08d70dd9..71fb0bbd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ along with Mod Organizer. If not, see . #include "tutorialmanager.h" #include "nxmaccessmanager.h" #include "instancemanager.h" +#include "instancemanagerdialog.h" #include "organizercore.h" #include "env.h" #include "envmodule.h" @@ -298,6 +299,8 @@ int runApplication( try { Settings settings(dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName())); + settings.setGlobalInstance(); + log::getDefault().setLevel(settings.diagnostics().logLevel()); log::debug("using ini at '{}'", settings.filename()); @@ -429,6 +432,12 @@ int runApplication( tt.stop(); + QTimer::singleShot(std::chrono::milliseconds(1), [&] + { + InstanceManagerDialog dlg(*pluginContainer, &mainWindow); + dlg.exec(); + }); + res = application.exec(); mainWindow.close(); diff --git a/src/settings.cpp b/src/settings.cpp index d37f0d99..43e7a4fa 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -68,17 +68,24 @@ Settings::Settings(const QString& path) : m_Network(m_Settings), m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings), m_Interface(m_Settings), m_Diagnostics(m_Settings) { - if (s_Instance != nullptr) { - throw std::runtime_error("second instance of \"Settings\" created"); - } else { - s_Instance = this; - } } Settings::~Settings() { MOBase::QuestionBoxMemory::setCallbacks({}, {}, {}); - s_Instance = nullptr; + + if (s_Instance == this) { + s_Instance = nullptr; + } +} + +void Settings::setGlobalInstance() +{ + if (s_Instance != nullptr) { + throw std::runtime_error("second instance of \"Settings\" created"); + } else { + s_Instance = this; + } } Settings &Settings::instance() diff --git a/src/settings.h b/src/settings.h index a1b30462..84102c72 100644 --- a/src/settings.h +++ b/src/settings.h @@ -681,6 +681,7 @@ public: ~Settings(); static Settings &instance(); + void setGlobalInstance(); // name of the ini file // -- cgit v1.3.1 From 048e3ac0d9b4e4258c9c2ac4d39d332f340e73a0 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 24 Jul 2020 15:43:13 -0400 Subject: split create instance dialog pages --- src/CMakeLists.txt | 1 + src/createinstancedialog.cpp | 1053 +------------------------------------ src/createinstancedialogpages.cpp | 976 ++++++++++++++++++++++++++++++++++ src/createinstancedialogpages.h | 219 ++++++++ 4 files changed, 1199 insertions(+), 1050 deletions(-) create mode 100644 src/createinstancedialogpages.cpp create mode 100644 src/createinstancedialogpages.h (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index af350584..844f5e19 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -85,6 +85,7 @@ add_filter(NAME src/executables GROUPS add_filter(NAME src/instances GROUPS createinstancedialog + createinstancedialogpages instancemanager instancemanagerdialog ) diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index 5c85aa68..63be7346 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -1,1063 +1,16 @@ #include "createinstancedialog.h" #include "ui_createinstancedialog.h" +#include "createinstancedialogpages.h" #include "instancemanager.h" -#include "plugincontainer.h" +//#include "plugincontainer.h" #include "settings.h" #include "shared/appconfig.h" -#include +//#include #include #include using namespace MOBase; -namespace cid -{ - -using MOBase::TaskDialog; - -class PathChecker -{ -public: - PathChecker(QLabel* existsLabel, QLabel* invalidLabel) - : m_exists(existsLabel), m_invalid(invalidLabel) - { - m_existsOriginal = m_exists->text(); - m_invalidOriginal = m_invalid->text(); - } - - QString sanitizeFileName(const QString& name) const - { - QString new_name = name; - - // Restrict the allowed characters - new_name = new_name.remove(QRegExp("[^A-Za-z0-9 _=+;!@#$%^'\\-\\.\\[\\]\\{\\}\\(\\)]")); - - // Don't end in spaces and periods - new_name = new_name.remove(QRegExp("\\.*$")); - new_name = new_name.remove(QRegExp(" *$")); - - // Recurse until stuff stops changing - if (new_name != name) { - return sanitizeFileName(new_name); - } - - return new_name; - } - - // same thing as above, but allows path separators and colons - // - QString sanitizePath(const QString& path) const - { - QString new_name = path; - - // Restrict the allowed characters - new_name = new_name.remove(QRegExp("[^\\\\\\/A-Za-z0-9 _=+;!@#$%^:'\\-\\.\\[\\]\\{\\}\\(\\)]")); - - // Don't end in spaces and periods - new_name = new_name.remove(QRegExp("\\.*$")); - new_name = new_name.remove(QRegExp(" *$")); - - // Recurse until stuff stops changing - if (new_name != path) { - return sanitizeFileName(new_name); - } - - return new_name; - } - - bool checkName(QString parentDir, QString name) const - { - bool exists = false; - bool invalid = false; - bool empty = false; - - name = name.trimmed(); - - if (name.isEmpty()) { - empty = true; - } else { - const QString sanitized = sanitizeFileName(name); - - if (name != sanitized) { - invalid = true; - } else { - exists = QDir(parentDir).exists(name); - } - } - - bool okay = false; - - if (exists) { - m_exists->setVisible(true); - setPossiblePlaceholder(m_exists, m_existsOriginal, QDir(parentDir).filePath(name)); - m_invalid->setVisible(false); - } else if (invalid) { - m_exists->setVisible(false); - m_invalid->setVisible(true); - setPossiblePlaceholder(m_invalid, m_invalidOriginal, name); - } else { - okay = !empty; - m_exists->setVisible(false); - m_invalid->setVisible(false); - } - - return okay; - } - - bool checkPath(QString path) const - { - bool exists = false; - bool invalid = false; - bool empty = false; - - path = path.trimmed(); - - if (path.isEmpty()) { - empty = true; - } else { - const QString sanitized = sanitizePath(path); - - if (path != sanitized) { - invalid = true; - } else { - exists = QDir(path).exists(); - } - } - - bool okay = false; - - if (exists) { - m_exists->setVisible(true); - setPossiblePlaceholder(m_exists, m_existsOriginal, path); - m_invalid->setVisible(false); - } else if (invalid) { - m_exists->setVisible(false); - m_invalid->setVisible(true); - setPossiblePlaceholder(m_invalid, m_invalidOriginal, path); - } else { - okay = !empty; - m_exists->setVisible(false); - m_invalid->setVisible(false); - } - - return okay; - } - -private: - QLabel* m_exists; - QString m_existsOriginal; - - QLabel* m_invalid; - QString m_invalidOriginal; - - void setPossiblePlaceholder( - QLabel* label, const QString& s, const QString& arg) const - { - if (label->text().contains("%1")) { - label->setText(s.arg(arg)); - } - } -}; - -QString makeDefaultPath(const std::wstring& dir) -{ - return QDir::toNativeSeparators( - PathSettings::makeDefaultPath(QString::fromStdWString(dir))); -} - - -class Page -{ -public: - Page(CreateInstanceDialog& dlg) - : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()) - { - } - - virtual bool ready() const - { - return true; - } - - virtual bool skip() const - { - // no-op - return false; - } - - virtual void activated() - { - // no-op - } - - void updateNavigation() - { - m_dlg.updateNavigation(); - } - - void next() - { - m_dlg.next(); - } - - - virtual CreateInstanceDialog::Types selectedInstanceType() const - { - // no-op - return CreateInstanceDialog::NoType; - } - - virtual IPluginGame* selectedGame() const - { - // no-op - return nullptr; - } - - virtual QString selectedGameLocation() const - { - // no-op - return {}; - } - - virtual QString selectedGameEdition() const - { - // no-op - return {}; - } - - virtual QString selectedInstanceName() const - { - // no-op - return {}; - } - - virtual CreateInstanceDialog::Paths selectedPaths() const - { - // no-op - return {}; - } - -protected: - Ui::CreateInstanceDialog* ui; - CreateInstanceDialog& m_dlg; - const PluginContainer& m_pc; -}; - - -class InfoPage : public Page -{ -public: - InfoPage(CreateInstanceDialog& dlg) - : Page(dlg) - { - } -}; - - -class TypePage : public Page -{ -public: - TypePage(CreateInstanceDialog& dlg) - : Page(dlg), m_type(CreateInstanceDialog::NoType) - { - ui->createGlobal->setDescription( - ui->createGlobal->description() - .arg(InstanceManager::instance().instancesPath())); - - ui->createPortable->setDescription( - ui->createPortable->description() - .arg(qApp->applicationDirPath())); - - QObject::connect( - ui->createGlobal, &QAbstractButton::clicked, [&]{ global(); }); - - QObject::connect( - ui->createPortable, &QAbstractButton::clicked, [&]{ portable(); }); - } - - bool ready() const override - { - return (m_type != CreateInstanceDialog::NoType); - } - - CreateInstanceDialog::Types selectedInstanceType() const override - { - return m_type; - } - - void global() - { - m_type = CreateInstanceDialog::Global; - - ui->createGlobal->setChecked(true); - ui->createPortable->setChecked(false); - - next(); - } - - void portable() - { - m_type = CreateInstanceDialog::Portable; - - ui->createGlobal->setChecked(false); - ui->createPortable->setChecked(true); - - next(); - } - -private: - CreateInstanceDialog::Types m_type; -}; - - -class GamePage : public Page -{ -public: - GamePage(CreateInstanceDialog& dlg) - : Page(dlg), m_selection(nullptr) - { - createGames(); - fillList(); - - QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); }); - } - - bool ready() const override - { - return (m_selection != nullptr); - } - - IPluginGame* selectedGame() const override - { - if (!m_selection) { - return nullptr; - } - - return m_selection->game; - } - - QString selectedGameLocation() const override - { - if (!m_selection) { - return {}; - } - - return QDir::toNativeSeparators(m_selection->dir); - } - - void select(IPluginGame* game) - { - Game* checked = findGame(game); - - if (checked) { - if (!checked->installed) { - const auto path = QFileDialog::getExistingDirectory( - &m_dlg, QObject::tr("Find game installation")); - - if (path.isEmpty()) { - checked = nullptr; - } else { - checked = checkInstallation(path, checked); - } - } - } - - m_selection = checked; - selectButton(checked); - updateNavigation(); - } - - void selectCustom() - { - const auto path = QFileDialog::getExistingDirectory( - &m_dlg, QObject::tr("Find game installation")); - - if (path.isEmpty()) { - selectButton(m_selection); - return; - } - - for (auto& g : m_games) { - if (g->game->looksValid(path)) { - g->dir = path; - g->installed = true; - select(g->game); - updateButton(g.get()); - return; - } - } - - warnUnrecognized(path); - selectButton(m_selection); - } - - void warnUnrecognized(const QString& path) - { - QString supportedGames; - for (auto* game : sortedGamePlugins()) { - supportedGames += "
  • " + game->gameName() + "
  • "; - } - - QMessageBox::warning(&m_dlg, - QObject::tr("Unrecognized game"), - QObject::tr( - "The folder %1 does not seem to contain a game Mod Organizer can " - "manage.

    These are the games that can be managed:" - "
      %2
    ").arg(path).arg(supportedGames)); - } - -private: - struct Game - { - IPluginGame* game = nullptr; - QCommandLinkButton* button = nullptr; - QString dir; - bool installed = false; - - Game(IPluginGame* g) - : game(g), installed(g->isInstalled()) - { - if (installed) { - dir = game->gameDirectory().path(); - } - } - - Game(const Game&) = delete; - Game& operator=(const Game&) = delete; - }; - - std::vector> m_games; - Game* m_selection; - - - std::vector sortedGamePlugins() const - { - std::vector v; - - for (auto* game : m_pc.plugins()) { - v.push_back(game); - } - - std::sort(v.begin(), v.end(), [](auto* a, auto* b) { - return (a->gameName() < b->gameName()); - }); - - return v; - } - - Game* findGame(IPluginGame* game) - { - for (auto& g : m_games) { - if (g->game == game) { - return g.get(); - } - } - - return nullptr; - } - - void createGames() - { - m_games.clear(); - - for (auto* game : sortedGamePlugins()) { - m_games.push_back(std::make_unique(game)); - } - } - - void updateButton(Game* g) - { - if (!g || !g->button) { - return; - } - - g->button->setText(g->game->gameName()); - - if (g->installed) { - g->button->setDescription(g->dir); - } else { - g->button->setDescription(QObject::tr("No installation found")); - } - } - - void selectButton(Game* g) - { - // go through each game, set the button that is for game `g` as active; - // some button might not exist, which happens when selecting a custom - // folder for a game that was considered uninstalled - - for (const auto& gg : m_games) { - if (!g) { - // nothing should be selected - if (gg->button) { - gg->button->setChecked(false); - } - - continue; - } - - if (gg->game == g->game) { - // this is the button that should be selected - - if (!gg->button) { - // this happens when the button wasn't visible because the game - // was not installed; create it and show it - // and it has a button, just check it - createGameButton(gg.get()); - ui->games->addButton(gg->button, QDialogButtonBox::AcceptRole); - } - - gg->button->setChecked(true); - gg->button->setFocus(); - } else { - // this is not the button you're looking for - if (gg->button) { - gg->button->setChecked(false); - } - } - } - } - - QCommandLinkButton* createCustomButton() - { - auto* b = new QCommandLinkButton; - - b->setText(QObject::tr("Browse...")); - b->setDescription( - QObject::tr("The folder must contain a valid game installation")); - - QObject::connect(b, &QAbstractButton::clicked, [&] { - selectCustom(); - }); - - return b; - } - - void createGameButton(Game* g) - { - g->button = new QCommandLinkButton; - g->button->setCheckable(true); - - updateButton(g); - - QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { - select(g->game); - }); - } - - void fillList() - { - const bool showAll = ui->showAllGames->isChecked(); - - ui->games->clear(); - - ui->games->addButton(createCustomButton(), QDialogButtonBox::AcceptRole); - - for (auto& g : m_games) { - g->button = nullptr; - - if (!showAll && !g->installed) { - // not installed - continue; - } - - createGameButton(g.get()); - ui->games->addButton(g->button, QDialogButtonBox::AcceptRole); - } - } - - Game* checkInstallation(const QString& path, Game* g) - { - if (g->game->looksValid(path)) { - // okay - return g; - } - - // the selected game can't use that folder, find another one - auto* otherGame = findAnotherGame(path); - if (otherGame == g->game) { - // shouldn't happen, but okay - return g; - } - - if (otherGame) { - auto* confirmedGame = confirmOtherGame(path, g->game, otherGame); - - if (!confirmedGame) { - // cancelled - return nullptr; - } - - // make it look like the user clicked that button instead - g = findGame(confirmedGame); - if (!g) { - return nullptr; - } - } else { - // nothing can manage this, but the user can override - if (!confirmUnknown(path, g->game)) { - // cancelled - return nullptr; - } - } - - // remember this path - g->dir = path; - g->installed = true; - - updateButton(g); - - return g; - } - - IPluginGame* findAnotherGame(const QString& path) - { - for (auto* otherGame : m_pc.plugins()) { - if (otherGame->looksValid(path)) { - return otherGame; - } - } - - return nullptr; - } - - bool confirmUnknown(const QString& path, IPluginGame* game) - { - const auto r = TaskDialog(&m_dlg) - .title(QObject::tr("Unrecognized game")) - .main(QObject::tr("Unrecognized game")) - .content(QObject::tr( - "The folder %1 does not seem to contain an installation for " - "%2 or " - "for any other game Mod Organizer can manage.") - .arg(path) - .arg(game->gameName())) - .button({ - QObject::tr("Use this folder for %1").arg(game->gameName()), - QObject::tr("I know what I'm doing"), - QMessageBox::Ignore}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .exec(); - - return (r == QMessageBox::Ignore); - } - - IPluginGame* confirmOtherGame( - const QString& path, - IPluginGame* selectedGame, IPluginGame* guessedGame) - { - const auto r = TaskDialog(&m_dlg) - .title(QObject::tr("Incorrect game")) - .main(QObject::tr("Incorrect game")) - .content(QObject::tr( - "The folder %1 seems to contain an installation for " - "%2, " - "not " - "%3.") - .arg(path) - .arg(guessedGame->gameName()) - .arg(selectedGame->gameName())) - .button({ - QObject::tr("Manage %1 instead").arg(guessedGame->gameName()), - QMessageBox::Ok}) - .button({ - QObject::tr("Use this folder for %1").arg(selectedGame->gameName()), - QObject::tr("I know what I'm doing"), - QMessageBox::Ignore}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .exec(); - - switch (r) - { - case QMessageBox::Ok: - return guessedGame; - - case QMessageBox::Ignore: - return selectedGame; - - case QMessageBox::Cancel: - default: - return nullptr; - } - } -}; - - -class EditionsPage : public Page -{ -public: - EditionsPage(CreateInstanceDialog& dlg) - : Page(dlg), m_previousGame(nullptr) - { - } - - bool ready() const override - { - return !m_selection.isEmpty(); - } - - bool skip() const override - { - auto* g = m_dlg.game(); - if (!g) { - // shouldn't happen - return true; - } - - const auto variants = g->gameVariants(); - return (variants.size() < 2); - } - - void activated() override - { - auto* g = m_dlg.game(); - - if (m_previousGame != g) { - m_previousGame = g; - m_selection = ""; - fillList(); - } - } - - void select(const QString& variant) - { - for (auto* b : m_buttons) { - if (b->text() == variant) { - m_selection = variant; - b->setChecked(true); - } else { - b->setChecked(false); - } - } - - updateNavigation(); - } - - QString selectedGameEdition() const override - { - auto* g = m_dlg.game(); - if (!g) { - // shouldn't happen - return {}; - } - - const auto variants = g->gameVariants(); - if (variants.size() < 2) { - return {}; - } else { - return m_selection; - } - } - -private: - IPluginGame* m_previousGame; - std::vector m_buttons; - QString m_selection; - - void fillList() - { - ui->editions->clear(); - m_buttons.clear(); - - auto* g = m_dlg.game(); - if (!g) { - // shouldn't happen - return; - } - - const auto variants = g->gameVariants(); - for (auto& v : variants) { - auto* b = new QCommandLinkButton(v); - b->setCheckable(true); - - QObject::connect(b, &QAbstractButton::clicked, [v, this] { - select(v); - }); - - ui->editions->addButton(b, QDialogButtonBox::AcceptRole); - m_buttons.push_back(b); - } - } -}; - - -class NamePage : public Page -{ -public: - NamePage(CreateInstanceDialog& dlg) : - Page(dlg), m_modified(false), m_okay(false), - m_checker(ui->instanceNameExists, ui->instanceNameInvalid) - { - m_originalLabel = ui->instanceNameLabel->text(); - - QObject::connect( - ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); }); - } - - bool ready() const override - { - return m_okay; - } - - bool skip() const override - { - return (m_dlg.instanceType() == CreateInstanceDialog::Portable); - } - - void activated() override - { - auto* g = m_dlg.game(); - if (!g) { - // shouldn't happen, next should be disabled - return; - } - - ui->instanceNameLabel->setText(m_originalLabel.arg(g->gameName())); - - if (!m_modified || ui->instanceName->text().isEmpty()) { - const auto n = InstanceManager::instance().makeUniqueName(g->gameName()); - ui->instanceName->setText(n); - m_modified = false; - } - - updateWarnings(); - } - - QString selectedInstanceName() const override - { - if (!m_okay) { - return {}; - } - - const auto text = ui->instanceName->text().trimmed(); - return m_checker.sanitizeFileName(text); - } - -private: - PathChecker m_checker; - QString m_originalLabel; - bool m_modified; - bool m_okay; - - void onChanged() - { - m_modified = true; - updateWarnings(); - } - - void updateWarnings() - { - const auto root = InstanceManager::instance().instancesPath(); - - m_okay = m_checker.checkName(root, ui->instanceName->text()); - updateNavigation(); - } -}; - - -class PathsPage : public Page -{ -public: - PathsPage(CreateInstanceDialog& dlg) : - Page(dlg), - m_checker(ui->locationExists, ui->locationInvalid), - m_advancedChecker(ui->advancedDirExists, ui->advancedDirInvalid) - { - QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->downloads, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->mods, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->profiles, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->overwrite, &QLineEdit::textEdited, [&]{ onChanged(); }); - - QObject::connect( - ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); }); - - ui->pathPages->setCurrentIndex(0); - } - - - bool ready() const override - { - return checkPaths(); - } - - void activated() override - { - const auto name = m_dlg.instanceName(); - - setPaths(name, (m_lastInstanceName != name)); - checkPaths(); - updateNavigation(); - - m_lastInstanceName = name; - } - - CreateInstanceDialog::Paths selectedPaths() const override - { - CreateInstanceDialog::Paths p; - - if (ui->advancedPathOptions->isChecked()) { - p.base = ui->base->text(); - p.downloads = ui->downloads->text(); - p.mods = ui->mods->text(); - p.profiles = ui->profiles->text(); - p.overwrite = ui->overwrite->text(); - } else { - p.base = ui->location->text(); - } - - return p; - } - -private: - PathChecker m_checker, m_advancedChecker; - QString m_lastInstanceName; - - void onChanged() - { - checkPaths(); - updateNavigation(); - } - - bool checkPaths() const - { - if (ui->advancedPathOptions->isChecked()) { - return - checkAdvancedPath(ui->base->text()) && - checkVarPath(ui->downloads->text()); - } else { - return m_checker.checkPath(ui->location->text()); - } - } - - bool checkAdvancedPath(const QString& path) const - { - return m_advancedChecker.checkPath(path); - } - - bool checkVarPath(QString path) const - { - path = PathSettings::resolve(path, ui->base->text()); - return checkAdvancedPath(path); - } - - void onAdvanced() - { - if (ui->advancedPathOptions->isChecked()) { - ui->base->setText(ui->location->text()); - ui->pathPages->setCurrentIndex(1); - } else { - ui->location->setText(ui->base->text()); - ui->pathPages->setCurrentIndex(0); - } - - checkPaths(); - } - - void setPaths(const QString& name, bool force) - { - const auto root = InstanceManager::instance().instancesPath(); - const auto path = QDir::toNativeSeparators(root + "/" + name); - - setIfEmpty(ui->location, path, force); - - setIfEmpty(ui->base, path, force); - setIfEmpty(ui->downloads, makeDefaultPath(AppConfig::downloadPath()), force); - setIfEmpty(ui->mods, makeDefaultPath(AppConfig::modsPath()), force); - setIfEmpty(ui->profiles, makeDefaultPath(AppConfig::profilesPath()), force); - setIfEmpty(ui->overwrite, makeDefaultPath(AppConfig::overwritePath()), force); - } - - void setIfEmpty(QLineEdit* e, const QString& path, bool force) - { - if (e->text().isEmpty() || force) { - e->setText(path); - } - } -}; - - -class ConfirmationPage : public Page -{ -public: - ConfirmationPage(CreateInstanceDialog& dlg) - : Page(dlg) - { - } - - void activated() override - { - ui->review->setPlainText(makeReview()); - ui->creationLog->clear(); - } - - QString toLocalizedString(CreateInstanceDialog::Types t) const - { - switch (t) - { - case CreateInstanceDialog::Global: - return QObject::tr("Global"); - - case CreateInstanceDialog::Portable: - return QObject::tr("Portable"); - - default: - return QObject::tr("Instance type: %1").arg(QObject::tr("?")); - } - } - - QString makeReview() const - { - QStringList lines; - const auto paths = m_dlg.paths(); - - lines.push_back(QObject::tr("Instance type: %1").arg(toLocalizedString(m_dlg.instanceType()))); - lines.push_back(QObject::tr("Instance location: %1").arg(m_dlg.dataPath())); - - if (m_dlg.instanceType() != CreateInstanceDialog::Portable) { - lines.push_back(QObject::tr("Instance name: %1").arg(m_dlg.instanceName())); - } - - if (paths.downloads.isEmpty()) { - // simple settings - if (paths.base != m_dlg.dataPath()) { - lines.push_back(QObject::tr("Base directory: %1").arg(paths.base)); - } - } else { - // advanced settings - lines.push_back(QObject::tr("Base directory: %1").arg(paths.base)); - lines.push_back(dirLine(QObject::tr("Downloads"), paths.downloads)); - lines.push_back(dirLine(QObject::tr("Mods"), paths.mods)); - lines.push_back(dirLine(QObject::tr("Profiles"), paths.profiles)); - lines.push_back(dirLine(QObject::tr("Overwrite"), paths.overwrite)); - } - - // game - QString name = m_dlg.game()->gameName(); - if (!m_dlg.gameEdition().isEmpty()) { - name += " (" + m_dlg.gameEdition() + ")"; - } - - lines.push_back(QObject::tr("Game: %1").arg(name)); - lines.push_back(QObject::tr("Game location: %1").arg(m_dlg.gameLocation())); - - return lines.join("\n"); - } - - QString dirLine(const QString& caption, const QString& path) const - { - return QString(" - %1: %2").arg(caption).arg(path); - } -}; - -} // namespace - - CreateInstanceDialog::CreateInstanceDialog( const PluginContainer& pc, QWidget *parent) : QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc) diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp new file mode 100644 index 00000000..ab020c66 --- /dev/null +++ b/src/createinstancedialogpages.cpp @@ -0,0 +1,976 @@ +#include "createinstancedialogpages.h" +#include "ui_createinstancedialog.h" +#include "instancemanager.h" +#include "settings.h" +#include "plugincontainer.h" +#include "shared/appconfig.h" +#include +#include + +namespace cid +{ + +using MOBase::IPluginGame; +using MOBase::TaskDialog; + +QString makeDefaultPath(const std::wstring& dir) +{ + return QDir::toNativeSeparators( + PathSettings::makeDefaultPath(QString::fromStdWString(dir))); +} + + +PathChecker::PathChecker(QLabel* existsLabel, QLabel* invalidLabel) + : m_exists(existsLabel), m_invalid(invalidLabel) +{ + m_existsOriginal = m_exists->text(); + m_invalidOriginal = m_invalid->text(); +} + +QString PathChecker::sanitizeFileName(const QString& name) const +{ + QString new_name = name; + + // Restrict the allowed characters + new_name = new_name.remove(QRegExp("[^A-Za-z0-9 _=+;!@#$%^'\\-\\.\\[\\]\\{\\}\\(\\)]")); + + // Don't end in spaces and periods + new_name = new_name.remove(QRegExp("\\.*$")); + new_name = new_name.remove(QRegExp(" *$")); + + // Recurse until stuff stops changing + if (new_name != name) { + return sanitizeFileName(new_name); + } + + return new_name; +} + +// same thing as above, but allows path separators and colons +// +QString PathChecker::sanitizePath(const QString& path) const +{ + QString new_name = path; + + // Restrict the allowed characters + new_name = new_name.remove(QRegExp("[^\\\\\\/A-Za-z0-9 _=+;!@#$%^:'\\-\\.\\[\\]\\{\\}\\(\\)]")); + + // Don't end in spaces and periods + new_name = new_name.remove(QRegExp("\\.*$")); + new_name = new_name.remove(QRegExp(" *$")); + + // Recurse until stuff stops changing + if (new_name != path) { + return sanitizeFileName(new_name); + } + + return new_name; +} + +bool PathChecker::checkName(QString parentDir, QString name) const +{ + bool exists = false; + bool invalid = false; + bool empty = false; + + name = name.trimmed(); + + if (name.isEmpty()) { + empty = true; + } else { + const QString sanitized = sanitizeFileName(name); + + if (name != sanitized) { + invalid = true; + } else { + exists = QDir(parentDir).exists(name); + } + } + + bool okay = false; + + if (exists) { + m_exists->setVisible(true); + setPossiblePlaceholder(m_exists, m_existsOriginal, QDir(parentDir).filePath(name)); + m_invalid->setVisible(false); + } else if (invalid) { + m_exists->setVisible(false); + m_invalid->setVisible(true); + setPossiblePlaceholder(m_invalid, m_invalidOriginal, name); + } else { + okay = !empty; + m_exists->setVisible(false); + m_invalid->setVisible(false); + } + + return okay; +} + +bool PathChecker::checkPath(QString path) const +{ + bool exists = false; + bool invalid = false; + bool empty = false; + + path = path.trimmed(); + + if (path.isEmpty()) { + empty = true; + } else { + const QString sanitized = sanitizePath(path); + + if (path != sanitized) { + invalid = true; + } else { + exists = QDir(path).exists(); + } + } + + bool okay = false; + + if (exists) { + m_exists->setVisible(true); + setPossiblePlaceholder(m_exists, m_existsOriginal, path); + m_invalid->setVisible(false); + } else if (invalid) { + m_exists->setVisible(false); + m_invalid->setVisible(true); + setPossiblePlaceholder(m_invalid, m_invalidOriginal, path); + } else { + okay = !empty; + m_exists->setVisible(false); + m_invalid->setVisible(false); + } + + return okay; +} + +void PathChecker::setPossiblePlaceholder( + QLabel* label, const QString& s, const QString& arg) const +{ + if (label->text().contains("%1")) { + label->setText(s.arg(arg)); + } +} + + +Page::Page(CreateInstanceDialog& dlg) + : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()) +{ +} + +bool Page::ready() const +{ + return true; +} + +bool Page::skip() const +{ + // no-op + return false; +} + +void Page::activated() +{ + // no-op +} + +void Page::updateNavigation() +{ + m_dlg.updateNavigation(); +} + +void Page::next() +{ + m_dlg.next(); +} + + +CreateInstanceDialog::Types Page::selectedInstanceType() const +{ + // no-op + return CreateInstanceDialog::NoType; +} + +IPluginGame* Page::selectedGame() const +{ + // no-op + return nullptr; +} + +QString Page::selectedGameLocation() const +{ + // no-op + return {}; +} + +QString Page::selectedGameEdition() const +{ + // no-op + return {}; +} + +QString Page::selectedInstanceName() const +{ + // no-op + return {}; +} + +CreateInstanceDialog::Paths Page::selectedPaths() const +{ + // no-op + return {}; +} + + +InfoPage::InfoPage(CreateInstanceDialog& dlg) + : Page(dlg) +{ +} + + +TypePage::TypePage(CreateInstanceDialog& dlg) + : Page(dlg), m_type(CreateInstanceDialog::NoType) +{ + ui->createGlobal->setDescription( + ui->createGlobal->description() + .arg(InstanceManager::instance().instancesPath())); + + ui->createPortable->setDescription( + ui->createPortable->description() + .arg(qApp->applicationDirPath())); + + QObject::connect( + ui->createGlobal, &QAbstractButton::clicked, [&]{ global(); }); + + QObject::connect( + ui->createPortable, &QAbstractButton::clicked, [&]{ portable(); }); +} + +bool TypePage::ready() const +{ + return (m_type != CreateInstanceDialog::NoType); +} + +CreateInstanceDialog::Types TypePage::selectedInstanceType() const +{ + return m_type; +} + +void TypePage::global() +{ + m_type = CreateInstanceDialog::Global; + + ui->createGlobal->setChecked(true); + ui->createPortable->setChecked(false); + + next(); +} + +void TypePage::portable() +{ + m_type = CreateInstanceDialog::Portable; + + ui->createGlobal->setChecked(false); + ui->createPortable->setChecked(true); + + next(); +} + + +GamePage::Game::Game(IPluginGame* g) + : game(g), installed(g->isInstalled()) +{ + if (installed) { + dir = game->gameDirectory().path(); + } +} + + +GamePage::GamePage(CreateInstanceDialog& dlg) + : Page(dlg), m_selection(nullptr) +{ + createGames(); + fillList(); + + QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); }); +} + +bool GamePage::ready() const +{ + return (m_selection != nullptr); +} + +IPluginGame* GamePage::selectedGame() const +{ + if (!m_selection) { + return nullptr; + } + + return m_selection->game; +} + +QString GamePage::selectedGameLocation() const +{ + if (!m_selection) { + return {}; + } + + return QDir::toNativeSeparators(m_selection->dir); +} + +void GamePage::select(IPluginGame* game) +{ + Game* checked = findGame(game); + + if (checked) { + if (!checked->installed) { + const auto path = QFileDialog::getExistingDirectory( + &m_dlg, QObject::tr("Find game installation")); + + if (path.isEmpty()) { + checked = nullptr; + } else { + checked = checkInstallation(path, checked); + } + } + } + + m_selection = checked; + selectButton(checked); + updateNavigation(); +} + +void GamePage::selectCustom() +{ + const auto path = QFileDialog::getExistingDirectory( + &m_dlg, QObject::tr("Find game installation")); + + if (path.isEmpty()) { + selectButton(m_selection); + return; + } + + for (auto& g : m_games) { + if (g->game->looksValid(path)) { + g->dir = path; + g->installed = true; + select(g->game); + updateButton(g.get()); + return; + } + } + + warnUnrecognized(path); + selectButton(m_selection); +} + +void GamePage::warnUnrecognized(const QString& path) +{ + QString supportedGames; + for (auto* game : sortedGamePlugins()) { + supportedGames += "
  • " + game->gameName() + "
  • "; + } + + QMessageBox::warning(&m_dlg, + QObject::tr("Unrecognized game"), + QObject::tr( + "The folder %1 does not seem to contain a game Mod Organizer can " + "manage.

    These are the games that can be managed:" + "
      %2
    ").arg(path).arg(supportedGames)); +} + +std::vector GamePage::sortedGamePlugins() const +{ + std::vector v; + + for (auto* game : m_pc.plugins()) { + v.push_back(game); + } + + std::sort(v.begin(), v.end(), [](auto* a, auto* b) { + return (a->gameName() < b->gameName()); + }); + + return v; +} + +GamePage::Game* GamePage::findGame(IPluginGame* game) +{ + for (auto& g : m_games) { + if (g->game == game) { + return g.get(); + } + } + + return nullptr; +} + +void GamePage::createGames() +{ + m_games.clear(); + + for (auto* game : sortedGamePlugins()) { + m_games.push_back(std::make_unique(game)); + } +} + +void GamePage::updateButton(Game* g) +{ + if (!g || !g->button) { + return; + } + + g->button->setText(g->game->gameName()); + + if (g->installed) { + g->button->setDescription(g->dir); + } else { + g->button->setDescription(QObject::tr("No installation found")); + } +} + +void GamePage::selectButton(Game* g) +{ + // go through each game, set the button that is for game `g` as active; + // some button might not exist, which happens when selecting a custom + // folder for a game that was considered uninstalled + + for (const auto& gg : m_games) { + if (!g) { + // nothing should be selected + if (gg->button) { + gg->button->setChecked(false); + } + + continue; + } + + if (gg->game == g->game) { + // this is the button that should be selected + + if (!gg->button) { + // this happens when the button wasn't visible because the game + // was not installed; create it and show it + // and it has a button, just check it + createGameButton(gg.get()); + ui->games->addButton(gg->button, QDialogButtonBox::AcceptRole); + } + + gg->button->setChecked(true); + gg->button->setFocus(); + } else { + // this is not the button you're looking for + if (gg->button) { + gg->button->setChecked(false); + } + } + } +} + +QCommandLinkButton* GamePage::createCustomButton() +{ + auto* b = new QCommandLinkButton; + + b->setText(QObject::tr("Browse...")); + b->setDescription( + QObject::tr("The folder must contain a valid game installation")); + + QObject::connect(b, &QAbstractButton::clicked, [&] { + selectCustom(); + }); + + return b; +} + +void GamePage::createGameButton(Game* g) +{ + g->button = new QCommandLinkButton; + g->button->setCheckable(true); + + updateButton(g); + + QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { + select(g->game); + }); +} + +void GamePage::fillList() +{ + const bool showAll = ui->showAllGames->isChecked(); + + ui->games->clear(); + + ui->games->addButton(createCustomButton(), QDialogButtonBox::AcceptRole); + + for (auto& g : m_games) { + g->button = nullptr; + + if (!showAll && !g->installed) { + // not installed + continue; + } + + createGameButton(g.get()); + ui->games->addButton(g->button, QDialogButtonBox::AcceptRole); + } +} + +GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) +{ + if (g->game->looksValid(path)) { + // okay + return g; + } + + // the selected game can't use that folder, find another one + auto* otherGame = findAnotherGame(path); + if (otherGame == g->game) { + // shouldn't happen, but okay + return g; + } + + if (otherGame) { + auto* confirmedGame = confirmOtherGame(path, g->game, otherGame); + + if (!confirmedGame) { + // cancelled + return nullptr; + } + + // make it look like the user clicked that button instead + g = findGame(confirmedGame); + if (!g) { + return nullptr; + } + } else { + // nothing can manage this, but the user can override + if (!confirmUnknown(path, g->game)) { + // cancelled + return nullptr; + } + } + + // remember this path + g->dir = path; + g->installed = true; + + updateButton(g); + + return g; +} + +IPluginGame* GamePage::findAnotherGame(const QString& path) +{ + for (auto* otherGame : m_pc.plugins()) { + if (otherGame->looksValid(path)) { + return otherGame; + } + } + + return nullptr; +} + +bool GamePage::confirmUnknown(const QString& path, IPluginGame* game) +{ + const auto r = TaskDialog(&m_dlg) + .title(QObject::tr("Unrecognized game")) + .main(QObject::tr("Unrecognized game")) + .content(QObject::tr( + "The folder %1 does not seem to contain an installation for " + "%2 or " + "for any other game Mod Organizer can manage.") + .arg(path) + .arg(game->gameName())) + .button({ + QObject::tr("Use this folder for %1").arg(game->gameName()), + QObject::tr("I know what I'm doing"), + QMessageBox::Ignore}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); + + return (r == QMessageBox::Ignore); +} + +IPluginGame* GamePage::confirmOtherGame( + const QString& path, + IPluginGame* selectedGame, IPluginGame* guessedGame) +{ + const auto r = TaskDialog(&m_dlg) + .title(QObject::tr("Incorrect game")) + .main(QObject::tr("Incorrect game")) + .content(QObject::tr( + "The folder %1 seems to contain an installation for " + "%2, " + "not " + "%3.") + .arg(path) + .arg(guessedGame->gameName()) + .arg(selectedGame->gameName())) + .button({ + QObject::tr("Manage %1 instead").arg(guessedGame->gameName()), + QMessageBox::Ok}) + .button({ + QObject::tr("Use this folder for %1").arg(selectedGame->gameName()), + QObject::tr("I know what I'm doing"), + QMessageBox::Ignore}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); + + switch (r) + { + case QMessageBox::Ok: + return guessedGame; + + case QMessageBox::Ignore: + return selectedGame; + + case QMessageBox::Cancel: + default: + return nullptr; + } +} + + +EditionsPage::EditionsPage(CreateInstanceDialog& dlg) + : Page(dlg), m_previousGame(nullptr) +{ +} + +bool EditionsPage::ready() const +{ + return !m_selection.isEmpty(); +} + +bool EditionsPage::skip() const +{ + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen + return true; + } + + const auto variants = g->gameVariants(); + return (variants.size() < 2); +} + +void EditionsPage::activated() +{ + auto* g = m_dlg.game(); + + if (m_previousGame != g) { + m_previousGame = g; + m_selection = ""; + fillList(); + } +} + +void EditionsPage::select(const QString& variant) +{ + for (auto* b : m_buttons) { + if (b->text() == variant) { + m_selection = variant; + b->setChecked(true); + } else { + b->setChecked(false); + } + } + + updateNavigation(); +} + +QString EditionsPage::selectedGameEdition() const +{ + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen + return {}; + } + + const auto variants = g->gameVariants(); + if (variants.size() < 2) { + return {}; + } else { + return m_selection; + } +} + +void EditionsPage::fillList() +{ + ui->editions->clear(); + m_buttons.clear(); + + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen + return; + } + + const auto variants = g->gameVariants(); + for (auto& v : variants) { + auto* b = new QCommandLinkButton(v); + b->setCheckable(true); + + QObject::connect(b, &QAbstractButton::clicked, [v, this] { + select(v); + }); + + ui->editions->addButton(b, QDialogButtonBox::AcceptRole); + m_buttons.push_back(b); + } +} + + +NamePage::NamePage(CreateInstanceDialog& dlg) : + Page(dlg), m_modified(false), m_okay(false), + m_checker(ui->instanceNameExists, ui->instanceNameInvalid) +{ + m_originalLabel = ui->instanceNameLabel->text(); + + QObject::connect( + ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); }); +} + +bool NamePage::ready() const +{ + return m_okay; +} + +bool NamePage::skip() const +{ + return (m_dlg.instanceType() == CreateInstanceDialog::Portable); +} + +void NamePage::activated() +{ + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen, next should be disabled + return; + } + + ui->instanceNameLabel->setText(m_originalLabel.arg(g->gameName())); + + if (!m_modified || ui->instanceName->text().isEmpty()) { + const auto n = InstanceManager::instance().makeUniqueName(g->gameName()); + ui->instanceName->setText(n); + m_modified = false; + } + + updateWarnings(); +} + +QString NamePage::selectedInstanceName() const +{ + if (!m_okay) { + return {}; + } + + const auto text = ui->instanceName->text().trimmed(); + return m_checker.sanitizeFileName(text); +} + +void NamePage::onChanged() +{ + m_modified = true; + updateWarnings(); +} + +void NamePage::updateWarnings() +{ + const auto root = InstanceManager::instance().instancesPath(); + + m_okay = m_checker.checkName(root, ui->instanceName->text()); + updateNavigation(); +} + + +PathsPage::PathsPage(CreateInstanceDialog& dlg) : + Page(dlg), + m_checker(ui->locationExists, ui->locationInvalid), + m_advancedChecker(ui->advancedDirExists, ui->advancedDirInvalid) +{ + QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(ui->downloads, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(ui->mods, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(ui->profiles, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(ui->overwrite, &QLineEdit::textEdited, [&]{ onChanged(); }); + + QObject::connect( + ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); }); + + ui->pathPages->setCurrentIndex(0); +} + + +bool PathsPage::ready() const +{ + return checkPaths(); +} + +void PathsPage::activated() +{ + const auto name = m_dlg.instanceName(); + + setPaths(name, (m_lastInstanceName != name)); + checkPaths(); + updateNavigation(); + + m_lastInstanceName = name; +} + +CreateInstanceDialog::Paths PathsPage::selectedPaths() const +{ + CreateInstanceDialog::Paths p; + + if (ui->advancedPathOptions->isChecked()) { + p.base = ui->base->text(); + p.downloads = ui->downloads->text(); + p.mods = ui->mods->text(); + p.profiles = ui->profiles->text(); + p.overwrite = ui->overwrite->text(); + } else { + p.base = ui->location->text(); + } + + return p; +} + +void PathsPage::onChanged() +{ + checkPaths(); + updateNavigation(); +} + +bool PathsPage::checkPaths() const +{ + if (ui->advancedPathOptions->isChecked()) { + return + checkAdvancedPath(ui->base->text()) && + checkVarPath(ui->downloads->text()); + } else { + return m_checker.checkPath(ui->location->text()); + } +} + +bool PathsPage::checkAdvancedPath(const QString& path) const +{ + return m_advancedChecker.checkPath(path); +} + +bool PathsPage::checkVarPath(QString path) const +{ + path = PathSettings::resolve(path, ui->base->text()); + return checkAdvancedPath(path); +} + +void PathsPage::onAdvanced() +{ + if (ui->advancedPathOptions->isChecked()) { + ui->base->setText(ui->location->text()); + ui->pathPages->setCurrentIndex(1); + } else { + ui->location->setText(ui->base->text()); + ui->pathPages->setCurrentIndex(0); + } + + checkPaths(); +} + +void PathsPage::setPaths(const QString& name, bool force) +{ + const auto root = InstanceManager::instance().instancesPath(); + const auto path = QDir::toNativeSeparators(root + "/" + name); + + setIfEmpty(ui->location, path, force); + + setIfEmpty(ui->base, path, force); + setIfEmpty(ui->downloads, makeDefaultPath(AppConfig::downloadPath()), force); + setIfEmpty(ui->mods, makeDefaultPath(AppConfig::modsPath()), force); + setIfEmpty(ui->profiles, makeDefaultPath(AppConfig::profilesPath()), force); + setIfEmpty(ui->overwrite, makeDefaultPath(AppConfig::overwritePath()), force); +} + +void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force) +{ + if (e->text().isEmpty() || force) { + e->setText(path); + } +} + + +ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) + : Page(dlg) +{ +} + +void ConfirmationPage::activated() +{ + ui->review->setPlainText(makeReview()); + ui->creationLog->clear(); +} + +QString ConfirmationPage::toLocalizedString(CreateInstanceDialog::Types t) const +{ + switch (t) + { + case CreateInstanceDialog::Global: + return QObject::tr("Global"); + + case CreateInstanceDialog::Portable: + return QObject::tr("Portable"); + + default: + return QObject::tr("Instance type: %1").arg(QObject::tr("?")); + } +} + +QString ConfirmationPage::makeReview() const +{ + QStringList lines; + const auto paths = m_dlg.paths(); + + lines.push_back(QObject::tr("Instance type: %1").arg(toLocalizedString(m_dlg.instanceType()))); + lines.push_back(QObject::tr("Instance location: %1").arg(m_dlg.dataPath())); + + if (m_dlg.instanceType() != CreateInstanceDialog::Portable) { + lines.push_back(QObject::tr("Instance name: %1").arg(m_dlg.instanceName())); + } + + if (paths.downloads.isEmpty()) { + // simple settings + if (paths.base != m_dlg.dataPath()) { + lines.push_back(QObject::tr("Base directory: %1").arg(paths.base)); + } + } else { + // advanced settings + lines.push_back(QObject::tr("Base directory: %1").arg(paths.base)); + lines.push_back(dirLine(QObject::tr("Downloads"), paths.downloads)); + lines.push_back(dirLine(QObject::tr("Mods"), paths.mods)); + lines.push_back(dirLine(QObject::tr("Profiles"), paths.profiles)); + lines.push_back(dirLine(QObject::tr("Overwrite"), paths.overwrite)); + } + + // game + QString name = m_dlg.game()->gameName(); + if (!m_dlg.gameEdition().isEmpty()) { + name += " (" + m_dlg.gameEdition() + ")"; + } + + lines.push_back(QObject::tr("Game: %1").arg(name)); + lines.push_back(QObject::tr("Game location: %1").arg(m_dlg.gameLocation())); + + return lines.join("\n"); +} + +QString ConfirmationPage::dirLine(const QString& caption, const QString& path) const +{ + return QString(" - %1: %2").arg(caption).arg(path); +} + +} // namespace diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h new file mode 100644 index 00000000..0baff666 --- /dev/null +++ b/src/createinstancedialogpages.h @@ -0,0 +1,219 @@ +#ifndef MODORGANIZER_CREATEINSTANCEDIALOGPAGES_INCLUDED +#define MODORGANIZER_CREATEINSTANCEDIALOGPAGES_INCLUDED + +#include +#include +#include +#include "createinstancedialog.h" + +namespace MOBase { class IPluginGame; } + +namespace cid +{ + +QString makeDefaultPath(const std::wstring& dir); + +class PathChecker +{ +public: + PathChecker(QLabel* existsLabel, QLabel* invalidLabel); + + QString sanitizeFileName(const QString& name) const; + + // same thing as above, but allows path separators and colons + // + QString sanitizePath(const QString& path) const; + + bool checkName(QString parentDir, QString name) const; + bool checkPath(QString path) const; + +private: + QLabel* m_exists; + QString m_existsOriginal; + + QLabel* m_invalid; + QString m_invalidOriginal; + + void setPossiblePlaceholder( + QLabel* label, const QString& s, const QString& arg) const; +}; + + +class Page +{ +public: + Page(CreateInstanceDialog& dlg); + + virtual bool ready() const; + virtual bool skip() const; + virtual void activated(); + + void updateNavigation(); + void next(); + + virtual CreateInstanceDialog::Types selectedInstanceType() const; + virtual MOBase::IPluginGame* selectedGame() const; + virtual QString selectedGameLocation() const; + virtual QString selectedGameEdition() const; + virtual QString selectedInstanceName() const; + virtual CreateInstanceDialog::Paths selectedPaths() const; + +protected: + Ui::CreateInstanceDialog* ui; + CreateInstanceDialog& m_dlg; + const PluginContainer& m_pc; +}; + + +class InfoPage : public Page +{ +public: + InfoPage(CreateInstanceDialog& dlg); +}; + + +class TypePage : public Page +{ +public: + TypePage(CreateInstanceDialog& dlg); + + bool ready() const override; + CreateInstanceDialog::Types selectedInstanceType() const override; + + void global(); + void portable(); + +private: + CreateInstanceDialog::Types m_type; +}; + + +class GamePage : public Page +{ +public: + GamePage(CreateInstanceDialog& dlg); + + bool ready() const override; + MOBase::IPluginGame* selectedGame() const override; + QString selectedGameLocation() const override; + + void select(MOBase::IPluginGame* game); + void selectCustom(); + + void warnUnrecognized(const QString& path); + +private: + struct Game + { + MOBase::IPluginGame* game = nullptr; + QCommandLinkButton* button = nullptr; + QString dir; + bool installed = false; + + Game(MOBase::IPluginGame* g); + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; + }; + + std::vector> m_games; + Game* m_selection; + + + std::vector sortedGamePlugins() const; + Game* findGame(MOBase::IPluginGame* game); + void createGames(); + void updateButton(Game* g); + void selectButton(Game* g); + QCommandLinkButton* createCustomButton(); + void createGameButton(Game* g); + void fillList(); + Game* checkInstallation(const QString& path, Game* g); + MOBase::IPluginGame* findAnotherGame(const QString& path); + bool confirmUnknown(const QString& path, MOBase::IPluginGame* game); + MOBase::IPluginGame* confirmOtherGame( + const QString& path, + MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame); +}; + + +class EditionsPage : public Page +{ +public: + EditionsPage(CreateInstanceDialog& dlg); + + bool ready() const override; + bool skip() const override; + void activated() override; + QString selectedGameEdition() const override; + + void select(const QString& variant); + +private: + MOBase::IPluginGame* m_previousGame; + std::vector m_buttons; + QString m_selection; + + void fillList(); +}; + + +class NamePage : public Page +{ +public: + NamePage(CreateInstanceDialog& dlg); + + bool ready() const override; + bool skip() const override; + void activated() override; + QString selectedInstanceName() const override; + +private: + PathChecker m_checker; + QString m_originalLabel; + bool m_modified; + bool m_okay; + + void onChanged(); + void updateWarnings(); +}; + + +class PathsPage : public Page +{ +public: + PathsPage(CreateInstanceDialog& dlg); + + bool ready() const override; + void activated() override; + + CreateInstanceDialog::Paths selectedPaths() const override; + +private: + PathChecker m_checker, m_advancedChecker; + QString m_lastInstanceName; + + void onChanged(); + bool checkPaths() const; + bool checkAdvancedPath(const QString& path) const; + bool checkVarPath(QString path) const; + void onAdvanced(); + void setPaths(const QString& name, bool force); + void setIfEmpty(QLineEdit* e, const QString& path, bool force); +}; + + +class ConfirmationPage : public Page +{ +public: + ConfirmationPage(CreateInstanceDialog& dlg); + + void activated() override; + + QString toLocalizedString(CreateInstanceDialog::Types t) const; + QString makeReview() const; + QString dirLine(const QString& caption, const QString& path) const; +}; + +} // namespace + +#endif // MODORGANIZER_CREATEINSTANCEDIALOGPAGES_INCLUDED -- cgit v1.3.1 From 4c5e3da2334a1d0c474148be8881b46d6ca6d3fa Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 17 Aug 2020 05:06:26 -0400 Subject: moved nexus api stuff to GlobalSettings pass a pointer to Settings around for things that can be called without settings, when creating the first instance added dummy plugin list, mod list and iorganizer to initialize plugins without an instance moved PluginContainer into the core filter, had nothing to do with the plugins list NexusInterface is now created manually instead of being a static singleton because it needs to know if the settings are available --- src/CMakeLists.txt | 2 +- src/createinstancedialog.cpp | 9 +- src/createinstancedialog.h | 6 +- src/createinstancedialogpages.cpp | 5 +- src/instancemanagerdialog.cpp | 2 +- src/main.cpp | 27 +++-- src/mainwindow.cpp | 4 +- src/modlist.cpp | 41 ++++++++ src/modlist.h | 14 +++ src/nexusinterface.cpp | 28 +++-- src/nexusinterface.h | 6 +- src/nxmaccessmanager.cpp | 37 +++++-- src/nxmaccessmanager.h | 11 +- src/organizercore.cpp | 4 +- src/organizerproxy.cpp | 212 ++++++++++++++++++++++++++++++++++++++ src/organizerproxy.h | 59 +++++++++++ src/plugincontainer.cpp | 88 ++++++++++------ src/pluginlist.cpp | 60 +++++++++++ src/pluginlist.h | 18 ++++ src/settings.cpp | 62 +++++------ src/settings.h | 36 +++---- src/settingsdialognexus.cpp | 18 ++-- src/settingsdialognexus.h | 4 +- 23 files changed, 623 insertions(+), 130 deletions(-) (limited to 'src/CMakeLists.txt') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 844f5e19..eec33460 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,6 +33,7 @@ add_filter(NAME src/core GROUPS nexusinterface nxmaccessmanager organizercore + plugincontainer apiuseraccount processrunner qdirfiletree @@ -136,7 +137,6 @@ add_filter(NAME src/modlist GROUPS ) add_filter(NAME src/plugins GROUPS - plugincontainer pluginlist pluginlistsortproxy pluginlistview diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index f4140e01..0f62fcf6 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -11,8 +11,8 @@ using namespace MOBase; CreateInstanceDialog::CreateInstanceDialog( - const PluginContainer& pc, QWidget *parent) : - QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), + const PluginContainer& pc, Settings* s, QWidget *parent) : + QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), m_settings(s), m_switching(false) { using namespace cid; @@ -55,6 +55,11 @@ const PluginContainer& CreateInstanceDialog::pluginContainer() return m_pc; } +Settings* CreateInstanceDialog::settings() +{ + return m_settings; +} + bool CreateInstanceDialog::isOnLastPage() const { for (int i=ui->pages->currentIndex() + 1; i < ui->pages->count(); ++i) { diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index 95d4fa68..0841ef29 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -8,6 +8,7 @@ namespace Ui { class CreateInstanceDialog; }; namespace cid { class Page; } class PluginContainer; +class Settings; class CreateInstanceDialog : public QDialog { @@ -47,12 +48,14 @@ public: explicit CreateInstanceDialog( - const PluginContainer& pc, QWidget *parent = nullptr); + const PluginContainer& pc, Settings* s, QWidget *parent = nullptr); ~CreateInstanceDialog(); Ui::CreateInstanceDialog* getUI(); + const PluginContainer& pluginContainer(); + Settings* settings(); void next(); void back(); @@ -77,6 +80,7 @@ public: private: std::unique_ptr ui; const PluginContainer& m_pc; + Settings* m_settings; std::vector> m_pages; QString m_originalNext; bool m_switching; diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 73b265c8..d809079d 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -964,7 +964,8 @@ NexusPage::NexusPage(CreateInstanceDialog& dlg) : Page(dlg), m_skip(false) { m_connectionUI.reset(new NexusConnectionUI( - Settings::instance(), &m_dlg, + &m_dlg, + dlg.settings(), ui->nexusConnect, nullptr, ui->nexusManual, @@ -972,7 +973,7 @@ NexusPage::NexusPage(CreateInstanceDialog& dlg) // just check it once, or connecting and then going back and forth would skip // the page, which would be unexpected - m_skip = Settings::instance().nexus().hasApiKey(); + m_skip = GlobalSettings::hasNexusApiKey(); } NexusPage::~NexusPage() = default; diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index 3b9dd344..7b8522ee 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -718,7 +718,7 @@ void InstanceManagerDialog::onSelection() void InstanceManagerDialog::createNew() { - CreateInstanceDialog dlg(m_pc, this); + CreateInstanceDialog dlg(m_pc, &Settings::instance(), this); if (dlg.exec() != QDialog::Accepted) { return; } diff --git a/src/main.cpp b/src/main.cpp index 93580931..fd5a47c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ along with Mod Organizer. If not, see . #include "nxmaccessmanager.h" #include "instancemanager.h" #include "instancemanagerdialog.h" +#include "createinstancedialog.h" #include "organizercore.h" #include "env.h" #include "envmodule.h" @@ -329,6 +330,9 @@ int runApplication( // this must outlive `organizer` std::unique_ptr pluginContainer; + log::debug("initializing nexus interface"); + NexusInterface ni(&settings); + log::debug("initializing core"); OrganizerCore organizer(settings); if (!organizer.bootstrap()) { @@ -394,8 +398,8 @@ int runApplication( auto splash = createSplash(settings, dataPath, game); QString apiKey; - if (settings.nexus().apiKey(apiKey)) { - NexusInterface::instance().getAccessManager()->apiCheck(apiKey); + if (GlobalSettings::nexusApiKey(apiKey)) { + ni.getAccessManager()->apiCheck(apiKey); } log::debug("initializing tutorials"); @@ -415,8 +419,7 @@ int runApplication( // set up main window and its data structures MainWindow mainWindow(settings, organizer, *pluginContainer); - NexusInterface::instance() - .getAccessManager()->setTopLevelWidget(&mainWindow); + ni.getAccessManager()->setTopLevelWidget(&mainWindow); QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, SLOT(setStyleFile(QString))); @@ -443,8 +446,7 @@ int runApplication( res = application.exec(); mainWindow.close(); - NexusInterface::instance() - .getAccessManager()->setTopLevelWidget(nullptr); + ni.getAccessManager()->setTopLevelWidget(nullptr); } settings.geometry().resetIfNeeded(); @@ -507,6 +509,7 @@ QString determineDataPath(const cl::CommandLine& cl) } } + int doOneRun( cl::CommandLine& cl, MOApplication& application, SingleInstance& instance) { @@ -515,6 +518,18 @@ int doOneRun( // resets things when MO is "restarted" resetForRestart(cl); + + //{ + // NexusInterface ni(nullptr); + // + // PluginContainer pc(nullptr); + // pc.loadPlugins(); + // + // CreateInstanceDialog dlg(pc, nullptr); + // dlg.exec(); + //} + + const QString dataPath = determineDataPath(cl); if (dataPath.isEmpty()) { return 1; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3ef34e93..9ad51510 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -4226,7 +4226,7 @@ void MainWindow::checkModsForUpdates() NexusInterface::instance().requestTrackingInfo(this, QVariant(), QString()); } else { QString apiKey; - if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); NexusInterface::instance().getAccessManager()->apiCheck(apiKey); } else { @@ -5434,7 +5434,7 @@ void MainWindow::modUpdateCheck(std::multimap IDs) ModInfo::manualUpdateCheck(this, IDs); } else { QString apiKey; - if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); NexusInterface::instance().getAccessManager()->apiCheck(apiKey); } else diff --git a/src/modlist.cpp b/src/modlist.cpp index 1f845999..bf9aef83 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -1528,3 +1528,44 @@ void ModList::disableSelected(const QItemSelectionModel *selectionModel) m_Profile->setModsEnabled(QList(), modsToDisable); } } + + +QString DummyModList::displayName(const QString &internalName) const +{ + return {}; +} + +QStringList DummyModList::allMods() const +{ + return {}; +} + +IModList::ModStates DummyModList::state(const QString &name) const +{ + return 0; +} + +bool DummyModList::setActive(const QString &name, bool active) +{ + return true; +} + +int DummyModList::priority(const QString &name) const +{ + return -1; +} + +bool DummyModList::setPriority(const QString &name, int newPriority) +{ + return true; +} + +bool DummyModList::onModStateChanged(const std::function &func) +{ + return true; +} + +bool DummyModList::onModMoved(const std::function &func) +{ + return true; +} diff --git a/src/modlist.h b/src/modlist.h index 385ca04c..3ab486c9 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -397,5 +397,19 @@ private: }; + +class DummyModList : public MOBase::IModList +{ +public: + QString displayName(const QString &internalName) const override; + QStringList allMods() const override; + ModStates state(const QString &name) const override; + bool setActive(const QString &name, bool active) override; + int priority(const QString &name) const override; + bool setPriority(const QString &name, int newPriority) override; + bool onModStateChanged(const std::function &func) override; + bool onModMoved(const std::function &func) override; +}; + #endif // MODLIST_H diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 396cec11..1364d7e1 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -23,6 +23,7 @@ along with Mod Organizer. If not, see . #include "nxmaccessmanager.h" #include "selectiondialog.h" #include "bbcode.h" +#include "settings.h" #include #include "shared/util.h" #include @@ -235,31 +236,42 @@ APILimits NexusInterface::parseLimits( } -NexusInterface::NexusInterface() +static NexusInterface* g_instance = nullptr; + +NexusInterface::NexusInterface(Settings* s) : m_PluginContainer(nullptr) { + MO_ASSERT(!g_instance); + g_instance = this; + m_User.limits(defaultAPILimits()); m_MOVersion = createVersionInfo(); - m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); + m_AccessManager = new NXMAccessManager( + this, s, m_MOVersion.displayString(3)); + m_DiskCache = new QNetworkDiskCache(this); + connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); } -NXMAccessManager *NexusInterface::getAccessManager() +NexusInterface::~NexusInterface() { - return m_AccessManager; + cleanup(); + + MO_ASSERT(g_instance == this); + g_instance = nullptr; } -NexusInterface::~NexusInterface() +NXMAccessManager *NexusInterface::getAccessManager() { - cleanup(); + return m_AccessManager; } NexusInterface& NexusInterface::instance() { - static NexusInterface ni; - return ni; + MO_ASSERT(g_instance); + return *g_instance; } void NexusInterface::setCacheDirectory(const QString &directory) diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 72f30a38..cf365b2e 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -40,7 +40,7 @@ namespace MOBase { class IPluginGame; } class NexusInterface; class NXMAccessManager; - +class Settings; /** * @brief convenience class to make nxm requests easier @@ -153,7 +153,9 @@ public: static APILimits parseLimits(const QNetworkReply* reply); static APILimits parseLimits(const QList& headers); + NexusInterface(Settings* s); ~NexusInterface(); + static NexusInterface& instance(); /** @@ -533,8 +535,6 @@ private: static const int MAX_ACTIVE_DOWNLOADS = 6; private: - - NexusInterface(); void nextRequest(); void requestFinished(std::list::iterator iter); MOBase::IPluginGame *getGame(QString gameName) const; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 2fe676ba..6c8b9054 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -48,8 +48,8 @@ const QString NexusSSO("wss://sso.nexusmods.com"); const QString NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2"); -ValidationProgressDialog::ValidationProgressDialog(NexusKeyValidator& v) - : m_validator(v), m_updateTimer(nullptr), m_first(true) +ValidationProgressDialog::ValidationProgressDialog(Settings* s, NexusKeyValidator& v) + : m_settings(s), m_validator(v), m_updateTimer(nullptr), m_first(true) { ui.reset(new Ui::ValidationProgressDialog); ui->setupUi(this); @@ -98,7 +98,10 @@ void ValidationProgressDialog::stop() void ValidationProgressDialog::showEvent(QShowEvent* e) { if (m_first) { - Settings::instance().geometry().centerOnMainWindowMonitor(this); + if (m_settings) { + m_settings->geometry().centerOnMainWindowMonitor(this); + } + m_first = false; } } @@ -592,8 +595,8 @@ void ValidationAttempt::cleanup() } -NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am) - : m_manager(am) +NexusKeyValidator::NexusKeyValidator(Settings* s, NXMAccessManager& am) + : m_settings(s), m_manager(am) { } @@ -602,6 +605,15 @@ NexusKeyValidator::~NexusKeyValidator() cancel(); } +std::vector NexusKeyValidator::getTimeouts() const +{ + if (m_settings) { + return m_settings->nexus().validationTimeouts(); + } else { + return {10s, 15s, 20s}; + } +} + void NexusKeyValidator::start(const QString& key, Behaviour b) { if (isActive()) { @@ -611,7 +623,7 @@ void NexusKeyValidator::start(const QString& key, Behaviour b) m_key = key; - const auto timeouts = Settings::instance().nexus().validationTimeouts(); + const auto timeouts = getTimeouts(); switch (b) { @@ -755,10 +767,11 @@ void NexusKeyValidator::setFinished( } -NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) +NXMAccessManager::NXMAccessManager(QObject *parent, Settings* s, const QString &moVersion) : QNetworkAccessManager(parent) + , m_Settings(s) , m_MOVersion(moVersion) - , m_validator(*this) + , m_validator(s, *this) , m_validationState(NotChecked) { m_validator.finished = [&](auto&& r, auto&& m, auto&& u) { @@ -769,8 +782,10 @@ NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) onValidatorAttemptFinished(a); }; - setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( - Settings::instance().paths().cache() + "/nexus_cookies.dat"))); + if (m_Settings) { + setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( + m_Settings->paths().cache() + "/nexus_cookies.dat"))); + } if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) { // why is this necessary all of a sudden? @@ -963,7 +978,7 @@ void NXMAccessManager::clearApiKey() void NXMAccessManager::startProgress() { if (!m_ProgressDialog) { - m_ProgressDialog.reset(new ValidationProgressDialog(m_validator)); + m_ProgressDialog.reset(new ValidationProgressDialog(m_Settings, m_validator)); } m_ProgressDialog->start(); diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index 6a45d880..60100467 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -33,6 +33,7 @@ along with Mod Organizer. If not, see . namespace MOBase { class IPluginGame; } namespace Ui { class ValidationProgressDialog; } class NXMAccessManager; +class Settings; class NexusSSOLogin { @@ -146,7 +147,7 @@ public: std::function finished; std::function attemptFinished; - NexusKeyValidator(NXMAccessManager& am); + NexusKeyValidator(Settings* s, NXMAccessManager& am); ~NexusKeyValidator(); void start(const QString& key, Behaviour b); @@ -157,11 +158,13 @@ public: const ValidationAttempt* currentAttempt() const; private: + Settings* m_settings; NXMAccessManager& m_manager; QString m_key; std::vector> m_attempts; void createAttempts(const std::vector& timeouts); + std::vector getTimeouts() const; bool nextTry(); void onAttemptSuccess(const ValidationAttempt& a, const APIUserAccount& u); @@ -178,7 +181,7 @@ class ValidationProgressDialog : public QDialog Q_OBJECT; public: - ValidationProgressDialog(NexusKeyValidator& v); + ValidationProgressDialog(Settings* s, NexusKeyValidator& v); void setParentWidget(QWidget* w); @@ -191,6 +194,7 @@ protected: private: std::unique_ptr ui; + Settings* m_settings; NexusKeyValidator& m_validator; QTimer* m_updateTimer; bool m_first; @@ -209,7 +213,7 @@ class NXMAccessManager : public QNetworkAccessManager { Q_OBJECT public: - NXMAccessManager(QObject *parent, const QString &moVersion); + NXMAccessManager(QObject *parent, Settings* s, const QString &moVersion); void setTopLevelWidget(QWidget* w); @@ -264,6 +268,7 @@ private: }; QWidget* m_TopLevel; + Settings* m_Settings; mutable std::unique_ptr m_ProgressDialog; QString m_MOVersion; NexusKeyValidator m_validator; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 7fd8c33b..0d561581 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -339,7 +339,7 @@ bool OrganizerCore::nexusApi(bool retry) return false; } else { QString apiKey; - if (m_Settings.nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { // credentials stored or user entered them manually log::debug("attempt to verify nexus api key"); accessManager->apiCheck(apiKey); @@ -1426,7 +1426,7 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function f) f(); } else { QString apiKey; - if (settings().nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { doAfterLogin([f]{ f(); }); NexusInterface::instance().getAccessManager()->apiCheck(apiKey); } else { diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 45efc00c..8cc95a8b 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -287,3 +287,215 @@ bool OrganizerProxy::onPluginSettingChanged(std::functiononPluginSettingChanged(func); } + + + +DummyOrganizerProxy::DummyOrganizerProxy(const QString &pluginName) : + m_PluginName(pluginName), + m_mods(new DummyModList), m_plugins(new DummyPluginList) +{ +} + +DummyOrganizerProxy::~DummyOrganizerProxy() = default; + +IModRepositoryBridge *DummyOrganizerProxy::createNexusBridge() const +{ + return nullptr; +} + +QString DummyOrganizerProxy::profileName() const +{ + return {}; +} + +QString DummyOrganizerProxy::profilePath() const +{ + return {}; +} + +QString DummyOrganizerProxy::downloadsPath() const +{ + return {}; +} + +QString DummyOrganizerProxy::overwritePath() const +{ + return {}; +} + +QString DummyOrganizerProxy::basePath() const +{ + return {}; +} + +QString DummyOrganizerProxy::modsPath() const +{ + return {}; +} + +VersionInfo DummyOrganizerProxy::appVersion() const +{ + return {}; +} + +IModInterface *DummyOrganizerProxy::getMod(const QString &name) const +{ + return nullptr; +} + +IPluginGame *DummyOrganizerProxy::getGame(const QString &gameName) const +{ + return nullptr; +} + +IModInterface *DummyOrganizerProxy::createMod(MOBase::GuessedValue &name) +{ + return nullptr; +} + +bool DummyOrganizerProxy::removeMod(IModInterface *mod) +{ + return true; +} + +void DummyOrganizerProxy::modDataChanged(IModInterface *mod) +{ +} + +QVariant DummyOrganizerProxy::pluginSetting(const QString &pluginName, const QString &key) const +{ + if (key == "enabled") { + return true; + } + + return {}; +} + +void DummyOrganizerProxy::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value) +{ +} + +QVariant DummyOrganizerProxy::persistent(const QString &pluginName, const QString &key, const QVariant &def) const +{ + return {}; +} + +void DummyOrganizerProxy::setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync) +{ +} + +QString DummyOrganizerProxy::pluginDataPath() const +{ + return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()) + "/data"; +} + +HANDLE DummyOrganizerProxy::startApplication( + const QString& exe, const QStringList& args, const QString &cwd, + const QString& profile, const QString &overwrite, bool ignoreOverwrite) +{ + return INVALID_HANDLE_VALUE; +} + +bool DummyOrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const +{ + return true; +} + +bool DummyOrganizerProxy::onAboutToRun(const std::function &func) +{ + return true; +} + +bool DummyOrganizerProxy::onFinishedRun(const std::function &func) +{ + return true; +} + +bool DummyOrganizerProxy::onModInstalled(const std::function &func) +{ + return true; +} + +bool DummyOrganizerProxy::onUserInterfaceInitialized(std::function const& func) +{ + return true; +} + +bool DummyOrganizerProxy::onProfileChanged(std::function const& func) +{ + return true; +} + +bool DummyOrganizerProxy::onPluginSettingChanged(std::function const& func) +{ + return true; +} + +void DummyOrganizerProxy::refreshModList(bool saveChanges) +{ +} + +IModInterface *DummyOrganizerProxy::installMod(const QString &fileName, const QString &nameSuggestion) +{ + return nullptr; +} + +QString DummyOrganizerProxy::resolvePath(const QString &fileName) const +{ + return {}; +} + +QStringList DummyOrganizerProxy::listDirectories(const QString &directoryName) const +{ + return {}; +} + +QStringList DummyOrganizerProxy::findFiles(const QString &path, const std::function &filter) const +{ + return {}; +} + +QStringList DummyOrganizerProxy::findFiles(const QString& path, const QStringList& globFilters) const +{ + return {}; +} + +QStringList DummyOrganizerProxy::getFileOrigins(const QString &fileName) const +{ + return {}; +} + +QList DummyOrganizerProxy::findFileInfos(const QString &path, const std::function &filter) const +{ + return {}; +} + +MOBase::IDownloadManager *DummyOrganizerProxy::downloadManager() const +{ + return nullptr; +} + +MOBase::IPluginList *DummyOrganizerProxy::pluginList() const +{ + return m_plugins.get(); +} + +MOBase::IModList *DummyOrganizerProxy::modList() const +{ + return m_mods.get(); +} + +MOBase::IProfile *DummyOrganizerProxy::profile() const +{ + return nullptr; +} + +MOBase::IPluginGame const *DummyOrganizerProxy::managedGame() const +{ + return nullptr; +} + +QStringList DummyOrganizerProxy::modsSortedByProfilePriority() const +{ + return {}; +} diff --git a/src/organizerproxy.h b/src/organizerproxy.h index 6690d612..3bd70113 100644 --- a/src/organizerproxy.h +++ b/src/organizerproxy.h @@ -81,4 +81,63 @@ private: }; + +class DummyOrganizerProxy : public MOBase::IOrganizer +{ +public: + DummyOrganizerProxy(const QString &pluginName); + ~DummyOrganizerProxy(); + + virtual MOBase::IModRepositoryBridge *createNexusBridge() const; + virtual QString profileName() const; + virtual QString profilePath() const; + virtual QString downloadsPath() const; + virtual QString overwritePath() const; + virtual QString basePath() const; + virtual QString modsPath() const; + virtual MOBase::VersionInfo appVersion() const; + virtual MOBase::IModInterface *getMod(const QString &name) const; + virtual MOBase::IPluginGame *getGame(const QString &gameName) const; + virtual MOBase::IModInterface *createMod(MOBase::GuessedValue &name); + virtual bool removeMod(MOBase::IModInterface *mod); + virtual void modDataChanged(MOBase::IModInterface *mod); + virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const; + virtual void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value); + virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const; + virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true); + virtual QString pluginDataPath() const; + virtual MOBase::IModInterface *installMod(const QString &fileName, const QString &nameSuggestion = QString()); + virtual QString resolvePath(const QString &fileName) const; + virtual QStringList listDirectories(const QString &directoryName) const; + virtual QStringList findFiles(const QString &path, const std::function &filter) const override; + virtual QStringList findFiles(const QString &path, const QStringList &globFilters) const override; + virtual QStringList getFileOrigins(const QString &fileName) const; + virtual QList findFileInfos(const QString &path, const std::function &filter) const; + + virtual MOBase::IDownloadManager *downloadManager() const; + virtual MOBase::IPluginList *pluginList() const; + virtual MOBase::IModList *modList() const; + virtual MOBase::IProfile *profile() const override; + virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "", + const QString &profile = "", const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false); + virtual bool waitForApplication(HANDLE handle, LPDWORD exitCode = nullptr) const; + virtual void refreshModList(bool saveChanges); + + virtual bool onAboutToRun(const std::function &func); + virtual bool onFinishedRun(const std::function &func); + virtual bool onModInstalled(const std::function &func); + virtual bool onUserInterfaceInitialized(std::function const& func); + virtual bool onProfileChanged(std::function const& func); + virtual bool onPluginSettingChanged(std::function const& func); + + virtual MOBase::IPluginGame const *managedGame() const; + + virtual QStringList modsSortedByProfilePriority() const; + +private: + const QString &m_PluginName; + std::unique_ptr m_plugins; + std::unique_ptr m_mods; +}; + #endif // ORGANIZERPROXY_H diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 4771359d..6f2670dd 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -173,10 +173,21 @@ bool PluginContainer::verifyPlugin(IPlugin *plugin) { if (plugin == nullptr) { return false; - } else if (!plugin->init(new OrganizerProxy(m_Organizer, this, plugin))) { + } + + IOrganizer* proxy = nullptr; + + if (m_Organizer) { + proxy = new OrganizerProxy(m_Organizer, this, plugin); + } else { + proxy = new DummyOrganizerProxy(plugin); + } + + if (!plugin->init(proxy)) { log::warn("plugin failed to initialize"); return false; } + return true; } @@ -200,7 +211,9 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) return false; } plugin->setProperty("filename", fileName); - m_Organizer->settings().plugins().registerPlugin(pluginObj); + if (m_Organizer) { + m_Organizer->settings().plugins().registerPlugin(pluginObj); + } } { // diagnosis plugin @@ -244,7 +257,9 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) IPluginInstaller *installer = qobject_cast(plugin); if (verifyPlugin(installer)) { bf::at_key(m_Plugins).push_back(installer); - m_Organizer->installationManager()->registerInstaller(installer); + if (m_Organizer) { + m_Organizer->installationManager()->registerInstaller(installer); + } return true; } } @@ -317,7 +332,7 @@ void PluginContainer::unloadPlugins() } // disconnect all slots before unloading plugins so plugins don't have to take care of that - if (m_Organizer != nullptr) { + if (m_Organizer) { m_Organizer->disconnectPlugins(); } @@ -363,24 +378,28 @@ void PluginContainer::loadPlugins() registerPlugin(plugin, ""); } - QFile loadCheck(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp"); - if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) { - // oh, there was a failed plugin load last time. Find out which plugin was loaded last - QString fileName; - while (!loadCheck.atEnd()) { - fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed(); - } - if (QMessageBox::question(nullptr, QObject::tr("Plugin error"), - QObject::tr("It appears the plugin \"%1\" failed to load last startup and caused MO to crash. Do you want to disable it?\n" - "(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)").arg(fileName), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { - m_Organizer->settings().plugins().addBlacklist(fileName); + QFile loadCheck; + + if (m_Organizer) { + loadCheck.setFileName(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp"); + if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) { + // oh, there was a failed plugin load last time. Find out which plugin was loaded last + QString fileName; + while (!loadCheck.atEnd()) { + fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed(); + } + if (QMessageBox::question(nullptr, QObject::tr("Plugin error"), + QObject::tr("It appears the plugin \"%1\" failed to load last startup and caused MO to crash. Do you want to disable it?\n" + "(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)").arg(fileName), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { + m_Organizer->settings().plugins().addBlacklist(fileName); + } + loadCheck.close(); } - loadCheck.close(); - } - loadCheck.open(QIODevice::WriteOnly); + loadCheck.open(QIODevice::WriteOnly); + } QString pluginPath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath)); @@ -388,13 +407,20 @@ void PluginContainer::loadPlugins() while (iter.hasNext()) { iter.next(); - if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) { - log::debug("plugin \"{}\" blacklisted", iter.fileName()); - continue; + + if (m_Organizer) { + if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) { + log::debug("plugin \"{}\" blacklisted", iter.fileName()); + continue; + } } - loadCheck.write(iter.fileName().toUtf8()); - loadCheck.write("\n"); - loadCheck.flush(); + + if (loadCheck.isOpen()) { + loadCheck.write(iter.fileName().toUtf8()); + loadCheck.write("\n"); + loadCheck.flush(); + } + QString pluginName = iter.filePath(); if (QLibrary::isLibrary(pluginName)) { std::unique_ptr pluginLoader(new QPluginLoader(pluginName, this)); @@ -416,12 +442,16 @@ void PluginContainer::loadPlugins() } // remove the load check file on success - loadCheck.remove(); + if (loadCheck.isOpen()) { + loadCheck.remove(); + } - bf::at_key(m_Plugins).push_back(m_Organizer); bf::at_key(m_Plugins).push_back(this); - m_Organizer->connectPlugins(this); + if (m_Organizer) { + bf::at_key(m_Plugins).push_back(m_Organizer); + m_Organizer->connectPlugins(this); + } } diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index a4c1ec6d..7134d246 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -1671,3 +1671,63 @@ void PluginList::managedGameChanged(const IPluginGame *gamePlugin) { m_GamePlugin = gamePlugin; } + + + +QStringList DummyPluginList::pluginNames() const +{ + return {}; +} + +IPluginList::PluginStates DummyPluginList::state(const QString &name) const +{ + return 0; +} + +void DummyPluginList::setState(const QString &name, PluginStates state) +{ +} + +int DummyPluginList::priority(const QString &name) const +{ + return -1; +} + +int DummyPluginList::loadOrder(const QString &name) const +{ + return -1; +} + +void DummyPluginList::setLoadOrder(const QStringList &pluginList) +{ +} + +bool DummyPluginList::isMaster(const QString &name) const +{ + return false; +} + +QStringList DummyPluginList::masters(const QString &name) const +{ + return {}; +} + +QString DummyPluginList::origin(const QString &name) const +{ + return {}; +} + +bool DummyPluginList::onRefreshed(const std::function &callback) +{ + return true; +} + +bool DummyPluginList::onPluginMoved(const std::function &func) +{ + return true; +} + +bool DummyPluginList::onPluginStateChanged(const std::function &func) +{ + return true; +} diff --git a/src/pluginlist.h b/src/pluginlist.h index 0b49b86f..bfadaf4f 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -410,6 +410,24 @@ private: bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const; }; + +class DummyPluginList : public MOBase::IPluginList +{ +public: + QStringList pluginNames() const override; + PluginStates state(const QString &name) const override; + void setState(const QString &name, PluginStates state) override; + int priority(const QString &name) const override; + int loadOrder(const QString &name) const override; + void setLoadOrder(const QStringList &pluginList) override; + bool isMaster(const QString &name) const override; + QStringList masters(const QString &name) const override; + QString origin(const QString &name) const override; + bool onRefreshed(const std::function &callback) override; + bool onPluginMoved(const std::function &func) override; + bool onPluginStateChanged(const std::function &func) override; +}; + #pragma warning(pop) #endif // PLUGINLIST_H diff --git a/src/settings.cpp b/src/settings.cpp index b286510d..593d66bf 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1812,37 +1812,6 @@ NexusSettings::NexusSettings(Settings& parent, QSettings& settings) { } -bool NexusSettings::apiKey(QString& apiKey) const -{ - QString tempKey = getWindowsCredential("APIKEY"); - if (tempKey.isEmpty()) - return false; - - apiKey = tempKey; - return true; -} - -bool NexusSettings::setApiKey(const QString& apiKey) -{ - if (!setWindowsCredential("APIKEY", apiKey)) { - const auto e = GetLastError(); - log::error("Storing API key failed: {}", formatSystemMessage(e)); - return false; - } - - return true; -} - -bool NexusSettings::clearApiKey() -{ - return setApiKey(""); -} - -bool NexusSettings::hasApiKey() const -{ - return !getWindowsCredential("APIKEY").isEmpty(); -} - bool NexusSettings::endorsementIntegration() const { return get(m_Settings, "Settings", "endorsement_integration", true); @@ -2224,6 +2193,37 @@ void GlobalSettings::setHideTutorialQuestion(bool b) settings().setValue("HideTutorialQuestion", b); } +bool GlobalSettings::nexusApiKey(QString& apiKey) +{ + QString tempKey = getWindowsCredential("APIKEY"); + if (tempKey.isEmpty()) + return false; + + apiKey = tempKey; + return true; +} + +bool GlobalSettings::setNexusApiKey(const QString& apiKey) +{ + if (!setWindowsCredential("APIKEY", apiKey)) { + const auto e = GetLastError(); + log::error("Storing API key failed: {}", formatSystemMessage(e)); + return false; + } + + return true; +} + +bool GlobalSettings::clearNexusApiKey() +{ + return setNexusApiKey(""); +} + +bool GlobalSettings::hasNexusApiKey() +{ + return !getWindowsCredential("APIKEY").isEmpty(); +} + void GlobalSettings::resetDialogs() { setHideCreateInstanceIntro(false); diff --git a/src/settings.h b/src/settings.h index a9501b9d..f71949d2 100644 --- a/src/settings.h +++ b/src/settings.h @@ -498,24 +498,6 @@ class NexusSettings public: NexusSettings(Settings& parent, QSettings& settings); - // if the key exists from the credentials store, puts it in `apiKey` and - // returns true; otherwise, returns false and leaves `apiKey` untouched - // - bool apiKey(QString& apiKey) const; - - // sets the api key in the credentials store, removes it if empty; returns - // false on errors - // - bool setApiKey(const QString& apiKey); - - // removes the api key from the credentials store; returns false on errors - // - bool clearApiKey(); - - // returns whether an API key is currently stored - // - bool hasApiKey() const; - // returns whether endorsement integration is enabled // bool endorsementIntegration() const; @@ -844,6 +826,24 @@ public: static bool hideTutorialQuestion(); static void setHideTutorialQuestion(bool b); + // if the key exists from the credentials store, puts it in `apiKey` and + // returns true; otherwise, returns false and leaves `apiKey` untouched + // + static bool nexusApiKey(QString& apiKey); + + // sets the api key in the credentials store, removes it if empty; returns + // false on errors + // + static bool setNexusApiKey(const QString& apiKey); + + // removes the api key from the credentials store; returns false on errors + // + static bool clearNexusApiKey(); + + // returns whether an API key is currently stored + // + static bool hasNexusApiKey(); + // resets anything that the user can disable static void resetDialogs(); diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp index 54af9b59..1ed4c58f 100644 --- a/src/settingsdialognexus.cpp +++ b/src/settingsdialognexus.cpp @@ -72,13 +72,14 @@ private: NexusConnectionUI::NexusConnectionUI( - Settings& s, QWidget* parent, + Settings* s, QAbstractButton* connectButton, QAbstractButton* disconnectButton, QAbstractButton* manualButton, QListWidget* logList) : - m_parent(parent), m_settings(s), + m_parent(parent), + m_settings(s), m_connect(connectButton), m_disconnect(disconnectButton), m_manual(manualButton), @@ -96,7 +97,7 @@ NexusConnectionUI::NexusConnectionUI( QObject::connect(manualButton, &QPushButton::clicked, [&]{ manual(); }); } - if (m_settings.nexus().hasApiKey()) { + if (GlobalSettings::hasNexusApiKey()) { addLog(tr("Connected.")); } else { addLog(tr("Not connected.")); @@ -162,7 +163,7 @@ void NexusConnectionUI::validateKey(const QString& key) { if (!m_nexusValidator) { m_nexusValidator.reset(new NexusKeyValidator( - *NexusInterface::instance().getAccessManager())); + m_settings, *NexusInterface::instance().getAccessManager())); m_nexusValidator->finished = [&](auto&& r, auto&& m, auto&& u) { onValidatorFinished(r, m, u); @@ -231,7 +232,7 @@ void NexusConnectionUI::addLog(const QString& s) bool NexusConnectionUI::setKey(const QString& key) { - const bool ret = m_settings.nexus().setApiKey(key); + const bool ret = GlobalSettings::setNexusApiKey(key); updateState(); emit keyChanged(); @@ -241,7 +242,7 @@ bool NexusConnectionUI::setKey(const QString& key) bool NexusConnectionUI::clearKey() { - const auto ret = m_settings.nexus().clearApiKey(); + const auto ret = GlobalSettings::clearNexusApiKey(); NexusInterface::instance().getAccessManager()->clearApiKey(); updateState(); @@ -274,7 +275,7 @@ void NexusConnectionUI::updateState() setButton(m_disconnect, false); setButton(m_manual, true, QObject::tr("Cancel")); } - else if (m_settings.nexus().hasApiKey()) { + else if (GlobalSettings::hasNexusApiKey()) { // api key is present setButton(m_connect, false, QObject::tr("Connect to Nexus")); setButton(m_disconnect, true); @@ -327,7 +328,8 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) } m_connectionUI.reset(new NexusConnectionUI( - settings(), &dialog(), + &dialog(), + &settings(), ui->nexusConnect, ui->nexusDisconnect, ui->nexusManualKey, diff --git a/src/settingsdialognexus.h b/src/settingsdialognexus.h index a2d6a4f8..c915accf 100644 --- a/src/settingsdialognexus.h +++ b/src/settingsdialognexus.h @@ -13,8 +13,8 @@ class NexusConnectionUI : public QObject public: NexusConnectionUI( - Settings& s, QWidget* parent, + Settings* s, QAbstractButton* connectButton, QAbstractButton* disconnectButton, QAbstractButton* manualButton, @@ -30,7 +30,7 @@ signals: private: QWidget* m_parent; - Settings& m_settings; + Settings* m_settings; QAbstractButton* m_connect; QAbstractButton* m_disconnect; QAbstractButton* m_manual; -- cgit v1.3.1