summaryrefslogtreecommitdiff
path: root/src/instancemanagerdialog.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/instancemanagerdialog.cpp')
-rw-r--r--src/instancemanagerdialog.cpp853
1 files changed, 853 insertions, 0 deletions
diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp
new file mode 100644
index 00000000..3b6daeb8
--- /dev/null
+++ b/src/instancemanagerdialog.cpp
@@ -0,0 +1,853 @@
+#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 "shared/util.h"
+#include <utility.h>
+#include <report.h>
+#include <iplugingame.h>
+
+using namespace MOBase;
+
+class InstanceInfo
+{
+public:
+ struct Object
+ {
+ QString path;
+ bool mandatoryDelete;
+
+ Object(QString p, bool d=false)
+ : path(std::move(p)), mandatoryDelete(d)
+ {
+ }
+
+ bool operator<(const Object& o) const
+ {
+ if (mandatoryDelete && !o.mandatoryDelete) {
+ return true;
+ } else if (!mandatoryDelete && o.mandatoryDelete) {
+ return false;
+ }
+
+ return false;
+ }
+ };
+
+
+ InstanceInfo(QDir dir, bool isPortable)
+ : m_portable(isPortable)
+ {
+ setDir(dir);
+ }
+
+ void setDir(const QDir& dir)
+ {
+ m_dir = dir;
+ m_settings.reset(new Settings(InstanceManager::iniPath(dir)));
+ }
+
+ QString name() const
+ {
+ if (m_portable) {
+ return QObject::tr("Portable");
+ } else {
+ 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 QDir::toNativeSeparators(*n);
+ } else {
+ return {};
+ }
+ }
+
+ QString location() const
+ {
+ return QDir::toNativeSeparators(m_dir.path());
+ }
+
+ QString baseDirectory() const
+ {
+ return QDir::toNativeSeparators(m_settings->paths().base());
+ }
+
+ QString iniFile() const
+ {
+ return InstanceManager::iniPath(m_dir);
+ }
+
+ QIcon icon(const PluginContainer& plugins) const
+ {
+ const auto* game = InstanceManager::singleton().gamePluginForDirectory(
+ m_dir, plugins);
+
+ if (game)
+ return game->gameIcon();
+
+ QPixmap empty(32, 32);
+ empty.fill(QColor(0, 0, 0, 0));
+ return QIcon(empty);
+ }
+
+ bool isPortable() const
+ {
+ return m_portable;
+ }
+
+ bool isActive() const
+ {
+ auto& m = InstanceManager::singleton();
+
+ if (auto i=m.currentInstance())
+ {
+ if (m_portable) {
+ return i->isPortable();
+ } else {
+ return (i->name() == name());
+ }
+ }
+
+ return false;
+ }
+
+ // returns a list of files and folders that must be deleted when deleting
+ // this instance
+ //
+ std::vector<Object> objectsForDeletion() const
+ {
+ // native separators and ending slash
+ auto prettyDir = [](auto s) {
+ if (!s.endsWith("/") || !s.endsWith("\\")) {
+ s += "/";
+ }
+
+ return QDir::toNativeSeparators(s);
+ };
+
+ // native separators
+ auto prettyFile = [](auto s) {
+ return QDir::toNativeSeparators(s);
+ };
+
+
+ // lowercase, native separators and ending slash
+ auto canonicalDir = [](auto s) {
+ s = s.toLower();
+ if (!s.endsWith("/") || !s.endsWith("\\")) {
+ s += "/";
+ }
+
+ return QDir::toNativeSeparators(s);
+ };
+
+ // lower and native separators
+ auto canonicalFile = [](auto s) {
+ return QDir::toNativeSeparators(s.toLower());
+ };
+
+
+
+ // whether the given directory is contained in the root
+ auto dirInRoot = [&](auto root, auto dir) {
+ return canonicalDir(dir).startsWith(canonicalDir(root));
+ };
+
+ // whether the given file is contained in the root
+ auto fileInRoot = [&](auto root, auto file) {
+ return canonicalFile(file).startsWith(canonicalDir(root));
+ };
+
+
+
+
+ const auto loc = location();
+ const auto base = m_settings->paths().base();
+
+
+ // directories that might contain the individual files and directories
+ // set in the path settings
+ std::vector<Object> roots;
+
+ // a portable instance has its location in the installation directory,
+ // don't delete that
+ if (!isPortable()) {
+ if (QDir(loc).exists()) {
+ roots.push_back({loc, true});
+ }
+ }
+
+ // the base directory is the location directory by default, don't add it
+ // if it's the same
+ if (canonicalDir(base) != canonicalDir(loc)) {
+ if (QDir(base).exists()) {
+ roots.push_back({base, false});
+ }
+ }
+
+
+ // all the directories that are part of an instance; none of them are
+ // mandatory for deletion
+ const std::vector<Object> dirs = {
+ m_settings->paths().downloads(),
+ m_settings->paths().mods(),
+ m_settings->paths().cache(),
+ m_settings->paths().profiles(),
+ m_settings->paths().overwrite(),
+ m_dir.filePath(QString::fromStdWString(AppConfig::dumpsDir())),
+ m_dir.filePath(QString::fromStdWString(AppConfig::logPath())),
+ };
+
+ // all the files that are part of an instance
+ const std::vector<Object> files = {
+ {iniFile(), true}, // the ini file must be deleted
+ };
+
+
+ // this will contain the root directories, plus all the individual
+ // directories that are not inside these roots
+ std::vector<Object> cleanDirs;
+
+ for (const auto& f : dirs) {
+ bool inRoots = false;
+
+ for (const auto& root : roots) {
+ if (dirInRoot(root.path, f.path)) {
+ inRoots = true;
+ break;
+ }
+ }
+
+ if (!inRoots) {
+ // not in roots, this is a path that was changed by the user; make
+ // sure it exists
+ if (QDir(f.path).exists()) {
+ cleanDirs.push_back({prettyDir(f.path), f.mandatoryDelete});
+ }
+ }
+ }
+
+ // prepending the roots
+ for (auto itor=roots.rbegin(); itor!=roots.rend(); ++itor) {
+ cleanDirs.insert(
+ cleanDirs.begin(),
+ {prettyDir(itor->path), itor->mandatoryDelete});
+ }
+
+
+
+ // this will contain the individual files that are not inside the roots;
+ // not that this only contains the INI file for now, so most of this is
+ // useless
+ std::vector<Object> cleanFiles;
+
+ for (const auto& f : files) {
+ bool inRoots = false;
+
+ for (const auto& root : roots) {
+ if (fileInRoot(root.path, f.path)) {
+ inRoots = true;
+ break;
+ }
+ }
+
+ if (!inRoots) {
+ // not in roots, this is a path that was changed by the user; make
+ // sure it exists
+ if (QFileInfo(f.path).exists()) {
+ cleanFiles.push_back({prettyFile(f.path), f.mandatoryDelete});
+ }
+ }
+ }
+
+
+ // contains all the directories and files to be deleted
+ std::vector<Object> all;
+ all.insert(all.end(), cleanDirs.begin(), cleanDirs.end());
+ all.insert(all.end(), cleanFiles.begin(), cleanFiles.end());
+
+ // mandatory on top
+ std::stable_sort(all.begin(), all.end());
+
+ return all;
+ }
+
+private:
+ const bool m_portable;
+ QDir m_dir;
+ std::unique_ptr<Settings> m_settings;
+};
+
+
+InstanceManagerDialog::~InstanceManagerDialog() = default;
+
+InstanceManagerDialog::InstanceManagerDialog(
+ const PluginContainer& pc, QWidget *parent) :
+ QDialog(parent), ui(new Ui::InstanceManagerDialog), m_pc(pc),
+ m_model(nullptr), m_restartOnSelect(true)
+{
+ ui->setupUi(this);
+
+ ui->splitter->setSizes({200, 1});
+ ui->splitter->setStretchFactor(0, 0);
+ ui->splitter->setStretchFactor(1, 1);
+
+ m_model = new QStandardItemModel;
+ ui->list->setModel(m_model);
+
+ m_filter.setEdit(ui->filter);
+ m_filter.setList(ui->list);
+ m_filter.setUpdateDelay(false);
+ m_filter.setFilteredBorder(false);
+
+ updateInstances();
+ updateList();
+ selectActiveInstance();
+
+ connect(ui->createNew, &QPushButton::clicked, [&]{ createNew(); });
+
+ connect(ui->list->selectionModel(), &QItemSelectionModel::selectionChanged, [&]{ onSelection(); });
+ connect(ui->list, &QListView::activated, [&]{ openSelectedInstance(); });
+
+ connect(ui->rename, &QPushButton::clicked, [&]{ rename(); });
+ connect(ui->exploreLocation, &QPushButton::clicked, [&]{ exploreLocation(); });
+ connect(ui->exploreBaseDirectory, &QPushButton::clicked, [&]{ exploreBaseDirectory(); });
+ connect(ui->exploreGame, &QPushButton::clicked, [&]{ exploreGame(); });
+
+ connect(ui->convertToGlobal, &QPushButton::clicked, [&]{ convertToGlobal(); });
+ connect(ui->convertToPortable, &QPushButton::clicked, [&]{ convertToPortable(); });
+ connect(ui->openINI, &QPushButton::clicked, [&]{ openINI(); });
+ connect(ui->deleteInstance, &QPushButton::clicked, [&]{ deleteInstance(); });
+
+ connect(ui->switchToInstance, &QPushButton::clicked, [&]{ openSelectedInstance(); });
+ connect(ui->close, &QPushButton::clicked, [&]{ close(); });
+}
+
+void InstanceManagerDialog::updateInstances()
+{
+ auto& m = InstanceManager::singleton();
+
+ m_instances.clear();
+
+ for (auto&& d : m.instancePaths()) {
+ m_instances.push_back(std::make_unique<InstanceInfo>(d, false));
+ }
+
+ // sort first, prepend portable after so it's always on top
+ std::sort(m_instances.begin(), m_instances.end(), [](auto&& a, auto&& b) {
+ return (MOBase::naturalCompare(a->name(), b->name()) < 0);
+ });
+
+ if (m.portableInstanceExists()) {
+ m_instances.insert(
+ m_instances.begin(),
+ std::make_unique<InstanceInfo>(m.portablePath(), true));
+ }
+}
+
+void InstanceManagerDialog::updateList()
+{
+ const auto prevSelIndex = singleSelectionIndex();
+ const auto* prevSel = singleSelection();
+
+ m_model->clear();
+
+ const std::size_t NoSel = -1;
+ std::size_t sel = NoSel;
+
+ for (std::size_t i=0; i<m_instances.size(); ++i) {
+ const auto& ii = *m_instances[i];
+
+ auto* item = new QStandardItem(ii.name());
+ item->setIcon(ii.icon(m_pc));
+
+ m_model->appendRow(item);
+
+ if (&ii == prevSel) {
+ sel = i;
+ }
+ }
+
+ // keep current selection or select the next one if there was a selection;
+ // there's no selection when opening the dialog, that's handled in the ctor
+ if (prevSel) {
+ if (m_instances.empty()) {
+ select(-1);
+ } else {
+ if (sel == NoSel) {
+ if (prevSelIndex >= m_instances.size()) {
+ sel = m_instances.size() - 1;
+ } else {
+ sel = prevSelIndex;
+ }
+ }
+
+ select(sel);
+ }
+ }
+}
+
+void InstanceManagerDialog::select(std::size_t i)
+{
+ if (i < m_instances.size()) {
+ const auto& ii = m_instances[i];
+ fillData(*ii);
+
+ ui->list->selectionModel()->select(
+ m_filter.mapFromSource(m_filter.sourceModel()->index(i, 0)),
+ QItemSelectionModel::ClearAndSelect);
+ } else {
+ clearData();
+ }
+}
+
+void InstanceManagerDialog::select(const QString& name)
+{
+ for (std::size_t i=0; i<m_instances.size(); ++i) {
+ if (m_instances[i]->name() == name) {
+ select(i);
+ return;
+ }
+ }
+
+ log::error("can't select instance {}, not in list", name);
+}
+
+void InstanceManagerDialog::selectActiveInstance()
+{
+ const auto active = InstanceManager::singleton().currentInstance();
+
+ if (active) {
+ for (std::size_t i=0; i<m_instances.size(); ++i) {
+ if (m_instances[i]->name() == active->name()) {
+ select(i);
+
+ ui->list->scrollTo(
+ m_filter.mapFromSource(m_filter.sourceModel()->index(i, 0)));
+
+ return;
+ }
+ }
+ }
+
+ select(0);
+}
+
+void InstanceManagerDialog::openSelectedInstance()
+{
+ const auto i = singleSelectionIndex();
+ if (i == NoSelection) {
+ return;
+ }
+
+ if (m_instances[i]->isPortable()) {
+ InstanceManager::singleton().setCurrentInstance("");
+ } else {
+ InstanceManager::singleton().setCurrentInstance(m_instances[i]->name());
+ }
+
+ if (m_restartOnSelect) {
+ ExitModOrganizer(Exit::Restart);
+ }
+
+ accept();
+}
+
+QString getInstanceName(
+ QWidget* parent, const QString& title, const QString& moreText,
+ const QString& label, const QString& oldName={})
+{
+ auto& m = InstanceManager::singleton();
+
+ QDialog dlg(parent);
+ dlg.setWindowTitle(title);
+
+ auto* ly = new QVBoxLayout(&dlg);
+
+ auto* bb = new QDialogButtonBox(
+ QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
+
+ auto* text = new QLineEdit(oldName);
+ text->selectAll();
+
+ auto* error = new QLabel;
+
+ if (!moreText.isEmpty()) {
+ auto* lb = new QLabel(moreText);
+ lb->setWordWrap(true);
+ ly->addWidget(lb);
+ ly->addSpacing(10);
+ }
+
+ auto* lb = new QLabel(label);
+ lb->setWordWrap(true);
+ ly->addWidget(lb);
+
+ ly->addWidget(text);
+ ly->addWidget(error);
+ ly->addStretch();
+ ly->addWidget(bb);
+
+ auto check = [&] {
+ bool okay = false;
+
+ if (text->text().isEmpty()) {
+ error->setText("");
+ } else if (!m.validInstanceName(text->text())) {
+ error->setText(QObject::tr("The instance name must be a valid folder name."));
+ } else {
+ const auto name = m.sanitizeInstanceName(text->text());
+
+ if ((name != oldName) && m.instanceExists(text->text())) {
+ error->setText(QObject::tr("An instance with this name already exists."));
+ } else {
+ okay = true;
+ }
+ }
+
+ error->setVisible(!okay);
+ bb->button(QDialogButtonBox::Ok)->setEnabled(okay);
+ };
+
+ QObject::connect(text, &QLineEdit::textChanged, [&] { check(); });
+ QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); });
+ QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); });
+
+ check();
+
+ dlg.resize({400, 120});
+ if (dlg.exec() != QDialog::Accepted) {
+ return {};
+ }
+
+ return m.sanitizeInstanceName(text->text());
+}
+
+void InstanceManagerDialog::rename()
+{
+ auto* i = singleSelection();
+ if (!i) {
+ return;
+ }
+
+ const auto selIndex = singleSelectionIndex();
+
+ auto& m = InstanceManager::singleton();
+ if (i->isActive()) {
+ QMessageBox::information(this,
+ tr("Rename instance"), tr("The active instance cannot be renamed."));
+ return;
+ }
+
+ const auto newName = getInstanceName(
+ this, tr("Rename instance"), "", tr("Instance name"), i->name());
+
+ if (newName.isEmpty()) {
+ return;
+ }
+
+ const QString src = i->location();
+ const QString dest = QDir::toNativeSeparators(
+ QFileInfo(i->location()).dir().path() + "/" + newName);
+
+ const auto r = shell::Rename(src, dest, false);
+ if (!r) {
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("Failed to rename \"%1\" to \"%2\": %3")
+ .arg(src).arg(dest).arg(r.toString()));
+
+ return;
+ }
+
+ m_model->item(selIndex)->setText(newName);
+ i->setDir(dest);
+ fillData(*i);
+}
+
+void InstanceManagerDialog::exploreLocation()
+{
+ if (const auto* i=singleSelection()) {
+ shell::Explore(i->location());
+ }
+}
+
+void InstanceManagerDialog::exploreBaseDirectory()
+{
+ if (const auto* i=singleSelection()) {
+ shell::Explore(i->baseDirectory());
+ }
+}
+
+void InstanceManagerDialog::exploreGame()
+{
+ if (const auto* i=singleSelection()) {
+ shell::Explore(i->gamePath());
+ }
+}
+
+void InstanceManagerDialog::openINI()
+{
+ if (const auto* i=singleSelection()) {
+ shell::Open(i->iniFile());
+ }
+}
+
+void InstanceManagerDialog::deleteInstance()
+{
+ const auto* i = singleSelection();
+ if (!i) {
+ return;
+ }
+
+ auto& m = InstanceManager::singleton();
+ if (i->isActive()) {
+ QMessageBox::information(this,
+ tr("Deleting instance"), tr("The active instance cannot be deleted."));
+ return;
+ }
+
+ const auto Recycle = QMessageBox::Save;
+ const auto Delete = QMessageBox::Yes;
+ const auto Cancel = QMessageBox::Cancel;
+
+ const auto files = i->objectsForDeletion();
+
+ MOBase::TaskDialog dlg(this);
+
+ dlg
+ .title(tr("Deleting instance"))
+ .main(tr("These files and folders will be deleted"))
+ .content(tr("All checked items will be deleted."))
+ .icon(QMessageBox::Warning)
+ .button({tr("Move to the recycle bin"), Recycle})
+ .button({tr("Delete permanently"), Delete})
+ .button({tr("Cancel"), Cancel});
+
+
+ auto* list = new QListWidget();
+ list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ list->setMaximumHeight(160);
+
+ for (const auto& f : files) {
+ auto* item = new QListWidgetItem(f.path);
+
+ if (f.mandatoryDelete) {
+ item->setFlags(item->flags() & (~Qt::ItemIsEnabled));
+ } else {
+ item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+ }
+
+ item->setCheckState(Qt::Checked);
+ list->addItem(item);
+ }
+
+ dlg.addContent(list);
+ dlg.setWidth(600);
+
+
+ const auto r = dlg.exec();
+
+ if (r != Recycle && r != Delete) {
+ return;
+ }
+
+
+ QStringList selected;
+
+ for (int i=0; i<list->count(); ++i) {
+ if (list->item(i)->checkState() == Qt::Checked) {
+ selected.append(list->item(i)->text());
+ }
+ }
+
+ if (selected.isEmpty()) {
+ QMessageBox::information(
+ this, tr("Deleting instance"), tr("Nothing to delete."));
+
+ return;
+ }
+
+ if (!doDelete(selected, (r == Recycle))) {
+ return;
+ }
+
+ updateInstances();
+ updateList();
+}
+
+
+void InstanceManagerDialog::setRestartOnSelect(bool b)
+{
+ m_restartOnSelect = b;
+}
+
+bool InstanceManagerDialog::doDelete(const QStringList& files, bool recycle)
+{
+ if (MOBase::shellDelete(files, recycle, this)) {
+ return true;
+ }
+
+ const auto e = GetLastError();
+ if (e == ERROR_CANCELLED) {
+ log::debug("deletion cancelled by user");
+ } else {
+ log::error("failed to delete, {}", formatSystemMessage(e));
+ }
+
+ return false;
+}
+
+void InstanceManagerDialog::convertToGlobal()
+{
+ // not implemented
+}
+
+void InstanceManagerDialog::convertToPortable()
+{
+ // not implemented
+}
+
+void InstanceManagerDialog::onSelection()
+{
+ const auto i = singleSelectionIndex();
+ if (i == NoSelection) {
+ return;
+ }
+
+ select(i);
+}
+
+void InstanceManagerDialog::createNew()
+{
+ CreateInstanceDialog dlg(m_pc, &Settings::instance(), this);
+ if (dlg.exec() != QDialog::Accepted) {
+ return;
+ }
+
+ if (dlg.switching()) {
+ // restarting MO
+ return;
+ }
+
+ updateInstances();
+ updateList();
+
+ select(dlg.instanceName());
+}
+
+std::size_t InstanceManagerDialog::singleSelectionIndex() const
+{
+ const auto sel = m_filter.mapSelectionToSource(
+ ui->list->selectionModel()->selection());
+
+ if (sel.size() != 1) {
+ return NoSelection;
+ }
+
+ return static_cast<std::size_t>(sel.indexes()[0].row());
+}
+
+InstanceInfo* InstanceManagerDialog::singleSelection()
+{
+ const auto i = singleSelectionIndex();
+ if (i == NoSelection) {
+ return nullptr;
+ }
+
+ return m_instances[i].get();
+}
+
+const InstanceInfo* InstanceManagerDialog::singleSelection() const
+{
+ const auto i = singleSelectionIndex();
+ if (i == NoSelection) {
+ return nullptr;
+ }
+
+ return m_instances[i].get();
+}
+
+void InstanceManagerDialog::fillData(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());
+ setButtonsEnabled(true);
+
+ const auto& m = InstanceManager::singleton();
+
+ ui->rename->setEnabled(!ii.isPortable());
+
+ if (ii.isPortable()) {
+ ui->convertToPortable->setVisible(false);
+ ui->convertToGlobal->setVisible(true);
+ ui->convertToGlobal->setEnabled(true);
+ } else {
+ ui->convertToPortable->setVisible(true);
+ ui->convertToGlobal->setVisible(false);
+
+ if (m.portableInstanceExists()) {
+ ui->convertToPortable->setEnabled(false);
+ ui->convertToPortable->setToolTip(tr("A portable instance already exists."));
+ } else {
+ ui->convertToPortable->setEnabled(false);
+ ui->convertToPortable->setToolTip("");
+ }
+ }
+
+ // not implemented, hide the buttons
+ ui->convertToPortable->setVisible(false);
+ ui->convertToGlobal->setVisible(false);
+}
+
+void InstanceManagerDialog::clearData()
+{
+ ui->name->clear();
+ ui->location->clear();
+ ui->baseDirectory->clear();
+ ui->gameName->clear();
+ ui->gameDir->clear();
+
+ setButtonsEnabled(false);
+
+ ui->convertToPortable->setVisible(false);
+ ui->convertToGlobal->setVisible(false);
+}
+
+void InstanceManagerDialog::setButtonsEnabled(bool b)
+{
+ ui->rename->setEnabled(b);
+ ui->exploreLocation->setEnabled(b);
+ ui->exploreBaseDirectory->setEnabled(b);
+ ui->exploreGame->setEnabled(b);
+ ui->convertToPortable->setEnabled(b);
+ ui->convertToGlobal->setEnabled(b);
+ ui->deleteInstance->setEnabled(b);
+ ui->switchToInstance->setEnabled(b);
+}