Rename instance's physical dir when renaming instances (#3550)

This commit is contained in:
Alexandru Ionut Tripon 2025-04-02 09:03:08 +03:00 committed by GitHub
commit 40ff0321f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 378 additions and 114 deletions

View File

@ -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

View File

@ -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();

View File

@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
QString name() const;
void setName(QString val);
/// Sync name and rename instance dir accordingly; returns true if successful
bool syncInstanceDirName(const QString& newRoot) const;
/// Value used for instance window titles
QString windowTitle() const;

View File

@ -21,6 +21,8 @@ set(CORE_SOURCES
BaseVersion.h
BaseInstance.h
BaseInstance.cpp
InstanceDirUpdate.h
InstanceDirUpdate.cpp
NullInstance.h
MMCZip.h
MMCZip.cpp

View File

@ -0,0 +1,126 @@
// 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 <https://www.gnu.org/licenses/>.
*
* 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 <QCheckBox>
#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. <br />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: <br/>"
" - Old instance root: %1<br/>"
" - New instance root: %2<br/>"
"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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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);

View File

@ -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());

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
};

View File

@ -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());

View File

@ -112,40 +112,76 @@
<string>Folders</string>
</property>
<layout class="QGridLayout" name="foldersBoxLayout">
<item row="8" column="0">
<widget class="QLabel" name="labelDownloadsDir">
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<property name="text">
<string>&amp;Downloads:</string>
<string>I&amp;nstances:</string>
</property>
<property name="buddy">
<cstring>downloadsDirTextBox</cstring>
<cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QToolButton" name="downloadsDirBrowseBtn">
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="instDirTextBox"/>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="instDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="javaDirTextBox"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelSkinsDir">
<item row="1" column="1">
<widget class="QLabel" name="labelRenamingBehavior">
<property name="text">
<string>&amp;Skins:</string>
</property>
<property name="buddy">
<cstring>skinsDirTextBox</cstring>
<string>Rename instance folders</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QComboBox" name="renamingBehaviorComboBox">
<item>
<property name="text">
<string>Never</string>
</property>
</item>
<item>
<property name="text">
<string>Ask</string>
</property>
</item>
<item>
<property name="text">
<string>Always</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods:</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="modsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelIconsDir">
<property name="text">
<string>&amp;Icons:</string>
@ -155,7 +191,81 @@
</property>
</widget>
</item>
<item row="9" column="1" colspan="2">
<item row="3" column="1" colspan="2">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="3" column="3">
<widget class="QToolButton" name="iconsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelJavaDir">
<property name="text">
<string>&amp;Java:</string>
</property>
<property name="buddy">
<cstring>javaDirTextBox</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="javaDirTextBox"/>
</item>
<item row="4" column="3">
<widget class="QToolButton" name="javaDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelSkinsDir">
<property name="text">
<string>&amp;Skins:</string>
</property>
<property name="buddy">
<cstring>skinsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="5" column="3">
<widget class="QToolButton" name="skinsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="labelDownloadsDir">
<property name="text">
<string>&amp;Downloads:</string>
</property>
<property name="buddy">
<cstring>downloadsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="6" column="3">
<widget class="QToolButton" name="downloadsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="7" column="1" colspan="3">
<layout class="QHBoxLayout" name="downloadModsCheckLayout">
<item>
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
@ -179,83 +289,6 @@
</item>
</layout>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelJavaDir">
<property name="text">
<string>&amp;Java:</string>
</property>
<property name="buddy">
<cstring>javaDirTextBox</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods:</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="modsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="instDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="iconsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<property name="text">
<string>I&amp;nstances:</string>
</property>
<property name="buddy">
<cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="javaDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QToolButton" name="skinsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>