diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c3477d331..816f7b8ab 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -709,7 +709,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ToolbarsLocked", false); + // Instance m_settings->registerSetting("InstSortMode", "Name"); + m_settings->registerSetting("InstRenamingMode", "AskEverytime"); m_settings->registerSetting("SelectedInstance", QString()); // Window state and geometry diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index ccfd0b847..eab91a5eb 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -386,6 +386,12 @@ void BaseInstance::setName(QString val) emit propertiesChanged(this); } +bool BaseInstance::syncInstanceDirName(const QString& newRoot) const +{ + auto oldRoot = instanceRoot(); + return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); +} + QString BaseInstance::name() const { return m_settings->get("name").toString(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 9827a08b4..2a2b4dc4a 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceDirUpdate.h" + +#include + +#include "Application.h" +#include "FileSystem.h" + +#include "InstanceList.h" +#include "ui/dialogs/CustomMessageBox.h" + +QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent) +{ + if (oldName == newName) + return QString(); + + QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); + if (renamingMode == "MetadataOnly") + return QString(); + + auto oldRoot = instance->instanceRoot(); + auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); + auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); + if (oldRoot == newRoot) + return QString(); + if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) + return QString(); + + // Check for conflict + if (QDir(newRoot).exists()) { + QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), + QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.").arg(newRoot)); + return QString(); + } + + // Ask if we should rename + if (renamingMode == "AskEverytime") { + auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); + auto dialog = + CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"), + QObject::tr("Would you also like to rename the instance folder?\n\n" + "Old name: %1\n" + "New name: %2") + .arg(oldName, newName), + QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox); + + auto res = dialog->exec(); + if (checkBox->isChecked()) { + if (res == QMessageBox::Yes) + APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); + else + APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); + } + if (res == QMessageBox::No) + return QString(); + } + + // Check for linked instances + if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) + return QString(); + + // Now we can confirm that a renaming is happening + if (!instance->syncInstanceDirName(newRoot)) { + QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), + QObject::tr("An error occurred when performing the following renaming operation:
" + " - Old instance root: %1
" + " - New instance root: %2
" + "Only the metadata is renamed.") + .arg(oldRoot, newRoot)); + return QString(); + } + return newRoot; +} + +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) +{ + auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); + if (!linkedInstances.empty()) { + auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"), + QObject::tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "%2 it could break the other instance(s), \n\n" + "Do you wish to proceed?", + nullptr, linkedInstances.count()) + .arg(linkedInstances.join("\n")) + .arg(verb), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if (response != QMessageBox::Yes) + return false; + } + return true; +} diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h new file mode 100644 index 000000000..b92a59c4c --- /dev/null +++ b/launcher/InstanceDirUpdate.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseInstance.h" + +/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened +QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent); + +/// Check if there are linked instances, and display a warning; return true if the operation should proceed +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a9473ac15..ddf726373 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -123,6 +123,7 @@ #include "KonamiCode.h" #include "InstanceCopyTask.h" +#include "InstanceDirUpdate.h" #include "Json.h" @@ -288,10 +289,27 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi view->setSelectionMode(QAbstractItemView::SingleSelection); // FIXME: leaks ListViewDelegate - view->setItemDelegate(new ListViewDelegate(this)); + auto delegate = new ListViewDelegate(this); + view->setItemDelegate(delegate); view->setFrameShape(QFrame::NoFrame); // do not show ugly blue border on the mac view->setAttribute(Qt::WA_MacShowFocusRect, false); + connect(delegate, &ListViewDelegate::textChanged, this, [this](QString before, QString after) { + if (auto newRoot = askToUpdateInstanceDirName(m_selectedInstance, before, after, this); !newRoot.isEmpty()) { + auto oldID = m_selectedInstance->id(); + auto newID = QFileInfo(newRoot).fileName(); + QString origGroup(APPLICATION->instances()->getInstanceGroup(oldID)); + bool syncGroup = origGroup != GroupId() && oldID != newID; + if (syncGroup) + APPLICATION->instances()->setInstanceGroup(oldID, GroupId()); + + refreshInstances(); + setSelectedInstanceById(newID); + + if (syncGroup) + APPLICATION->instances()->setInstanceGroup(newID, origGroup); + } + }); view->installEventFilter(this); view->setContextMenuPolicy(Qt::CustomContextMenu); @@ -1377,20 +1395,8 @@ void MainWindow::on_actionDeleteInstance_triggered() if (response != QMessageBox::Yes) return; - auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); - if (!linkedInstances.empty()) { - response = CustomMessageBox::selectable(this, tr("There are linked instances"), - tr("The following instance(s) might reference files in this instance:\n\n" - "%1\n\n" - "Deleting it could break the other instance(s), \n\n" - "Do you wish to proceed?", - nullptr, linkedInstances.count()) - .arg(linkedInstances.join("\n")), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - if (response != QMessageBox::Yes) - return; - } + if (!checkLinkedInstances(id, this, tr("Deleting"))) + return; if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); diff --git a/launcher/ui/dialogs/CustomMessageBox.cpp b/launcher/ui/dialogs/CustomMessageBox.cpp index 1af47a449..ca0fe99e0 100644 --- a/launcher/ui/dialogs/CustomMessageBox.cpp +++ b/launcher/ui/dialogs/CustomMessageBox.cpp @@ -21,7 +21,8 @@ QMessageBox* selectable(QWidget* parent, const QString& text, QMessageBox::Icon icon, QMessageBox::StandardButtons buttons, - QMessageBox::StandardButton defaultButton) + QMessageBox::StandardButton defaultButton, + QCheckBox* checkBox) { QMessageBox* messageBox = new QMessageBox(parent); messageBox->setWindowTitle(title); @@ -31,6 +32,8 @@ QMessageBox* selectable(QWidget* parent, messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBox->setIcon(icon); messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + if (checkBox) + messageBox->setCheckBox(checkBox); return messageBox; } diff --git a/launcher/ui/dialogs/CustomMessageBox.h b/launcher/ui/dialogs/CustomMessageBox.h index a9bc6a24a..1ee29903e 100644 --- a/launcher/ui/dialogs/CustomMessageBox.h +++ b/launcher/ui/dialogs/CustomMessageBox.h @@ -23,5 +23,6 @@ QMessageBox* selectable(QWidget* parent, const QString& text, QMessageBox::Icon icon = QMessageBox::NoIcon, QMessageBox::StandardButtons buttons = QMessageBox::Ok, - QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton, + QCheckBox* checkBox = nullptr); } diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index d947163bc..c7115801e 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -397,6 +397,7 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, // Prevent instance names longer than 128 chars text.truncate(128); if (text.size() != 0) { + emit textChanged(model->data(index).toString(), text); model->setData(index, text); } } diff --git a/launcher/ui/instanceview/InstanceDelegate.h b/launcher/ui/instanceview/InstanceDelegate.h index 69dd32ba7..98ff9a2fc 100644 --- a/launcher/ui/instanceview/InstanceDelegate.h +++ b/launcher/ui/instanceview/InstanceDelegate.h @@ -33,6 +33,9 @@ class ListViewDelegate : public QStyledItemDelegate { void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + signals: + void textChanged(QString before, QString after) const; + private slots: void editingDone(); }; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 04ee01b00..d89a65a85 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -65,6 +65,15 @@ enum InstSortMode { Sort_LastLaunch }; +enum InstRenamingMode { + // Rename metadata only. + Rename_Always, + // Ask everytime. + Rename_Ask, + // Rename physical directory too. + Rename_Never +}; + LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); @@ -234,6 +243,7 @@ void LauncherPage::applySettings() s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked()); + // Instance auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); switch (sortMode) { case Sort_LastLaunch: @@ -245,6 +255,20 @@ void LauncherPage::applySettings() break; } + auto renamingMode = (InstRenamingMode)ui->renamingBehaviorComboBox->currentIndex(); + switch (renamingMode) { + case Rename_Always: + s->set("InstRenamingMode", "MetadataOnly"); + break; + case Rename_Never: + s->set("InstRenamingMode", "PhysicalDir"); + break; + case Rename_Ask: + default: + s->set("InstRenamingMode", "AskEverytime"); + break; + } + // Cat s->set("CatOpacity", ui->catOpacitySpinBox->value()); @@ -299,14 +323,25 @@ void LauncherPage::loadSettings() ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool()); + // Instance QString sortMode = s->get("InstSortMode").toString(); - if (sortMode == "LastLaunch") { ui->sortLastLaunchedBtn->setChecked(true); } else { ui->sortByNameBtn->setChecked(true); } + QString renamingMode = s->get("InstRenamingMode").toString(); + InstRenamingMode renamingModeEnum; + if (renamingMode == "MetadataOnly") { + renamingModeEnum = Rename_Always; + } else if (renamingMode == "PhysicalDir") { + renamingModeEnum = Rename_Never; + } else { + renamingModeEnum = Rename_Ask; + } + ui->renamingBehaviorComboBox->setCurrentIndex(renamingModeEnum); + // Cat ui->catOpacitySpinBox->setValue(s->get("CatOpacity").toInt()); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 31c878f3e..7942f4f4f 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -112,40 +112,76 @@ Folders - - + + - &Downloads: + I&nstances: - downloadsDirTextBox + instDirTextBox - - + + + + + Browse - - - - - - - - + + + - &Skins: - - - skinsDirTextBox + Rename instance folders + + + + + Never + + + + + Ask + + + + + Always + + + + + + + + &Mods: + + + modsDirTextBox + + + + + + + + + + Browse + + + + + &Icons: @@ -155,7 +191,81 @@ - + + + + + + + Browse + + + + + + + + &Java: + + + javaDirTextBox + + + + + + + + + + Browse + + + + + + + + &Skins: + + + skinsDirTextBox + + + + + + + + + + Browse + + + + + + + + &Downloads: + + + downloadsDirTextBox + + + + + + + + + + Browse + + + + + @@ -179,83 +289,6 @@ - - - - - - - &Java: - - - javaDirTextBox - - - - - - - &Mods: - - - modsDirTextBox - - - - - - - - - - - - - - - - Browse - - - - - - - Browse - - - - - - - Browse - - - - - - - I&nstances: - - - instDirTextBox - - - - - - - Browse - - - - - - - Browse - - -